Compare commits
85 Commits
format-uns
...
x11_debug
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
804b00c12a | ||
|
|
f724b2c171 | ||
|
|
70ce06cb95 | ||
|
|
9a5b97db00 | ||
|
|
0b75afd322 | ||
|
|
4fd698a093 | ||
|
|
b50846205c | ||
|
|
a574036efd | ||
|
|
89641acf2f | ||
|
|
611bf2d905 | ||
|
|
f476a8bc2a | ||
|
|
d3d0d01571 | ||
|
|
29d29f5a90 | ||
|
|
9824e40878 | ||
|
|
1ad8d6ab1c | ||
|
|
8745719687 | ||
|
|
c7c19609b3 | ||
|
|
428c143fbb | ||
|
|
f3460d440c | ||
|
|
071270fe88 | ||
|
|
a59dd7d06d | ||
|
|
868284876d | ||
|
|
6bbe9a2253 | ||
|
|
7a05db6d3d | ||
|
|
3587e9726b | ||
|
|
a96782cc6b | ||
|
|
0289c312c9 | ||
|
|
63a8095879 | ||
|
|
1768c0d996 | ||
|
|
27e9c68988 | ||
|
|
ad2ddf1200 | ||
|
|
d6e271c956 | ||
|
|
da29e33f50 | ||
|
|
3fd118f8e1 | ||
|
|
27beb9e697 | ||
|
|
c7d56302d2 | ||
|
|
74cb92f9cc | ||
|
|
8a659b0c60 | ||
|
|
25050e8027 | ||
|
|
2d9479667f | ||
|
|
1c617474fe | ||
|
|
1a0708f28c | ||
|
|
62e790074c | ||
|
|
c5b22eee2d | ||
|
|
e4bb666eab | ||
|
|
910f668f4d | ||
|
|
8e79609288 | ||
|
|
47122a3115 | ||
|
|
edd613062a | ||
|
|
3cd6719b30 | ||
|
|
afc0650a49 | ||
|
|
14c2fab8ab | ||
|
|
c752763301 | ||
|
|
2f65c3c6e6 | ||
|
|
959f0dcded | ||
|
|
be2df79d5c | ||
|
|
344e5e1cf2 | ||
|
|
ed86b86dc7 | ||
|
|
726f23e867 | ||
|
|
b1efea1100 | ||
|
|
2b21c89e3c | ||
|
|
d0fa012bf8 | ||
|
|
338df5de1d | ||
|
|
5f98b9617a | ||
|
|
18e2b43d6d | ||
|
|
5e3d85c023 | ||
|
|
ae55d35f19 | ||
|
|
d0142b820f | ||
|
|
b218d8778d | ||
|
|
de8ef08143 | ||
|
|
66b73c2d60 | ||
|
|
ab8d25e0a2 | ||
|
|
95e360b170 | ||
|
|
f0d979576d | ||
|
|
fbcc5ccdb9 | ||
|
|
29b5253a1d | ||
|
|
94c3101fb0 | ||
|
|
a6e0c8aca1 | ||
|
|
d12b8c3945 | ||
|
|
356fcec337 | ||
|
|
08123a270a | ||
|
|
6eb8e83411 | ||
|
|
4c51ee7816 | ||
|
|
660cf214c7 | ||
|
|
b2565fadfb |
75
Cargo.lock
generated
75
Cargo.lock
generated
@@ -88,9 +88,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alacritty_terminal"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6d1ea4484c8676f295307a4892d478c70ac8da1dbd8c7c10830a504b7f1022f"
|
||||
version = "0.24.1-dev"
|
||||
source = "git+https://github.com/alacritty/alacritty?rev=cacdb5bb3b72bad2c729227537979d95af75978f#cacdb5bb3b72bad2c729227537979d95af75978f"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"bitflags 2.4.2",
|
||||
@@ -107,7 +106,7 @@ dependencies = [
|
||||
"signal-hook",
|
||||
"unicode-width",
|
||||
"vte",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -347,13 +346,13 @@ dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"feature_flags",
|
||||
"file_icons",
|
||||
"fs",
|
||||
"futures 0.3.28",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"gray_matter",
|
||||
"heed",
|
||||
"html_to_markdown",
|
||||
"http 0.1.0",
|
||||
"indoc",
|
||||
"language",
|
||||
@@ -368,7 +367,6 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rope",
|
||||
"rustdoc_to_markdown",
|
||||
"schemars",
|
||||
"search",
|
||||
"semantic_index",
|
||||
@@ -2395,6 +2393,7 @@ dependencies = [
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3393,7 +3392,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
|
||||
dependencies = [
|
||||
"libloading 0.7.4",
|
||||
"libloading 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4788,18 +4787,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"
|
||||
@@ -5079,6 +5066,18 @@ dependencies = [
|
||||
"syn 2.0.59",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html_to_markdown"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"html5ever",
|
||||
"indoc",
|
||||
"markup5ever_rcdom",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.1.0"
|
||||
@@ -5954,12 +5953,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"
|
||||
@@ -7834,6 +7827,7 @@ dependencies = [
|
||||
"unicase",
|
||||
"util",
|
||||
"workspace",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8635,18 +8629,6 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustdoc_to_markdown"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"html5ever",
|
||||
"indoc",
|
||||
"markup5ever_rcdom",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.37.23"
|
||||
@@ -13079,15 +13061,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"
|
||||
@@ -13178,7 +13151,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.139.0"
|
||||
version = "0.140.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -13313,7 +13286,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_elixir"
|
||||
version = "0.0.4"
|
||||
version = "0.0.5"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
@@ -13412,7 +13385,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_php"
|
||||
version = "0.0.5"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
@@ -13468,7 +13441,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_vue"
|
||||
version = "0.0.2"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
17
Cargo.toml
17
Cargo.toml
@@ -41,6 +41,7 @@ members = [
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/headless",
|
||||
"crates/html_to_markdown",
|
||||
"crates/http",
|
||||
"crates/image_viewer",
|
||||
"crates/inline_completion_button",
|
||||
@@ -76,7 +77,6 @@ members = [
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rustdoc_to_markdown",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/search",
|
||||
@@ -150,6 +150,7 @@ assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_tooling = { path = "crates/assistant_tooling" }
|
||||
async-watch = "0.3.1"
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
base64 = "0.13"
|
||||
@@ -166,6 +167,7 @@ color = { path = "crates/color" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
dashmap = "5.5.3"
|
||||
db = { path = "crates/db" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
@@ -185,6 +187,7 @@ google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui" }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
headless = { path = "crates/headless" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http = { path = "crates/http" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
@@ -221,7 +224,6 @@ dev_server_projects = { path = "crates/dev_server_projects" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rustdoc_to_markdown = { path = "crates/rustdoc_to_markdown" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
search = { path = "crates/search" }
|
||||
@@ -461,6 +463,12 @@ codegen-units = 1
|
||||
[profile.release.package]
|
||||
zed = { codegen-units = 16 }
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
lto = false
|
||||
codegen-units = 16
|
||||
|
||||
[workspace.lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
todo = "deny"
|
||||
@@ -494,10 +502,5 @@ non_canonical_partial_ord_impl = "allow"
|
||||
reversed_empty_ranges = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
'cfg(gles)', # used in gpui
|
||||
] }
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"ib": "storage",
|
||||
"ico": "image",
|
||||
"ini": "settings",
|
||||
"inl": "cpp",
|
||||
"j2k": "image",
|
||||
"java": "java",
|
||||
"jfif": "image",
|
||||
|
||||
1
assets/icons/search_selection.svg
Normal file
1
assets/icons/search_selection.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>
|
||||
|
After Width: | Height: | Size: 299 B |
1
assets/icons/sparkle.svg
Normal file
1
assets/icons/sparkle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkle"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 481 B |
3
assets/icons/sparkle_filled.svg
Normal file
3
assets/icons/sparkle_filled.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.937 15.5C9.84772 15.1539 9.66734 14.8381 9.41462 14.5854C9.1619 14.3327 8.84607 14.1523 8.5 14.063L2.365 12.481C2.26033 12.4513 2.16821 12.3883 2.10261 12.3014C2.03702 12.2146 2.00153 12.1088 2.00153 12C2.00153 11.8912 2.03702 11.7854 2.10261 11.6986C2.16821 11.6118 2.26033 11.5487 2.365 11.519L8.5 9.93601C8.84595 9.84681 9.16169 9.66658 9.4144 9.41404C9.66711 9.16151 9.84757 8.84589 9.937 8.50001L11.519 2.36501C11.5484 2.25992 11.6114 2.16735 11.6983 2.1014C11.7853 2.03545 11.8914 1.99976 12.0005 1.99976C12.1096 1.99976 12.2157 2.03545 12.3027 2.1014C12.3896 2.16735 12.4526 2.25992 12.482 2.36501L14.063 8.50001C14.1523 8.84608 14.3327 9.1619 14.5854 9.41462C14.8381 9.66734 15.1539 9.84773 15.5 9.93701L21.635 11.518C21.7405 11.5471 21.8335 11.61 21.8998 11.6971C21.9661 11.7841 22.0021 11.8906 22.0021 12C22.0021 12.1094 21.9661 12.2159 21.8998 12.3029C21.8335 12.39 21.7405 12.4529 21.635 12.482L15.5 14.063C15.1539 14.1523 14.8381 14.3327 14.5854 14.5854C14.3327 14.8381 14.1523 15.1539 14.063 15.5L12.481 21.635C12.4516 21.7401 12.3886 21.8327 12.3017 21.8986C12.2147 21.9646 12.1086 22.0003 11.9995 22.0003C11.8904 22.0003 11.7843 21.9646 11.6973 21.8986C11.6104 21.8327 11.5474 21.7401 11.518 21.635L9.937 15.5Z" fill="black" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
assets/icons/star.svg
Normal file
1
assets/icons/star.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.97942 1.25171L6.9585 1.30199L5.58662 4.60039C5.54342 4.70426 5.44573 4.77523 5.3336 4.78422L1.7727 5.0697L1.71841 5.07405L1.38687 5.10063L1.08608 5.12475C0.820085 5.14607 0.712228 5.47802 0.914889 5.65162L1.14406 5.84793L1.39666 6.06431L1.43802 6.09974L4.15105 8.42374C4.23648 8.49692 4.2738 8.61176 4.24769 8.72118L3.41882 12.196L3.40618 12.249L3.32901 12.5725L3.25899 12.866C3.19708 13.1256 3.47945 13.3308 3.70718 13.1917L3.9647 13.0344L4.24854 12.861L4.29502 12.8326L7.34365 10.9705C7.43965 10.9119 7.5604 10.9119 7.6564 10.9705L10.705 12.8326L10.7515 12.861L11.0354 13.0344L11.2929 13.1917C11.5206 13.3308 11.803 13.1256 11.7411 12.866L11.671 12.5725L11.5939 12.249L11.5812 12.196L10.7524 8.72118C10.7263 8.61176 10.7636 8.49692 10.849 8.42374L13.562 6.09974L13.6034 6.06431L13.856 5.84793L14.0852 5.65162C14.2878 5.47802 14.18 5.14607 13.914 5.12475L13.6132 5.10063L13.2816 5.07405L13.2274 5.0697L9.66645 4.78422C9.55432 4.77523 9.45663 4.70426 9.41343 4.60039L8.04155 1.30199L8.02064 1.25171L7.89291 0.944609L7.77702 0.665992C7.67454 0.419604 7.32551 0.419604 7.22303 0.665992L7.10715 0.944609L6.97942 1.25171ZM7.50003 2.60397L6.50994 4.98442C6.32273 5.43453 5.89944 5.74207 5.41351 5.78103L2.84361 5.98705L4.8016 7.66428C5.17183 7.98142 5.33351 8.47903 5.2204 8.95321L4.62221 11.461L6.8224 10.1171C7.23842 9.86302 7.76164 9.86302 8.17766 10.1171L10.3778 11.461L9.77965 8.95321C9.66654 8.47903 9.82822 7.98142 10.1984 7.66428L12.1564 5.98705L9.58654 5.78103C9.10061 5.74207 8.67732 5.43453 8.49011 4.98442L7.50003 2.60397Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
assets/icons/star_filled.svg
Normal file
1
assets/icons/star_filled.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.22303 0.665992C7.32551 0.419604 7.67454 0.419604 7.77702 0.665992L9.41343 4.60039C9.45663 4.70426 9.55432 4.77523 9.66645 4.78422L13.914 5.12475C14.18 5.14607 14.2878 5.47802 14.0852 5.65162L10.849 8.42374C10.7636 8.49692 10.7263 8.61176 10.7524 8.72118L11.7411 12.866C11.803 13.1256 11.5206 13.3308 11.2929 13.1917L7.6564 10.9705C7.5604 10.9119 7.43965 10.9119 7.34365 10.9705L3.70718 13.1917C3.47945 13.3308 3.19708 13.1256 3.25899 12.866L4.24769 8.72118C4.2738 8.61176 4.23648 8.49692 4.15105 8.42374L0.914889 5.65162C0.712228 5.47802 0.820086 5.14607 1.08608 5.12475L5.3336 4.78422C5.44573 4.77523 5.54342 4.70426 5.58662 4.60039L7.22303 0.665992Z" fill="currentColor"></path></svg>
|
||||
|
After Width: | Height: | Size: 794 B |
5
assets/icons/zed_assistant_filled.svg
Normal file
5
assets/icons/zed_assistant_filled.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1017 B |
@@ -28,7 +28,6 @@
|
||||
"ctrl-0": "zed::ResetBufferFontSize",
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"alt-f9": "zed::Hide",
|
||||
"f11": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
@@ -206,15 +205,10 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
"ctrl-n": "prompt_library::NewPrompt",
|
||||
"ctrl-shift-s": "prompt_library::ToggleDefaultPrompt"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -226,7 +220,8 @@
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"ctrl-h": "search::ToggleReplace"
|
||||
"ctrl-h": "search::ToggleReplace",
|
||||
"ctrl-l": "search::ToggleSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -290,6 +285,7 @@
|
||||
"ctrl-alt-g": "search::SelectNextMatch",
|
||||
"ctrl-alt-shift-g": "search::SelectPrevMatch",
|
||||
"ctrl-alt-shift-h": "search::ToggleReplace",
|
||||
"ctrl-alt-shift-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-w": "search::ToggleWholeWord",
|
||||
@@ -548,6 +544,18 @@
|
||||
"ctrl-enter": "assistant::InlineAssist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
@@ -636,12 +644,7 @@
|
||||
"pagedown": ["terminal::SendKeystroke", "pagedown"],
|
||||
"escape": ["terminal::SendKeystroke", "escape"],
|
||||
"enter": ["terminal::SendKeystroke", "enter"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
|
||||
// Some nice conveniences
|
||||
"ctrl-backspace": ["terminal::SendText", "\u0015"],
|
||||
"ctrl-right": ["terminal::SendText", "\u0005"],
|
||||
"ctrl-left": ["terminal::SendText", "\u0001"]
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -176,6 +176,12 @@
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-alt-l": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"selection_search_enabled": true
|
||||
}
|
||||
],
|
||||
"cmd-e": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
@@ -233,6 +239,14 @@
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
"cmd-n": "prompt_library::NewPrompt",
|
||||
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
|
||||
"cmd-w": "workspace::CloseWindow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
@@ -242,7 +256,8 @@
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"cmd-f": "search::FocusSearch",
|
||||
"cmd-alt-f": "search::ToggleReplace"
|
||||
"cmd-alt-f": "search::ToggleReplace",
|
||||
"cmd-alt-l": "search::ToggleSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -308,6 +323,7 @@
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"cmd-alt-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||
"alt-cmd-w": "search::ToggleWholeWord",
|
||||
@@ -639,7 +655,7 @@
|
||||
{
|
||||
"context": "Picker",
|
||||
"bindings": {
|
||||
"alt-e": "picker::UseSelectedQuery",
|
||||
"f2": "picker::UseSelectedQuery",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
|
||||
}
|
||||
|
||||
@@ -379,8 +379,8 @@
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
"> >": "vim::Indent",
|
||||
"< <": "vim::Outdent",
|
||||
">": ["vim::PushOperator", "Indent"],
|
||||
"<": ["vim::PushOperator", "Outdent"],
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
// tree-sitter related commands
|
||||
@@ -459,6 +459,18 @@
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == >",
|
||||
"bindings": {
|
||||
">": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == <",
|
||||
"bindings": {
|
||||
"<": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
@@ -568,7 +580,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal",
|
||||
"context": "Editor && vim_mode == normal && !VimWaiting",
|
||||
"bindings": {
|
||||
"g c c": "editor::ToggleComments"
|
||||
}
|
||||
|
||||
@@ -131,14 +131,7 @@
|
||||
// The default number of lines to expand excerpts in the multibuffer by.
|
||||
"expand_excerpt_lines": 3,
|
||||
// Globs to match against file paths to determine if a file is private.
|
||||
"private_files": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/secrets.yml"
|
||||
],
|
||||
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||
"use_on_type_format": true,
|
||||
@@ -164,6 +157,12 @@
|
||||
// "none"
|
||||
// 3. Draw all invisible symbols:
|
||||
// "all"
|
||||
// 4. Draw whitespaces at boundaries only:
|
||||
// "boundaries"
|
||||
// For a whitespace to be on a boundary, any of the following conditions need to be met:
|
||||
// - It is a tab
|
||||
// - It is adjacent to an edge (start or end)
|
||||
// - It is adjacent to a whitespace (left or right)
|
||||
"show_whitespaces": "selection",
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
@@ -453,7 +452,8 @@
|
||||
// Send anonymized usage data like what languages you're using Zed with.
|
||||
"metrics": true
|
||||
},
|
||||
// Automatically update Zed
|
||||
// Automatically update Zed. This setting may be ignored on Linux if
|
||||
// installed through a package manager.
|
||||
"auto_update": true,
|
||||
// Diagnostics configuration.
|
||||
"diagnostics": {
|
||||
@@ -672,9 +672,6 @@
|
||||
"Elixir": {
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
},
|
||||
"Gleam": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
@@ -700,6 +697,7 @@
|
||||
}
|
||||
},
|
||||
"JavaScript": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -709,9 +707,6 @@
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Make": {
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"format_on_save": "off",
|
||||
"prettier": {
|
||||
@@ -724,9 +719,6 @@
|
||||
"plugins": ["@prettier/plugin-php"]
|
||||
}
|
||||
},
|
||||
"Prisma": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Ruby": {
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "..."]
|
||||
},
|
||||
@@ -748,6 +740,7 @@
|
||||
}
|
||||
},
|
||||
"TSX": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -758,6 +751,7 @@
|
||||
}
|
||||
},
|
||||
"TypeScript": {
|
||||
"language_servers": ["typescript-language-server", "!vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
|
||||
@@ -62,16 +62,16 @@ impl ActivityIndicator {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.statuses.retain(|s| s.name != name);
|
||||
this.statuses.push(LspStatus { name, status });
|
||||
cx.notify();
|
||||
cx.notify(); // commented back in
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach(); // commented back in
|
||||
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); // commented back in
|
||||
}
|
||||
|
||||
Self {
|
||||
|
||||
@@ -22,12 +22,13 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
file_icons.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
@@ -40,7 +41,6 @@ parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
rustdoc_to_markdown.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
semantic_index.workspace = true
|
||||
@@ -59,7 +59,6 @@ util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
picker.workspace = true
|
||||
gray_matter = "0.2.7"
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod codegen;
|
||||
mod completion_provider;
|
||||
mod conversation_store;
|
||||
mod inline_assistant;
|
||||
mod model_selector;
|
||||
mod prompt_library;
|
||||
mod prompts;
|
||||
mod saved_conversation;
|
||||
mod search;
|
||||
mod slash_command;
|
||||
mod streaming_diff;
|
||||
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
|
||||
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OpenAiModel};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
pub(crate) use conversation_store::*;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
pub(crate) use inline_assistant::*;
|
||||
pub(crate) use model_selector::*;
|
||||
pub(crate) use saved_conversation::*;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use slash_command::{
|
||||
active_command, default_command, fetch_command, file_command, project_command, prompt_command,
|
||||
rustdoc_command, search_command, tabs_command,
|
||||
};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
sync::Arc,
|
||||
};
|
||||
pub(crate) use streaming_diff::*;
|
||||
use util::paths::EMBEDDINGS_DIR;
|
||||
|
||||
actions!(
|
||||
@@ -80,14 +88,14 @@ impl Display for Role {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum LanguageModel {
|
||||
ZedDotDev(ZedDotDevModel),
|
||||
Cloud(CloudModel),
|
||||
OpenAi(OpenAiModel),
|
||||
Anthropic(AnthropicModel),
|
||||
}
|
||||
|
||||
impl Default for LanguageModel {
|
||||
fn default() -> Self {
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::default())
|
||||
LanguageModel::Cloud(CloudModel::default())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +104,7 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => format!("openai/{}", model.id()),
|
||||
LanguageModel::Anthropic(model) => format!("anthropic/{}", model.id()),
|
||||
LanguageModel::ZedDotDev(model) => format!("zed.dev/{}", model.id()),
|
||||
LanguageModel::Cloud(model) => format!("zed.dev/{}", model.id()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +112,7 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => model.display_name().into(),
|
||||
LanguageModel::Anthropic(model) => model.display_name().into(),
|
||||
LanguageModel::ZedDotDev(model) => model.display_name().into(),
|
||||
LanguageModel::Cloud(model) => model.display_name().into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +120,7 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => model.max_token_count(),
|
||||
LanguageModel::Anthropic(model) => model.max_token_count(),
|
||||
LanguageModel::ZedDotDev(model) => model.max_token_count(),
|
||||
LanguageModel::Cloud(model) => model.max_token_count(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +128,7 @@ impl LanguageModel {
|
||||
match self {
|
||||
LanguageModel::OpenAi(model) => model.id(),
|
||||
LanguageModel::Anthropic(model) => model.id(),
|
||||
LanguageModel::ZedDotDev(model) => model.id(),
|
||||
LanguageModel::Cloud(model) => model.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,6 +173,20 @@ impl LanguageModelRequest {
|
||||
tools: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Before we send the request to the server, we can perform fixups on it appropriate to the model.
|
||||
pub fn preprocess(&mut self) {
|
||||
match &self.model {
|
||||
LanguageModel::OpenAi(_) => {}
|
||||
LanguageModel::Anthropic(_) => {}
|
||||
LanguageModel::Cloud(model) => match model {
|
||||
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku => {
|
||||
preprocess_anthropic_request(self);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
@@ -251,9 +273,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
completion_provider::init(client, cx);
|
||||
|
||||
prompt_library::init(cx);
|
||||
completion_provider::init(client.clone(), cx);
|
||||
assistant_slash_command::init(cx);
|
||||
register_slash_commands(cx);
|
||||
assistant_panel::init(cx);
|
||||
inline_assistant::init(client.telemetry().clone(), cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Assistant::NAMESPACE);
|
||||
@@ -266,13 +292,25 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
cx.observe_global::<SettingsStore>(|cx| {
|
||||
Assistant::update_global(cx, |assistant, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
assistant.set_enabled(settings.enabled, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn register_slash_commands(cx: &mut AppContext) {
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
slash_command_registry.register_command(file_command::FileSlashCommand, true);
|
||||
slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
|
||||
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
|
||||
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
|
||||
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
|
||||
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
|
||||
slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
|
||||
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false);
|
||||
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,10 +14,10 @@ use serde::{
|
||||
use settings::{Settings, SettingsSources};
|
||||
use strum::{EnumIter, IntoEnumIterator};
|
||||
|
||||
use crate::LanguageModel;
|
||||
use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, EnumIter)]
|
||||
pub enum ZedDotDevModel {
|
||||
pub enum CloudModel {
|
||||
Gpt3Point5Turbo,
|
||||
Gpt4,
|
||||
Gpt4Turbo,
|
||||
@@ -29,7 +29,7 @@ pub enum ZedDotDevModel {
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl Serialize for ZedDotDevModel {
|
||||
impl Serialize for CloudModel {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
@@ -38,7 +38,7 @@ impl Serialize for ZedDotDevModel {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ZedDotDevModel {
|
||||
impl<'de> Deserialize<'de> for CloudModel {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
@@ -46,7 +46,7 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
|
||||
struct ZedDotDevModelVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ZedDotDevModelVisitor {
|
||||
type Value = ZedDotDevModel;
|
||||
type Value = CloudModel;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string for a ZedDotDevModel variant or a custom model")
|
||||
@@ -56,9 +56,9 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
let model = ZedDotDevModel::iter()
|
||||
let model = CloudModel::iter()
|
||||
.find(|model| model.id() == value)
|
||||
.unwrap_or_else(|| ZedDotDevModel::Custom(value.to_string()));
|
||||
.unwrap_or_else(|| CloudModel::Custom(value.to_string()));
|
||||
Ok(model)
|
||||
}
|
||||
}
|
||||
@@ -67,13 +67,13 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for ZedDotDevModel {
|
||||
impl JsonSchema for CloudModel {
|
||||
fn schema_name() -> String {
|
||||
"ZedDotDevModel".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
let variants = ZedDotDevModel::iter()
|
||||
let variants = CloudModel::iter()
|
||||
.filter_map(|model| {
|
||||
let id = model.id();
|
||||
if id.is_empty() {
|
||||
@@ -88,7 +88,7 @@ impl JsonSchema for ZedDotDevModel {
|
||||
enum_values: Some(variants.iter().map(|s| s.clone().into()).collect()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
title: Some("ZedDotDevModel".to_owned()),
|
||||
default: Some(ZedDotDevModel::default().id().into()),
|
||||
default: Some(CloudModel::default().id().into()),
|
||||
examples: variants.into_iter().map(Into::into).collect(),
|
||||
..Default::default()
|
||||
})),
|
||||
@@ -97,7 +97,7 @@ impl JsonSchema for ZedDotDevModel {
|
||||
}
|
||||
}
|
||||
|
||||
impl ZedDotDevModel {
|
||||
impl CloudModel {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Self::Gpt3Point5Turbo => "gpt-3.5-turbo",
|
||||
@@ -133,6 +133,15 @@ impl ZedDotDevModel {
|
||||
Self::Custom(_) => 4096, // TODO: Make this configurable
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preprocess_request(&self, request: &mut LanguageModelRequest) {
|
||||
match self {
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => {
|
||||
preprocess_anthropic_request(request)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -147,7 +156,7 @@ pub enum AssistantDockPosition {
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AssistantProvider {
|
||||
ZedDotDev {
|
||||
model: ZedDotDevModel,
|
||||
model: CloudModel,
|
||||
},
|
||||
OpenAi {
|
||||
model: OpenAiModel,
|
||||
@@ -175,9 +184,7 @@ impl Default for AssistantProvider {
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContent {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev {
|
||||
default_model: Option<ZedDotDevModel>,
|
||||
},
|
||||
ZedDotDev { default_model: Option<CloudModel> },
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
@@ -281,7 +288,7 @@ impl AssistantSettingsContent {
|
||||
Some(AssistantProviderContent::ZedDotDev {
|
||||
default_model: model,
|
||||
}) => {
|
||||
if let LanguageModel::ZedDotDev(new_model) = new_model {
|
||||
if let LanguageModel::Cloud(new_model) = new_model {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
}
|
||||
@@ -302,7 +309,7 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
provider => match new_model {
|
||||
LanguageModel::ZedDotDev(model) => {
|
||||
LanguageModel::Cloud(model) => {
|
||||
*provider = Some(AssistantProviderContent::ZedDotDev {
|
||||
default_model: Some(model),
|
||||
})
|
||||
@@ -613,7 +620,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::ZedDotDev {
|
||||
model: ZedDotDevModel::Custom("custom".into())
|
||||
model: CloudModel::Custom("custom".into())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,696 +0,0 @@
|
||||
use crate::{
|
||||
streaming_diff::{Hunk, StreamingDiff},
|
||||
CompletionProvider, LanguageModelRequest,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::telemetry::Telemetry;
|
||||
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{EventEmitter, Model, ModelContext, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use std::{cmp, future, ops::Range, sync::Arc, time::Instant};
|
||||
|
||||
pub enum Event {
|
||||
Finished,
|
||||
Undone,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CodegenKind {
|
||||
Transform { range: Range<Anchor> },
|
||||
Generate { position: Anchor },
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
buffer: Model<MultiBuffer>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
kind: CodegenKind,
|
||||
last_equal_ranges: Vec<Range<Anchor>>,
|
||||
transaction_id: Option<TransactionId>,
|
||||
error: Option<anyhow::Error>,
|
||||
generation: Task<()>,
|
||||
idle: bool,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Codegen {}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
kind: CodegenKind,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
Self {
|
||||
buffer: buffer.clone(),
|
||||
snapshot,
|
||||
kind,
|
||||
last_equal_ranges: Default::default(),
|
||||
transaction_id: Default::default(),
|
||||
error: Default::default(),
|
||||
idle: true,
|
||||
generation: Task::ready(()),
|
||||
telemetry,
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
_buffer: Model<MultiBuffer>,
|
||||
event: &multi_buffer::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
|
||||
if self.transaction_id == Some(*transaction_id) {
|
||||
self.transaction_id = None;
|
||||
self.generation = Task::ready(());
|
||||
cx.emit(Event::Undone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range(&self) -> Range<Anchor> {
|
||||
match &self.kind {
|
||||
CodegenKind::Transform { range } => range.clone(),
|
||||
CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &CodegenKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
|
||||
&self.last_equal_ranges
|
||||
}
|
||||
|
||||
pub fn idle(&self) -> bool {
|
||||
self.idle
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<&anyhow::Error> {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
|
||||
let range = self.range();
|
||||
let snapshot = self.snapshot.clone();
|
||||
let selected_text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
.collect::<Rope>();
|
||||
|
||||
let selection_start = range.start.to_point(&snapshot);
|
||||
let suggested_line_indent = snapshot
|
||||
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
|
||||
.into_values()
|
||||
.next()
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
|
||||
|
||||
let model_telemetry_id = prompt.model.telemetry_id();
|
||||
let response = CompletionProvider::global(cx).complete(prompt);
|
||||
let telemetry = self.telemetry.clone();
|
||||
self.generation = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let generate = async {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
let diff = cx.background_executor().spawn(async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let chunks = strip_invalid_spans_from_codeblock(response.await?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
let mut new_text = String::new();
|
||||
let mut base_indent = None;
|
||||
let mut line_indent = None;
|
||||
let mut first_line = true;
|
||||
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
if response_latency.is_none() {
|
||||
response_latency = Some(request_start.elapsed());
|
||||
}
|
||||
let chunk = chunk?;
|
||||
|
||||
let mut lines = chunk.split('\n').peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
new_text.push_str(line);
|
||||
if line_indent.is_none() {
|
||||
if let Some(non_whitespace_ch_ix) =
|
||||
new_text.find(|ch: char| !ch.is_whitespace())
|
||||
{
|
||||
line_indent = Some(non_whitespace_ch_ix);
|
||||
base_indent = base_indent.or(line_indent);
|
||||
|
||||
let line_indent = line_indent.unwrap();
|
||||
let base_indent = base_indent.unwrap();
|
||||
let indent_delta =
|
||||
line_indent as i32 - base_indent as i32;
|
||||
let mut corrected_indent_len = cmp::max(
|
||||
0,
|
||||
suggested_line_indent.len as i32 + indent_delta,
|
||||
)
|
||||
as usize;
|
||||
if first_line {
|
||||
corrected_indent_len = corrected_indent_len
|
||||
.saturating_sub(
|
||||
selection_start.column as usize,
|
||||
);
|
||||
}
|
||||
|
||||
let indent_char = suggested_line_indent.char();
|
||||
let mut indent_buffer = [0; 4];
|
||||
let indent_str =
|
||||
indent_char.encode_utf8(&mut indent_buffer);
|
||||
new_text.replace_range(
|
||||
..line_indent,
|
||||
&indent_str.repeat(corrected_indent_len),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if line_indent.is_some() {
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
new_text.clear();
|
||||
}
|
||||
|
||||
if lines.peek().is_some() {
|
||||
hunks_tx.send(diff.push_new("\n")).await?;
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
hunks_tx.send(diff.finish()).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let error_message = diff.await.err().map(|error| error.to_string());
|
||||
if let Some(telemetry) = telemetry {
|
||||
telemetry.report_assistant_event(
|
||||
None,
|
||||
telemetry_events::AssistantKind::Inline,
|
||||
model_telemetry_id,
|
||||
response_latency,
|
||||
error_message,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
while let Some(hunks) = hunks_rx.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
|
||||
let transaction = this.buffer.update(cx, |buffer, cx| {
|
||||
// Avoid grouping assistant edits with user edits.
|
||||
buffer.finalize_last_transaction(cx);
|
||||
|
||||
buffer.start_transaction(cx);
|
||||
buffer.edit(
|
||||
hunks.into_iter().filter_map(|hunk| match hunk {
|
||||
Hunk::Insert { text } => {
|
||||
let edit_start = snapshot.anchor_after(edit_start);
|
||||
Some((edit_start..edit_start, text))
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
Some((edit_range, String::new()))
|
||||
}
|
||||
Hunk::Keep { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
this.last_equal_ranges.push(edit_range);
|
||||
None
|
||||
}
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
buffer.end_transaction(cx)
|
||||
});
|
||||
|
||||
if let Some(transaction) = transaction {
|
||||
if let Some(first_transaction) = this.transaction_id {
|
||||
// Group all assistant edits into the first transaction.
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_transactions(
|
||||
transaction,
|
||||
first_transaction,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.transaction_id = Some(transaction);
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
|
||||
diff.await;
|
||||
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = generate.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
this.idle = true;
|
||||
if let Err(error) = result {
|
||||
this.error = Some(error);
|
||||
}
|
||||
cx.emit(Event::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
self.error.take();
|
||||
self.idle = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction_id) = self.transaction_id {
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_invalid_spans_from_codeblock(
|
||||
stream: impl Stream<Item = Result<String>>,
|
||||
) -> impl Stream<Item = Result<String>> {
|
||||
let mut first_line = true;
|
||||
let mut buffer = String::new();
|
||||
let mut starts_with_markdown_codeblock = false;
|
||||
let mut includes_start_or_end_span = false;
|
||||
stream.filter_map(move |chunk| {
|
||||
let chunk = match chunk {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => return future::ready(Some(Err(err))),
|
||||
};
|
||||
buffer.push_str(&chunk);
|
||||
|
||||
if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
|
||||
includes_start_or_end_span = true;
|
||||
|
||||
buffer = buffer
|
||||
.strip_prefix("<|S|>")
|
||||
.or_else(|| buffer.strip_prefix("<|S|"))
|
||||
.unwrap_or(&buffer)
|
||||
.to_string();
|
||||
} else if buffer.ends_with("|E|>") {
|
||||
includes_start_or_end_span = true;
|
||||
} else if buffer.starts_with("<|")
|
||||
|| buffer.starts_with("<|S")
|
||||
|| buffer.starts_with("<|S|")
|
||||
|| buffer.ends_with('|')
|
||||
|| buffer.ends_with("|E")
|
||||
|| buffer.ends_with("|E|")
|
||||
{
|
||||
return future::ready(None);
|
||||
}
|
||||
|
||||
if first_line {
|
||||
if buffer.is_empty() || buffer == "`" || buffer == "``" {
|
||||
return future::ready(None);
|
||||
} else if buffer.starts_with("```") {
|
||||
starts_with_markdown_codeblock = true;
|
||||
if let Some(newline_ix) = buffer.find('\n') {
|
||||
buffer.replace_range(..newline_ix + 1, "");
|
||||
first_line = false;
|
||||
} else {
|
||||
return future::ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut text = buffer.to_string();
|
||||
if starts_with_markdown_codeblock {
|
||||
text = text
|
||||
.strip_suffix("\n```\n")
|
||||
.or_else(|| text.strip_suffix("\n```"))
|
||||
.or_else(|| text.strip_suffix("\n``"))
|
||||
.or_else(|| text.strip_suffix("\n`"))
|
||||
.or_else(|| text.strip_suffix('\n'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
}
|
||||
|
||||
if includes_start_or_end_span {
|
||||
text = text
|
||||
.strip_suffix("|E|>")
|
||||
.or_else(|| text.strip_suffix("E|>"))
|
||||
.or_else(|| text.strip_prefix("|>"))
|
||||
.or_else(|| text.strip_prefix('>'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
};
|
||||
|
||||
if text.contains('\n') {
|
||||
first_line = false;
|
||||
}
|
||||
|
||||
let remainder = buffer.split_off(text.len());
|
||||
let result = if buffer.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Ok(buffer.clone()))
|
||||
};
|
||||
|
||||
buffer = remainder;
|
||||
future::ready(result)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::FakeCompletionProvider;
|
||||
|
||||
use super::*;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
Point,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DummyCompletionRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
let x = 0;
|
||||
for _ in 0..10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(buffer.clone(), CodegenKind::Transform { range }, None, cx)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
" let mut x = 0;\n",
|
||||
" while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
" }",
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_past_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
le
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 6))
|
||||
});
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"t mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_before_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
let provider = FakeCompletionProvider::default();
|
||||
cx.set_global(CompletionProvider::Fake(provider.clone()));
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = concat!(
|
||||
"fn main() {\n",
|
||||
" \n",
|
||||
"}\n" //
|
||||
);
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 2))
|
||||
});
|
||||
let codegen = cx.new_model(|cx| {
|
||||
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
|
||||
});
|
||||
|
||||
let request = LanguageModelRequest::default();
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"let mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk.into());
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks(
|
||||
"```html\n```js\nLorem ipsum dolor\n```\n```",
|
||||
2
|
||||
))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"```js\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"``\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
|
||||
stream::iter(
|
||||
text.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(size)
|
||||
.map(|chunk| Ok(chunk.iter().collect::<String>()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
mod anthropic;
|
||||
mod cloud;
|
||||
#[cfg(test)]
|
||||
mod fake;
|
||||
mod open_ai;
|
||||
mod zed;
|
||||
|
||||
pub use anthropic::*;
|
||||
pub use cloud::*;
|
||||
#[cfg(test)]
|
||||
pub use fake::*;
|
||||
pub use open_ai::*;
|
||||
pub use zed::*;
|
||||
|
||||
use crate::{
|
||||
assistant_settings::{AssistantProvider, AssistantSettings},
|
||||
@@ -25,8 +25,8 @@ use std::time::Duration;
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
let mut settings_version = 0;
|
||||
let provider = match &AssistantSettings::get_global(cx).provider {
|
||||
AssistantProvider::ZedDotDev { model } => CompletionProvider::ZedDotDev(
|
||||
ZedDotDevCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
|
||||
AssistantProvider::ZedDotDev { model } => CompletionProvider::Cloud(
|
||||
CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
|
||||
),
|
||||
AssistantProvider::OpenAi {
|
||||
model,
|
||||
@@ -87,14 +87,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
settings_version,
|
||||
);
|
||||
}
|
||||
(
|
||||
CompletionProvider::ZedDotDev(provider),
|
||||
AssistantProvider::ZedDotDev { model },
|
||||
) => {
|
||||
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
|
||||
provider.update(model.clone(), settings_version);
|
||||
}
|
||||
(_, AssistantProvider::ZedDotDev { model }) => {
|
||||
*provider = CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
|
||||
*provider = CompletionProvider::Cloud(CloudCompletionProvider::new(
|
||||
model.clone(),
|
||||
client.clone(),
|
||||
settings_version,
|
||||
@@ -142,7 +139,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
pub enum CompletionProvider {
|
||||
OpenAi(OpenAiCompletionProvider),
|
||||
Anthropic(AnthropicCompletionProvider),
|
||||
ZedDotDev(ZedDotDevCompletionProvider),
|
||||
Cloud(CloudCompletionProvider),
|
||||
#[cfg(test)]
|
||||
Fake(FakeCompletionProvider),
|
||||
}
|
||||
@@ -164,9 +161,9 @@ impl CompletionProvider {
|
||||
.available_models()
|
||||
.map(LanguageModel::Anthropic)
|
||||
.collect(),
|
||||
CompletionProvider::ZedDotDev(provider) => provider
|
||||
CompletionProvider::Cloud(provider) => provider
|
||||
.available_models()
|
||||
.map(LanguageModel::ZedDotDev)
|
||||
.map(LanguageModel::Cloud)
|
||||
.collect(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
@@ -177,7 +174,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.settings_version(),
|
||||
CompletionProvider::Anthropic(provider) => provider.settings_version(),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.settings_version(),
|
||||
CompletionProvider::Cloud(provider) => provider.settings_version(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -187,7 +184,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.is_authenticated(),
|
||||
CompletionProvider::Cloud(provider) => provider.is_authenticated(),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => true,
|
||||
}
|
||||
@@ -197,7 +194,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.authenticate(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authenticate(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
@@ -207,7 +204,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.authentication_prompt(cx),
|
||||
CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => unimplemented!(),
|
||||
}
|
||||
@@ -217,7 +214,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
|
||||
CompletionProvider::ZedDotDev(_) => Task::ready(Ok(())),
|
||||
CompletionProvider::Cloud(_) => Task::ready(Ok(())),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => Task::ready(Ok(())),
|
||||
}
|
||||
@@ -227,7 +224,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
|
||||
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
|
||||
CompletionProvider::ZedDotDev(provider) => LanguageModel::ZedDotDev(provider.model()),
|
||||
CompletionProvider::Cloud(provider) => LanguageModel::Cloud(provider.model()),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => LanguageModel::default(),
|
||||
}
|
||||
@@ -241,7 +238,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
|
||||
CompletionProvider::Cloud(provider) => provider.count_tokens(request, cx),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
|
||||
}
|
||||
@@ -254,7 +251,7 @@ impl CompletionProvider {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider.complete(request),
|
||||
CompletionProvider::Anthropic(provider) => provider.complete(request),
|
||||
CompletionProvider::ZedDotDev(provider) => provider.complete(request),
|
||||
CompletionProvider::Cloud(provider) => provider.complete(request),
|
||||
#[cfg(test)]
|
||||
CompletionProvider::Fake(provider) => provider.complete(),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::count_open_ai_tokens;
|
||||
use crate::{
|
||||
assistant_settings::AnthropicModel, CompletionProvider, LanguageModel, LanguageModelRequest,
|
||||
Role,
|
||||
};
|
||||
use anthropic::{stream_completion, Request, RequestMessage, Role as AnthropicRole};
|
||||
use crate::{count_open_ai_tokens, LanguageModelRequestMessage};
|
||||
use anthropic::{stream_completion, Request, RequestMessage};
|
||||
use anyhow::{anyhow, Result};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
@@ -167,53 +167,37 @@ impl AnthropicCompletionProvider {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn to_anthropic_request(&self, request: LanguageModelRequest) -> Request {
|
||||
fn to_anthropic_request(&self, mut request: LanguageModelRequest) -> Request {
|
||||
preprocess_anthropic_request(&mut request);
|
||||
|
||||
let model = match request.model {
|
||||
LanguageModel::Anthropic(model) => model,
|
||||
_ => self.model(),
|
||||
};
|
||||
|
||||
let mut system_message = String::new();
|
||||
|
||||
let mut messages: Vec<RequestMessage> = Vec::new();
|
||||
for message in request.messages {
|
||||
if message.content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match message.role {
|
||||
Role::User | Role::Assistant => {
|
||||
let role = match message.role {
|
||||
Role::User => AnthropicRole::User,
|
||||
Role::Assistant => AnthropicRole::Assistant,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if let Some(last_message) = messages.last_mut() {
|
||||
if last_message.role == role {
|
||||
last_message.content.push_str("\n\n");
|
||||
last_message.content.push_str(&message.content);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
messages.push(RequestMessage {
|
||||
role,
|
||||
content: message.content,
|
||||
});
|
||||
}
|
||||
Role::System => {
|
||||
if !system_message.is_empty() {
|
||||
system_message.push_str("\n\n");
|
||||
}
|
||||
system_message.push_str(&message.content);
|
||||
}
|
||||
}
|
||||
if request
|
||||
.messages
|
||||
.first()
|
||||
.map_or(false, |message| message.role == Role::System)
|
||||
{
|
||||
system_message = request.messages.remove(0).content;
|
||||
}
|
||||
|
||||
Request {
|
||||
model,
|
||||
messages,
|
||||
messages: request
|
||||
.messages
|
||||
.iter()
|
||||
.map(|msg| RequestMessage {
|
||||
role: match msg.role {
|
||||
Role::User => anthropic::Role::User,
|
||||
Role::Assistant => anthropic::Role::Assistant,
|
||||
Role::System => unreachable!("filtered out by preprocess_request"),
|
||||
},
|
||||
content: msg.content.clone(),
|
||||
})
|
||||
.collect(),
|
||||
stream: true,
|
||||
system: system_message,
|
||||
max_tokens: 4092,
|
||||
@@ -221,6 +205,49 @@ impl AnthropicCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preprocess_anthropic_request(request: &mut LanguageModelRequest) {
|
||||
let mut new_messages: Vec<LanguageModelRequestMessage> = Vec::new();
|
||||
let mut system_message = String::new();
|
||||
|
||||
for message in request.messages.drain(..) {
|
||||
if message.content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match message.role {
|
||||
Role::User | Role::Assistant => {
|
||||
if let Some(last_message) = new_messages.last_mut() {
|
||||
if last_message.role == message.role {
|
||||
last_message.content.push_str("\n\n");
|
||||
last_message.content.push_str(&message.content);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
new_messages.push(message);
|
||||
}
|
||||
Role::System => {
|
||||
if !system_message.is_empty() {
|
||||
system_message.push_str("\n\n");
|
||||
}
|
||||
system_message.push_str(&message.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !system_message.is_empty() {
|
||||
request.messages.insert(
|
||||
0,
|
||||
LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: system_message,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
request.messages = new_messages;
|
||||
}
|
||||
|
||||
struct AuthenticationPrompt {
|
||||
api_key: View<Editor>,
|
||||
api_url: String,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
assistant_settings::ZedDotDevModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
|
||||
assistant_settings::CloudModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
|
||||
LanguageModelRequest,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -10,17 +10,17 @@ use std::{future, sync::Arc};
|
||||
use strum::IntoEnumIterator;
|
||||
use ui::prelude::*;
|
||||
|
||||
pub struct ZedDotDevCompletionProvider {
|
||||
pub struct CloudCompletionProvider {
|
||||
client: Arc<Client>,
|
||||
model: ZedDotDevModel,
|
||||
model: CloudModel,
|
||||
settings_version: usize,
|
||||
status: client::Status,
|
||||
_maintain_client_status: Task<()>,
|
||||
}
|
||||
|
||||
impl ZedDotDevCompletionProvider {
|
||||
impl CloudCompletionProvider {
|
||||
pub fn new(
|
||||
model: ZedDotDevModel,
|
||||
model: CloudModel,
|
||||
client: Arc<Client>,
|
||||
settings_version: usize,
|
||||
cx: &mut AppContext,
|
||||
@@ -30,7 +30,7 @@ impl ZedDotDevCompletionProvider {
|
||||
let maintain_client_status = cx.spawn(|mut cx| async move {
|
||||
while let Some(status) = status_rx.next().await {
|
||||
let _ = cx.update_global::<CompletionProvider, _>(|provider, _cx| {
|
||||
if let CompletionProvider::ZedDotDev(provider) = provider {
|
||||
if let CompletionProvider::Cloud(provider) = provider {
|
||||
provider.status = status;
|
||||
} else {
|
||||
unreachable!()
|
||||
@@ -47,20 +47,20 @@ impl ZedDotDevCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, model: ZedDotDevModel, settings_version: usize) {
|
||||
pub fn update(&mut self, model: CloudModel, settings_version: usize) {
|
||||
self.model = model;
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = ZedDotDevModel> {
|
||||
let mut custom_model = if let ZedDotDevModel::Custom(custom_model) = self.model.clone() {
|
||||
pub fn available_models(&self) -> impl Iterator<Item = CloudModel> {
|
||||
let mut custom_model = if let CloudModel::Custom(custom_model) = self.model.clone() {
|
||||
Some(custom_model)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ZedDotDevModel::iter().filter_map(move |model| {
|
||||
if let ZedDotDevModel::Custom(_) = model {
|
||||
Some(ZedDotDevModel::Custom(custom_model.take()?))
|
||||
CloudModel::iter().filter_map(move |model| {
|
||||
if let CloudModel::Custom(_) = model {
|
||||
Some(CloudModel::Custom(custom_model.take()?))
|
||||
} else {
|
||||
Some(model)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ impl ZedDotDevCompletionProvider {
|
||||
self.settings_version
|
||||
}
|
||||
|
||||
pub fn model(&self) -> ZedDotDevModel {
|
||||
pub fn model(&self) -> CloudModel {
|
||||
self.model.clone()
|
||||
}
|
||||
|
||||
@@ -94,21 +94,19 @@ impl ZedDotDevCompletionProvider {
|
||||
cx: &AppContext,
|
||||
) -> BoxFuture<'static, Result<usize>> {
|
||||
match request.model {
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Turbo)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Omni)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt3Point5Turbo) => {
|
||||
LanguageModel::Cloud(CloudModel::Gpt4)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt4Turbo)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt4Omni)
|
||||
| LanguageModel::Cloud(CloudModel::Gpt3Point5Turbo) => {
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::ZedDotDev(
|
||||
ZedDotDevModel::Claude3Opus
|
||||
| ZedDotDevModel::Claude3Sonnet
|
||||
| ZedDotDevModel::Claude3Haiku,
|
||||
LanguageModel::Cloud(
|
||||
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku,
|
||||
) => {
|
||||
// Can't find a tokenizer for Claude 3, so for now just use the same as OpenAI's as an approximation.
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::ZedDotDev(ZedDotDevModel::Custom(model)) => {
|
||||
LanguageModel::Cloud(CloudModel::Custom(model)) => {
|
||||
let request = self.client.request(proto::CountTokensWithLanguageModel {
|
||||
model,
|
||||
messages: request
|
||||
@@ -129,8 +127,10 @@ impl ZedDotDevCompletionProvider {
|
||||
|
||||
pub fn complete(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
mut request: LanguageModelRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
request.preprocess();
|
||||
|
||||
let request = proto::CompleteWithLanguageModel {
|
||||
model: request.model.id().to_string(),
|
||||
messages: request
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::assistant_settings::ZedDotDevModel;
|
||||
use crate::assistant_settings::CloudModel;
|
||||
use crate::{
|
||||
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
|
||||
};
|
||||
@@ -210,9 +210,9 @@ pub fn count_open_ai_tokens(
|
||||
|
||||
match request.model {
|
||||
LanguageModel::Anthropic(_)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Opus)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Sonnet)
|
||||
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Haiku) => {
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Opus)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Haiku) => {
|
||||
// Tiktoken doesn't yet support these models, so we manually use the
|
||||
// same tokenizer as GPT-4.
|
||||
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
|
||||
|
||||
203
crates/assistant/src/conversation_store.rs
Normal file
203
crates/assistant/src/conversation_store.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Model, ModelContext, Task};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
|
||||
use ui::Context;
|
||||
use util::{paths::CONVERSATIONS_DIR, ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedConversation {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
pub const VERSION: &'static str = "0.2.0";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversationV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SavedConversationMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
pub struct ConversationStore {
|
||||
conversations_metadata: Vec<SavedConversationMetadata>,
|
||||
fs: Arc<dyn Fs>,
|
||||
_watch_updates: Task<Option<()>>,
|
||||
}
|
||||
|
||||
impl ConversationStore {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs
|
||||
.watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
|
||||
.await;
|
||||
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
|
||||
conversations_metadata: Vec::new(),
|
||||
fs,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
}),
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
Ok(this)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedConversation>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let saved_conversation = fs.load(&path).await?;
|
||||
let saved_conversation_json =
|
||||
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
|
||||
match saved_conversation_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
SavedConversation::VERSION => Ok(serde_json::from_value::<SavedConversation>(
|
||||
saved_conversation_json,
|
||||
)?),
|
||||
"0.1.0" => {
|
||||
let saved_conversation = serde_json::from_value::<SavedConversationV0_1_0>(
|
||||
saved_conversation_json,
|
||||
)?;
|
||||
Ok(SavedConversation {
|
||||
id: saved_conversation.id,
|
||||
zed: saved_conversation.zed,
|
||||
version: saved_conversation.version,
|
||||
text: saved_conversation.text,
|
||||
messages: saved_conversation.messages,
|
||||
message_metadata: saved_conversation.message_metadata,
|
||||
summary: saved_conversation.summary,
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"unrecognized saved conversation version: {}",
|
||||
version
|
||||
)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved conversation")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedConversationMetadata>> {
|
||||
let metadata = self.conversations_metadata.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
if query.is_empty() {
|
||||
metadata
|
||||
} else {
|
||||
let candidates = metadata
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| metadata[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
// This is used to filter out conversations saved by the new assistant.
|
||||
if !re.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(title) = re.replace(file_name, "").lines().next() {
|
||||
conversations.push(SavedConversationMetadata {
|
||||
title: title.to_string(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.conversations_metadata = conversations;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
1536
crates/assistant/src/inline_assistant.rs
Normal file
1536
crates/assistant/src/inline_assistant.rs
Normal file
File diff suppressed because it is too large
Load Diff
1226
crates/assistant/src/prompt_library.rs
Normal file
1226
crates/assistant/src/prompt_library.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,95 @@
|
||||
mod prompt;
|
||||
mod prompt_library;
|
||||
mod prompt_manager;
|
||||
use language::BufferSnapshot;
|
||||
use std::{fmt::Write, ops::Range};
|
||||
|
||||
pub use prompt::*;
|
||||
pub use prompt_library::*;
|
||||
pub use prompt_manager::*;
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(prompt, "You are an expert engineer.")?;
|
||||
"Text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(prompt, "You are an expert {language_name} engineer.")?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
)?;
|
||||
"Code"
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project_name) = project_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
)?;
|
||||
}
|
||||
|
||||
// Include file content.
|
||||
for chunk in buffer.text_for_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if range.is_empty() {
|
||||
prompt.push_str("<|START|>");
|
||||
} else {
|
||||
prompt.push_str("<|START|");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if !range.is_empty() {
|
||||
prompt.push_str("|END|>");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.end..buffer.len()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
prompt.push('\n');
|
||||
|
||||
if range.is_empty() {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|START|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
|
||||
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
use fs::Fs;
|
||||
use language::BufferSnapshot;
|
||||
use std::{fmt::Write, ops::Range, path::PathBuf, sync::Arc};
|
||||
use ui::SharedString;
|
||||
use util::paths::PROMPTS_DIR;
|
||||
|
||||
use gray_matter::{engine::YAML, Matter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::prompt_library::PromptId;
|
||||
|
||||
pub const PROMPT_DEFAULT_TITLE: &str = "Untitled Prompt";
|
||||
|
||||
fn standardize_value(value: String) -> String {
|
||||
value.replace(['\n', '\r', '"', '\''], "")
|
||||
}
|
||||
|
||||
fn slugify(input: String) -> String {
|
||||
let mut slug = String::new();
|
||||
for c in input.chars() {
|
||||
if c.is_alphanumeric() {
|
||||
slug.push(c.to_ascii_lowercase());
|
||||
} else if c.is_whitespace() {
|
||||
slug.push('-');
|
||||
} else {
|
||||
slug.push('_');
|
||||
}
|
||||
}
|
||||
slug
|
||||
}
|
||||
|
||||
#[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: PROMPT_DEFAULT_TITLE.to_string(),
|
||||
version: "1.0".to_string(),
|
||||
author: "You <you@email.com>".to_string(),
|
||||
languages: vec![],
|
||||
dependencies: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticPromptFrontmatter {
|
||||
/// Returns the frontmatter as a markdown frontmatter string
|
||||
pub fn frontmatter_string(&self) -> String {
|
||||
let mut frontmatter = format!(
|
||||
"---\ntitle: \"{}\"\nversion: \"{}\"\nauthor: \"{}\"\n",
|
||||
standardize_value(self.title.clone()),
|
||||
standardize_value(self.version.clone()),
|
||||
standardize_value(self.author.clone()),
|
||||
);
|
||||
|
||||
if !self.languages.is_empty() {
|
||||
let languages = self
|
||||
.languages
|
||||
.iter()
|
||||
.map(|l| standardize_value(l.clone()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
writeln!(frontmatter, "languages: [{}]", languages).unwrap();
|
||||
}
|
||||
|
||||
if !self.dependencies.is_empty() {
|
||||
let dependencies = self
|
||||
.dependencies
|
||||
.iter()
|
||||
.map(|d| standardize_value(d.clone()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
writeln!(frontmatter, "dependencies: [{}]", dependencies).unwrap();
|
||||
}
|
||||
|
||||
frontmatter.push_str("---\n");
|
||||
|
||||
frontmatter
|
||||
}
|
||||
}
|
||||
|
||||
/// A static 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 {
|
||||
#[serde(skip_deserializing)]
|
||||
id: PromptId,
|
||||
#[serde(skip)]
|
||||
metadata: StaticPromptFrontmatter,
|
||||
content: String,
|
||||
file_name: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Default for StaticPrompt {
|
||||
fn default() -> Self {
|
||||
let metadata = StaticPromptFrontmatter::default();
|
||||
|
||||
let content = metadata.clone().frontmatter_string();
|
||||
|
||||
Self {
|
||||
id: PromptId::new(),
|
||||
metadata,
|
||||
content,
|
||||
file_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticPrompt {
|
||||
pub fn new(content: String, file_name: Option<String>) -> Self {
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(&content);
|
||||
let file_name = if let Some(file_name) = file_name {
|
||||
let shared_filename: SharedString = file_name.into();
|
||||
Some(shared_filename)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let metadata = result
|
||||
.data
|
||||
.map_or_else(
|
||||
|| Err(anyhow::anyhow!("Failed to parse frontmatter")),
|
||||
|data| {
|
||||
let front_matter: StaticPromptFrontmatter = data.deserialize()?;
|
||||
Ok(front_matter)
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
if let Some(file_name) = &file_name {
|
||||
log::error!("Failed to parse frontmatter for {}: {}", file_name, e);
|
||||
} else {
|
||||
log::error!("Failed to parse frontmatter: {}", e);
|
||||
}
|
||||
StaticPromptFrontmatter::default()
|
||||
});
|
||||
|
||||
let id = if let Some(file_name) = &file_name {
|
||||
PromptId::from_str(file_name).unwrap_or_default()
|
||||
} else {
|
||||
PromptId::new()
|
||||
};
|
||||
|
||||
StaticPrompt {
|
||||
id,
|
||||
content,
|
||||
file_name,
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, id: PromptId, content: String) {
|
||||
let mut updated_prompt =
|
||||
StaticPrompt::new(content, self.file_name.clone().map(|s| s.to_string()));
|
||||
updated_prompt.id = id;
|
||||
*self = updated_prompt;
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticPrompt {
|
||||
/// Returns the prompt's id
|
||||
pub fn id(&self) -> &PromptId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn file_name(&self) -> Option<&SharedString> {
|
||||
self.file_name.as_ref()
|
||||
}
|
||||
|
||||
/// Sets the file name of the prompt
|
||||
pub fn new_file_name(&self) -> String {
|
||||
let in_name = format!(
|
||||
"{}_{}_{}",
|
||||
standardize_value(self.metadata.title.clone()),
|
||||
standardize_value(self.metadata.version.clone()),
|
||||
standardize_value(self.id.0.to_string())
|
||||
);
|
||||
let out_name = slugify(in_name);
|
||||
out_name
|
||||
}
|
||||
|
||||
/// Returns the prompt's content
|
||||
pub fn content(&self) -> &String {
|
||||
&self.content
|
||||
}
|
||||
|
||||
/// Returns the prompt's metadata
|
||||
pub fn _metadata(&self) -> &StaticPromptFrontmatter {
|
||||
&self.metadata
|
||||
}
|
||||
|
||||
/// Returns the prompt's title
|
||||
pub fn title(&self) -> SharedString {
|
||||
self.metadata.title.clone().into()
|
||||
}
|
||||
|
||||
pub fn body(&self) -> String {
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(self.content.as_str());
|
||||
result.content.clone()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<PathBuf> {
|
||||
if let Some(file_name) = self.file_name() {
|
||||
let path_str = format!("{}", file_name);
|
||||
Some(PROMPTS_DIR.join(path_str))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
|
||||
let file_name = self.file_name();
|
||||
let new_file_name = self.new_file_name();
|
||||
|
||||
let out_name = if let Some(file_name) = file_name {
|
||||
file_name.to_owned().to_string()
|
||||
} else {
|
||||
format!("{}.md", new_file_name)
|
||||
};
|
||||
let path = PROMPTS_DIR.join(&out_name);
|
||||
let json = self.content.clone();
|
||||
|
||||
fs.atomic_write(path, json).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(prompt, "You are an expert engineer.")?;
|
||||
"Text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(prompt, "You are an expert {language_name} engineer.")?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
)?;
|
||||
"Code"
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project_name) = project_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
)?;
|
||||
}
|
||||
|
||||
// Include file content.
|
||||
for chunk in buffer.text_for_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if range.is_empty() {
|
||||
prompt.push_str("<|START|>");
|
||||
} else {
|
||||
prompt.push_str("<|START|");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
if !range.is_empty() {
|
||||
prompt.push_str("|END|>");
|
||||
}
|
||||
|
||||
for chunk in buffer.text_for_range(range.end..buffer.len()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
prompt.push('\n');
|
||||
|
||||
if range.is_empty() {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|START|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
|
||||
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(prompt)
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
|
||||
use gray_matter::{engine::YAML, Matter};
|
||||
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);
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum SortOrder {
|
||||
Alphabetical,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl PromptId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
|
||||
pub fn from_str(id: &str) -> anyhow::Result<Self> {
|
||||
Ok(Self(Uuid::parse_str(id)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PromptId {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[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 new_prompt(&self) -> StaticPrompt {
|
||||
StaticPrompt::default()
|
||||
}
|
||||
|
||||
pub fn add_prompt(&self, prompt: StaticPrompt) {
|
||||
let mut state = self.state.write();
|
||||
let id = *prompt.id();
|
||||
state.prompts.insert(id, prompt);
|
||||
state.version += 1;
|
||||
}
|
||||
|
||||
pub fn prompts(&self) -> HashMap<PromptId, StaticPrompt> {
|
||||
let state = self.state.read();
|
||||
state.prompts.clone()
|
||||
}
|
||||
|
||||
pub fn sorted_prompts(&self, sort_order: SortOrder) -> Vec<(PromptId, StaticPrompt)> {
|
||||
let state = self.state.read();
|
||||
|
||||
let mut prompts = state
|
||||
.prompts
|
||||
.iter()
|
||||
.map(|(id, prompt)| (*id, prompt.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match sort_order {
|
||||
SortOrder::Alphabetical => prompts.sort_by(|(_, a), (_, b)| a.title().cmp(&b.title())),
|
||||
};
|
||||
|
||||
prompts
|
||||
}
|
||||
|
||||
pub fn prompt_by_id(&self, id: PromptId) -> Option<StaticPrompt> {
|
||||
let state = self.state.read();
|
||||
state.prompts.get(&id).cloned()
|
||||
}
|
||||
|
||||
pub fn first_prompt_id(&self) -> Option<PromptId> {
|
||||
let state = self.state.read();
|
||||
state.prompts.keys().next().cloned()
|
||||
}
|
||||
|
||||
pub fn is_dirty(&self, id: &PromptId) -> bool {
|
||||
let state = self.state.read();
|
||||
state.dirty_prompts.contains(&id)
|
||||
}
|
||||
|
||||
pub fn set_dirty(&self, id: PromptId, dirty: bool) {
|
||||
let mut state = self.state.write();
|
||||
if dirty {
|
||||
if !state.dirty_prompts.contains(&id) {
|
||||
state.dirty_prompts.push(id);
|
||||
}
|
||||
state.version += 1;
|
||||
} else {
|
||||
state.dirty_prompts.retain(|&i| i != id);
|
||||
state.version += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_index(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<()> {
|
||||
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")?;
|
||||
let file_name_lossy = if prompt_path.file_name().is_some() {
|
||||
Some(
|
||||
prompt_path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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))?;
|
||||
|
||||
// Check that the prompt is valid
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(&json);
|
||||
if result.data.is_none() {
|
||||
log::warn!("Invalid prompt: {:?}", prompt_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
let static_prompt = StaticPrompt::new(json, file_name_lossy.clone());
|
||||
|
||||
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_index(fs.clone()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save the current state of the prompt library to the
|
||||
/// file system as a JSON file
|
||||
pub async fn save_index(&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(())
|
||||
}
|
||||
|
||||
pub async fn save_prompt(
|
||||
&self,
|
||||
prompt_id: PromptId,
|
||||
updated_content: Option<String>,
|
||||
fs: Arc<dyn Fs>,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(updated_content) = updated_content {
|
||||
let mut state = self.state.write();
|
||||
if let Some(prompt) = state.prompts.get_mut(&prompt_id) {
|
||||
prompt.update(prompt_id, updated_content);
|
||||
state.version += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prompt) = self.prompt_by_id(prompt_id) {
|
||||
prompt.save(fs).await?;
|
||||
self.set_dirty(prompt_id, false);
|
||||
} else {
|
||||
log::warn!("Failed to save prompt: {:?}", prompt_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,512 +0,0 @@
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, EditorEvent};
|
||||
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, Indicator, ListItem, ListItemSpacing, Tooltip};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::ModalView;
|
||||
|
||||
use crate::prompts::{PromptId, PromptLibrary, SortOrder, StaticPrompt, PROMPT_DEFAULT_TITLE};
|
||||
|
||||
actions!(prompt_manager, [NewPrompt, SavePrompt]);
|
||||
|
||||
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>,
|
||||
last_new_prompt_id: Option<PromptId>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
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,
|
||||
_subscriptions: vec![],
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.max_height(rems(35.75))
|
||||
.modal(false)
|
||||
});
|
||||
|
||||
let focus_handle = picker.focus_handle(cx);
|
||||
|
||||
let subscriptions = vec![
|
||||
// cx.on_focus_in(&focus_handle, Self::focus_in),
|
||||
// cx.on_focus_out(&focus_handle, Self::focus_out),
|
||||
];
|
||||
|
||||
let mut manager = Self {
|
||||
focus_handle,
|
||||
prompt_library,
|
||||
language_registry,
|
||||
fs,
|
||||
picker,
|
||||
prompt_editors: HashMap::default(),
|
||||
active_prompt_id: None,
|
||||
last_new_prompt_id: None,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
|
||||
manager.active_prompt_id = manager.prompt_library.first_prompt_id();
|
||||
|
||||
manager
|
||||
}
|
||||
|
||||
fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
|
||||
let mut dispatch_context = KeyContext::new_with_defaults();
|
||||
dispatch_context.add("PromptManager");
|
||||
|
||||
let identifier = match self.active_editor() {
|
||||
Some(active_editor) if active_editor.focus_handle(cx).is_focused(cx) => "editing",
|
||||
_ => "not_editing",
|
||||
};
|
||||
|
||||
dispatch_context.add(identifier);
|
||||
dispatch_context
|
||||
}
|
||||
|
||||
pub fn new_prompt(&mut self, _: &NewPrompt, cx: &mut ViewContext<Self>) {
|
||||
// TODO: Why doesn't this prevent making a new prompt if you
|
||||
// move the picker selection/maybe unfocus the editor?
|
||||
|
||||
// Prevent making a new prompt if the last new prompt is still empty
|
||||
//
|
||||
// Instead, we'll focus the last new prompt
|
||||
if let Some(last_new_prompt_id) = self.last_new_prompt_id() {
|
||||
if let Some(last_new_prompt) = self.prompt_library.prompt_by_id(last_new_prompt_id) {
|
||||
let normalized_body = last_new_prompt
|
||||
.body()
|
||||
.trim()
|
||||
.replace(['\r', '\n'], "")
|
||||
.to_string();
|
||||
|
||||
if last_new_prompt.title() == PROMPT_DEFAULT_TITLE && normalized_body.is_empty() {
|
||||
self.set_editor_for_prompt(last_new_prompt_id, cx);
|
||||
self.focus_active_editor(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let prompt = self.prompt_library.new_prompt();
|
||||
self.set_last_new_prompt_id(Some(prompt.id().to_owned()));
|
||||
|
||||
self.prompt_library.add_prompt(prompt.clone());
|
||||
|
||||
let id = *prompt.id();
|
||||
self.picker.update(cx, |picker, _cx| {
|
||||
let prompts = self
|
||||
.prompt_library
|
||||
.sorted_prompts(SortOrder::Alphabetical)
|
||||
.clone()
|
||||
.into_iter();
|
||||
|
||||
picker.delegate.prompt_library = self.prompt_library.clone();
|
||||
picker.delegate.matching_prompts = prompts.clone().map(|(_, p)| Arc::new(p)).collect();
|
||||
picker.delegate.matching_prompt_ids = prompts.map(|(id, _)| id).collect();
|
||||
picker.delegate.selected_index = picker
|
||||
.delegate
|
||||
.matching_prompts
|
||||
.iter()
|
||||
.position(|p| p.id() == &id)
|
||||
.unwrap_or(0);
|
||||
});
|
||||
|
||||
self.active_prompt_id = Some(id);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn save_prompt(
|
||||
&mut self,
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_id: PromptId,
|
||||
new_content: String,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Result<()> {
|
||||
let library = self.prompt_library.clone();
|
||||
if library.prompt_by_id(prompt_id).is_some() {
|
||||
cx.spawn(|_, _| async move {
|
||||
library
|
||||
.save_prompt(prompt_id, Some(new_content), fs)
|
||||
.log_err()
|
||||
.await;
|
||||
})
|
||||
.detach();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 last_new_prompt_id(&self) -> Option<PromptId> {
|
||||
self.last_new_prompt_id
|
||||
}
|
||||
|
||||
pub fn set_last_new_prompt_id(&mut self, id: Option<PromptId>) {
|
||||
self.last_new_prompt_id = id;
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_editor(&self) -> Option<&View<Editor>> {
|
||||
self.active_prompt_id
|
||||
.and_then(|active_prompt_id| self.prompt_editors.get(&active_prompt_id))
|
||||
}
|
||||
|
||||
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_by_id(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()
|
||||
}
|
||||
|
||||
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_1_3()
|
||||
.overflow_hidden()
|
||||
.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)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("New Prompt", cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(NewPrompt.boxed_clone());
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.h(rems(38.25))
|
||||
.flex_grow()
|
||||
.justify_start()
|
||||
.child(picker),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PromptManager {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_prompt_id = self.active_prompt_id;
|
||||
let active_prompt = if let Some(active_prompt_id) = active_prompt_id {
|
||||
self.prompt_library.clone().prompt_by_id(active_prompt_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let active_editor = self.active_editor().map(|editor| editor.clone());
|
||||
let updated_content = if let Some(editor) = active_editor {
|
||||
Some(editor.read(cx).text(cx))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let can_save = active_prompt_id.is_some() && updated_content.is_some();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
h_flex()
|
||||
.id("prompt-manager")
|
||||
.key_context(self.dispatch_context(cx))
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::new_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_2_3().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()
|
||||
.overflow_hidden()
|
||||
.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(
|
||||
h_flex()
|
||||
.gap(Spacing::XXLarge.rems(cx))
|
||||
.child(if can_save {
|
||||
IconButton::new("save", IconName::Save)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Save Prompt", cx))
|
||||
.on_click(cx.listener(move |this, _event, cx| {
|
||||
if let Some(prompt_id) = active_prompt_id {
|
||||
this.save_prompt(
|
||||
fs.clone(),
|
||||
prompt_id,
|
||||
updated_content.clone().unwrap_or(
|
||||
"TODO: make unreachable"
|
||||
.to_string(),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
IconButton::new("save", IconName::Save)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(true)
|
||||
})
|
||||
.when_some(active_prompt, |this, active_prompt| {
|
||||
let path = active_prompt.path();
|
||||
|
||||
this.child(
|
||||
IconButton::new("reveal", IconName::Reveal)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(path.is_none())
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::text("Reveal in Finder", cx)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _event, cx| {
|
||||
if let Some(path) = path.clone() {
|
||||
cx.reveal_path(&path);
|
||||
}
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Close", cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(menu::Cancel.boxed_clone());
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when_some(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 EventEmitter<EditorEvent> 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,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
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.sorted_prompts(SortOrder::Alphabetical);
|
||||
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 prompt = self.matching_prompts.get(ix)?;
|
||||
|
||||
let is_diry = self.prompt_library.is_dirty(prompt.id());
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(Label::new(prompt.title()))
|
||||
.end_slot(div().when(is_diry, |this| this.child(Indicator::dot()))),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedConversation {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
pub const VERSION: &'static str = "0.2.0";
|
||||
|
||||
pub async fn load(path: &Path, fs: &dyn Fs) -> Result<Self> {
|
||||
let saved_conversation = fs.load(path).await?;
|
||||
let saved_conversation_json =
|
||||
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
|
||||
match saved_conversation_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
Self::VERSION => Ok(serde_json::from_value::<Self>(saved_conversation_json)?),
|
||||
"0.1.0" => {
|
||||
let saved_conversation =
|
||||
serde_json::from_value::<SavedConversationV0_1_0>(saved_conversation_json)?;
|
||||
Ok(Self {
|
||||
id: saved_conversation.id,
|
||||
zed: saved_conversation.zed,
|
||||
version: saved_conversation.version,
|
||||
text: saved_conversation.text,
|
||||
messages: saved_conversation.messages,
|
||||
message_metadata: saved_conversation.message_metadata,
|
||||
summary: saved_conversation.summary,
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"unrecognized saved conversation version: {}",
|
||||
version
|
||||
)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved conversation")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversationV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
}
|
||||
|
||||
pub struct SavedConversationMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
// This is used to filter out conversations saved by the new assistant.
|
||||
if !re.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ use std::{
|
||||
use workspace::Workspace;
|
||||
|
||||
pub mod active_command;
|
||||
pub mod default_command;
|
||||
pub mod fetch_command;
|
||||
pub mod file_command;
|
||||
pub mod project_command;
|
||||
pub mod prompt_command;
|
||||
@@ -25,10 +27,10 @@ pub mod search_command;
|
||||
pub mod tabs_command;
|
||||
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
editor: WeakView<ConversationEditor>,
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
workspace: WeakView<Workspace>,
|
||||
editor: Option<WeakView<ConversationEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandLine {
|
||||
@@ -40,9 +42,9 @@ pub(crate) struct SlashCommandLine {
|
||||
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
editor: WeakView<ConversationEditor>,
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
workspace: WeakView<Workspace>,
|
||||
editor: Option<WeakView<ConversationEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
@@ -96,6 +98,30 @@ impl SlashCommandCompletionProvider {
|
||||
new_text.push(' ');
|
||||
}
|
||||
|
||||
let confirm = editor.clone().zip(workspace.clone()).and_then(
|
||||
|(editor, workspace)| {
|
||||
(!requires_argument).then(|| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
None,
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}) as Arc<_>
|
||||
})
|
||||
},
|
||||
);
|
||||
Some(project::Completion {
|
||||
old_range: name_range.clone(),
|
||||
documentation: Some(Documentation::SingleLine(command.description())),
|
||||
@@ -104,25 +130,7 @@ impl SlashCommandCompletionProvider {
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: requires_argument,
|
||||
confirm: (!requires_argument).then(|| {
|
||||
let command_name = mat.string.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
None,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}) as Arc<_>
|
||||
}),
|
||||
confirm,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -157,33 +165,42 @@ impl SlashCommandCompletionProvider {
|
||||
Ok(completions
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|arg| project::Completion {
|
||||
old_range: argument_range.clone(),
|
||||
label: CodeLabel::plain(arg.clone(), None),
|
||||
new_text: arg.clone(),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: false,
|
||||
confirm: Some(Arc::new({
|
||||
let command_name = command_name.clone();
|
||||
let command_range = command_range.clone();
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
move |cx| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
Some(&arg),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})),
|
||||
.map(|command_argument| {
|
||||
let confirm =
|
||||
editor
|
||||
.clone()
|
||||
.zip(workspace.clone())
|
||||
.map(|(editor, workspace)| {
|
||||
Arc::new({
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
let command_argument = command_argument.clone();
|
||||
move |cx: &mut WindowContext| {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
Some(&command_argument),
|
||||
true,
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}) as Arc<_>
|
||||
});
|
||||
project::Completion {
|
||||
old_range: argument_range.clone(),
|
||||
label: CodeLabel::plain(command_argument.clone(), None),
|
||||
new_text: command_argument.clone(),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(0),
|
||||
lsp_completion: Default::default(),
|
||||
show_new_completions_on_confirm: false,
|
||||
confirm,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
@@ -96,6 +96,7 @@ impl SlashCommand for ActiveSlashCommand {
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
81
crates/assistant/src/slash_command/default_command.rs
Normal file
81
crates/assistant/src/slash_command/default_command.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use super::{prompt_command::PromptPlaceholder, SlashCommand, SlashCommandOutput};
|
||||
use crate::prompt_library::PromptStore;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct DefaultSlashCommand;
|
||||
|
||||
impl SlashCommand for DefaultSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"default".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert default prompt".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert Default Prompt".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
_argument: Option<&str>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
let store = store.await?;
|
||||
let prompts = store.default_prompt_metadata();
|
||||
|
||||
let mut text = String::new();
|
||||
writeln!(text, "Default Prompt:").unwrap();
|
||||
for prompt in prompts {
|
||||
if let Some(title) = prompt.title {
|
||||
writeln!(text, "/prompt {}", title).unwrap();
|
||||
}
|
||||
}
|
||||
text.pop();
|
||||
|
||||
Ok(SlashCommandOutput {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range: 0..text.len(),
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
PromptPlaceholder {
|
||||
title: "Default".into(),
|
||||
id,
|
||||
unfold,
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
text,
|
||||
run_commands_in_text: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
134
crates/assistant/src/slash_command/fetch_command.rs
Normal file
134
crates/assistant/src/slash_command/fetch_command.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use html_to_markdown::convert_html_to_markdown;
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct FetchSlashCommand;
|
||||
|
||||
impl FetchSlashCommand {
|
||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
|
||||
let mut url = url.to_owned();
|
||||
if !url.starts_with("https://") {
|
||||
url = format!("https://{url}");
|
||||
}
|
||||
|
||||
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading response body")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
convert_html_to_markdown(&body[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl SlashCommand for FetchSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
"fetch".into()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"insert URL contents".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
"Insert fetched URL contents".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
argument: Option<&str>,
|
||||
workspace: WeakView<Workspace>,
|
||||
_delegate: Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<SlashCommandOutput>> {
|
||||
let Some(argument) = argument else {
|
||||
return Task::ready(Err(anyhow!("missing URL")));
|
||||
};
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let url = argument.to_string();
|
||||
|
||||
let text = cx.background_executor().spawn({
|
||||
let url = url.clone();
|
||||
async move { Self::build_message(http_client, &url).await }
|
||||
});
|
||||
|
||||
let url = SharedString::from(url);
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
FetchPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
url: url.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct FetchPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub url: SharedString,
|
||||
}
|
||||
|
||||
impl RenderOnce for FetchPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::AtSign))
|
||||
.child(Label::new(format!("fetch {url}", url = self.url)))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
@@ -101,10 +101,10 @@ impl SlashCommand for FileSlashCommand {
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
@@ -187,6 +187,7 @@ impl SlashCommand for FileSlashCommand {
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
@@ -148,6 +148,7 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
use super::{SlashCommand, SlashCommandOutput};
|
||||
use crate::prompts::PromptLibrary;
|
||||
use crate::prompt_library::PromptStore;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::LspAdapterDelegate;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct PromptSlashCommand {
|
||||
library: Arc<PromptLibrary>,
|
||||
}
|
||||
|
||||
impl PromptSlashCommand {
|
||||
pub fn new(library: Arc<PromptLibrary>) -> Self {
|
||||
Self { library }
|
||||
}
|
||||
}
|
||||
pub(crate) struct PromptSlashCommand;
|
||||
|
||||
impl SlashCommand for PromptSlashCommand {
|
||||
fn name(&self) -> String {
|
||||
@@ -39,31 +30,16 @@ impl SlashCommand for PromptSlashCommand {
|
||||
fn complete_argument(
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
let library = self.library.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
let candidates = library
|
||||
.prompts()
|
||||
let prompts = store.await?.search(query).await;
|
||||
Ok(prompts
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, prompt)| StringMatchCandidate::new(ix, prompt.1.title().to_string()))
|
||||
.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())
|
||||
.filter_map(|prompt| Some(prompt.title?.to_string()))
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
@@ -79,19 +55,17 @@ impl SlashCommand for PromptSlashCommand {
|
||||
return Task::ready(Err(anyhow!("missing prompt name")));
|
||||
};
|
||||
|
||||
let library = self.library.clone();
|
||||
let store = PromptStore::global(cx);
|
||||
let title = SharedString::from(title.to_string());
|
||||
let prompt = cx.background_executor().spawn({
|
||||
let title = title.clone();
|
||||
async move {
|
||||
let prompt = library
|
||||
.prompts()
|
||||
.into_iter()
|
||||
.map(|prompt| (prompt.1.title(), prompt))
|
||||
.find(|(t, _)| t == &title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?
|
||||
.1;
|
||||
anyhow::Ok(prompt.1.body())
|
||||
let store = store.await?;
|
||||
let prompt_id = store
|
||||
.id_for_title(&title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||
let body = store.load(prompt_id).await?;
|
||||
anyhow::Ok(body)
|
||||
}
|
||||
});
|
||||
cx.foreground_executor().spawn(async move {
|
||||
@@ -102,16 +76,35 @@ impl SlashCommand for PromptSlashCommand {
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
render_placeholder: Arc::new(move |id, unfold, _cx| {
|
||||
ButtonLike::new(id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::Library))
|
||||
.child(Label::new(title.clone()))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
.into_any_element()
|
||||
PromptPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
title: title.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PromptPlaceholder {
|
||||
pub title: SharedString,
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
}
|
||||
|
||||
impl RenderOnce for PromptPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::Library))
|
||||
.child(Label::new(self.title))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,63 @@
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
|
||||
use fs::Fs;
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use html_to_markdown::convert_rustdoc_to_markdown;
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use language::LspAdapterDelegate;
|
||||
use rustdoc_to_markdown::convert_rustdoc_to_markdown;
|
||||
use project::{Project, ProjectPath};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum RustdocSource {
|
||||
/// The docs were sourced from local `cargo doc` output.
|
||||
Local,
|
||||
/// The docs were sourced from `docs.rs`.
|
||||
DocsDotRs,
|
||||
}
|
||||
|
||||
pub(crate) struct RustdocSlashCommand;
|
||||
|
||||
impl RustdocSlashCommand {
|
||||
async fn build_message(
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
crate_name: String,
|
||||
) -> Result<String> {
|
||||
module_path: Vec<String>,
|
||||
path_to_cargo_toml: Option<&Path>,
|
||||
) -> Result<(RustdocSource, String)> {
|
||||
let cargo_workspace_root = path_to_cargo_toml.and_then(|path| path.parent());
|
||||
if let Some(cargo_workspace_root) = cargo_workspace_root {
|
||||
let mut local_cargo_doc_path = cargo_workspace_root.join("target/doc");
|
||||
local_cargo_doc_path.push(&crate_name);
|
||||
if !module_path.is_empty() {
|
||||
local_cargo_doc_path.push(module_path.join("/"));
|
||||
}
|
||||
local_cargo_doc_path.push("index.html");
|
||||
|
||||
if let Ok(contents) = fs.load(&local_cargo_doc_path).await {
|
||||
return Ok((
|
||||
RustdocSource::Local,
|
||||
convert_rustdoc_to_markdown(contents.as_bytes())?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let version = "latest";
|
||||
let path = format!(
|
||||
"{crate_name}/{version}/{crate_name}/{module_path}",
|
||||
module_path = module_path.join("/")
|
||||
);
|
||||
|
||||
let mut response = http_client
|
||||
.get(
|
||||
&format!("https://docs.rs/{crate_name}"),
|
||||
&format!("https://docs.rs/{path}"),
|
||||
AsyncBody::default(),
|
||||
true,
|
||||
)
|
||||
@@ -41,7 +78,23 @@ impl RustdocSlashCommand {
|
||||
);
|
||||
}
|
||||
|
||||
convert_rustdoc_to_markdown(&body[..])
|
||||
Ok((
|
||||
RustdocSource::DocsDotRs,
|
||||
convert_rustdoc_to_markdown(&body[..])?,
|
||||
))
|
||||
}
|
||||
|
||||
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
|
||||
let worktree = project.read(cx).worktrees().next()?;
|
||||
let worktree = worktree.read(cx);
|
||||
let entry = worktree.entry_for_path("Cargo.toml")?;
|
||||
let path = ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: entry.path.clone(),
|
||||
};
|
||||
Some(Arc::from(
|
||||
project.read(cx).absolute_path(&path, cx)?.as_path(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +119,7 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
@@ -86,17 +139,43 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let http_client = workspace.read(cx).client().http_client();
|
||||
let crate_name = argument.to_string();
|
||||
let mut path_components = argument.split("::");
|
||||
let crate_name = match path_components
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("missing crate name"))
|
||||
{
|
||||
Ok(crate_name) => crate_name.to_string(),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
|
||||
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
|
||||
|
||||
let text = cx.background_executor().spawn({
|
||||
let crate_name = crate_name.clone();
|
||||
async move { Self::build_message(http_client, crate_name).await }
|
||||
let module_path = module_path.clone();
|
||||
async move {
|
||||
Self::build_message(
|
||||
fs,
|
||||
http_client,
|
||||
crate_name,
|
||||
module_path,
|
||||
path_to_cargo_toml.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
});
|
||||
|
||||
let crate_name = SharedString::from(crate_name);
|
||||
let module_path = if module_path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SharedString::from(module_path.join("::")))
|
||||
};
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let text = text.await?;
|
||||
let (source, text) = text.await?;
|
||||
let range = 0..text.len();
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
@@ -106,11 +185,14 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
RustdocPlaceholder {
|
||||
id,
|
||||
unfold,
|
||||
source,
|
||||
crate_name: crate_name.clone(),
|
||||
module_path: module_path.clone(),
|
||||
}
|
||||
.into_any_element()
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -120,18 +202,31 @@ impl SlashCommand for RustdocSlashCommand {
|
||||
struct RustdocPlaceholder {
|
||||
pub id: ElementId,
|
||||
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
|
||||
pub source: RustdocSource,
|
||||
pub crate_name: SharedString,
|
||||
pub module_path: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl RenderOnce for RustdocPlaceholder {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let unfold = self.unfold;
|
||||
|
||||
let crate_path = self
|
||||
.module_path
|
||||
.map(|module_path| format!("{crate_name}::{module_path}", crate_name = self.crate_name))
|
||||
.unwrap_or(self.crate_name.to_string());
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Icon::new(IconName::FileRust))
|
||||
.child(Label::new(format!("rustdoc: {}", self.crate_name)))
|
||||
.child(Label::new(format!(
|
||||
"rustdoc ({source}): {crate_path}",
|
||||
source = match self.source {
|
||||
RustdocSource::Local => "local",
|
||||
RustdocSource::DocsDotRs => "docs.rs",
|
||||
}
|
||||
)))
|
||||
.on_click(move |_, cx| unfold(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ impl SlashCommand for SearchSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
@@ -181,7 +181,11 @@ impl SlashCommand for SearchSlashCommand {
|
||||
}),
|
||||
});
|
||||
|
||||
SlashCommandOutput { text, sections }
|
||||
SlashCommandOutput {
|
||||
text,
|
||||
sections,
|
||||
run_commands_in_text: false,
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ impl SlashCommand for TabsSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<std::sync::atomic::AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
@@ -109,7 +109,11 @@ impl SlashCommand for TabsSlashCommand {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(SlashCommandOutput { text, sections })
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections,
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
}),
|
||||
Err(error) => Task::ready(Err(error)),
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
&self,
|
||||
query: String,
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>>;
|
||||
fn requires_argument(&self) -> bool;
|
||||
@@ -52,6 +52,7 @@ pub type RenderFoldPlaceholder = Arc<
|
||||
pub struct SlashCommandOutput {
|
||||
pub text: String,
|
||||
pub sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
pub run_commands_in_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -23,7 +23,10 @@ use smol::{fs::File, process::Command};
|
||||
use http::{HttpClient, HttpClientWithUrl};
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use std::{
|
||||
env::consts::{ARCH, OS},
|
||||
env::{
|
||||
self,
|
||||
consts::{ARCH, OS},
|
||||
},
|
||||
ffi::OsString,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
@@ -138,20 +141,24 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
let auto_updater = cx.new_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client);
|
||||
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
if option_env!("ZED_UPDATE_EXPLANATION").is_none()
|
||||
&& env::var("ZED_UPDATE_EXPLANATION").is_err()
|
||||
{
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
}
|
||||
} else {
|
||||
update_subscription.take();
|
||||
}
|
||||
} else {
|
||||
update_subscription.take();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
updater
|
||||
});
|
||||
@@ -159,6 +166,26 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Zed was installed via a package manager.",
|
||||
Some(message),
|
||||
&["Ok"],
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(message) = env::var("ZED_UPDATE_EXPLANATION").ok() {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Zed was installed via a package manager.",
|
||||
Some(&message),
|
||||
&["Ok"],
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
} else {
|
||||
@@ -342,16 +369,6 @@ impl AutoUpdater {
|
||||
}
|
||||
|
||||
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
||||
// Skip auto-update for flatpaks
|
||||
#[cfg(target_os = "linux")]
|
||||
if matches!(std::env::var("ZED_IS_FLATPAK_INSTALL"), Ok(_)) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (client, current_version) = this.read_with(&cx, |this, _| {
|
||||
(this.http_client.clone(), this.current_version)
|
||||
})?;
|
||||
@@ -509,7 +526,7 @@ async fn install_release_linux(
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
|
||||
let home_dir = PathBuf::from(std::env::var("HOME").context("no HOME env var set")?);
|
||||
let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
|
||||
|
||||
let extracted = temp_dir.path().join("zed");
|
||||
fs::create_dir_all(&extracted)
|
||||
|
||||
@@ -267,7 +267,7 @@ impl Room {
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(room),
|
||||
Err(error) => Err(anyhow!("room creation failed: {:?}", error)),
|
||||
Err(error) => Err(error.context("room creation failed")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -191,14 +191,15 @@ mod linux {
|
||||
let cli = env::current_exe()?;
|
||||
let dir = cli
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.ok_or_else(|| anyhow!("no parent path for cli"))?;
|
||||
|
||||
match dir.join("zed").canonicalize() {
|
||||
match dir.join("libexec").join("zed-editor").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
// development builds have Zed capitalized
|
||||
Err(e) => match dir.join("Zed").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
Err(_) => Err(e),
|
||||
// In development cli and zed are in the ./target/ directory together
|
||||
Err(e) => match cli.parent().unwrap().join("zed").canonicalize() {
|
||||
Ok(path) if path != cli => Ok(path),
|
||||
_ => Err(e),
|
||||
},
|
||||
}
|
||||
}?;
|
||||
@@ -254,10 +255,8 @@ mod linux {
|
||||
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
|
||||
process::exit(1);
|
||||
}
|
||||
if std::env::var("ZED_KEEP_FD").is_err() {
|
||||
if let Err(_) = fork::close_fd() {
|
||||
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
|
||||
}
|
||||
if let Err(_) = fork::close_fd() {
|
||||
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
|
||||
}
|
||||
let error =
|
||||
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
|
||||
@@ -315,7 +314,7 @@ mod flatpak {
|
||||
if let Some(flatpak_dir) = get_flatpak_dir() {
|
||||
let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
|
||||
args.append(&mut get_xdg_env_args());
|
||||
args.push("--env=ZED_IS_FLATPAK_INSTALL=1".into());
|
||||
args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
|
||||
args.push(
|
||||
format!(
|
||||
"--env={EXTRA_LIB_ENV_NAME}={}",
|
||||
@@ -333,7 +332,7 @@ mod flatpak {
|
||||
|
||||
if !is_app_location_set {
|
||||
args.push("--zed".into());
|
||||
args.push(flatpak_dir.join("bin").join("zed-app").into());
|
||||
args.push(flatpak_dir.join("libexec").join("zed-editor").into());
|
||||
}
|
||||
|
||||
let error = exec::execvp("/usr/bin/flatpak-spawn", args);
|
||||
@@ -347,8 +346,8 @@ mod flatpak {
|
||||
&& env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
|
||||
{
|
||||
if args.zed.is_none() {
|
||||
args.zed = Some("/app/bin/zed-app".into());
|
||||
env::set_var("ZED_IS_FLATPAK_INSTALL", "1");
|
||||
args.zed = Some("/app/libexec/zed-editor".into());
|
||||
env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed");
|
||||
}
|
||||
}
|
||||
args
|
||||
|
||||
@@ -107,4 +107,5 @@ theme.workspace = true
|
||||
unindent.workspace = true
|
||||
util.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
worktree = { workspace = true, features = ["test-support"] }
|
||||
headless.workspace = true
|
||||
|
||||
@@ -239,61 +239,74 @@ async fn fetch_extensions_from_blob_store(
|
||||
) -> anyhow::Result<()> {
|
||||
log::info!("fetching extensions from blob store");
|
||||
|
||||
let list = blob_store_client
|
||||
.list_objects()
|
||||
.bucket(blob_store_bucket)
|
||||
.prefix("extensions/")
|
||||
.send()
|
||||
.await?;
|
||||
let mut next_marker = None;
|
||||
let mut published_versions = HashMap::<String, Vec<String>>::default();
|
||||
|
||||
let objects = list.contents.unwrap_or_default();
|
||||
loop {
|
||||
let list = blob_store_client
|
||||
.list_objects()
|
||||
.bucket(blob_store_bucket)
|
||||
.prefix("extensions/")
|
||||
.set_marker(next_marker.clone())
|
||||
.send()
|
||||
.await?;
|
||||
let objects = list.contents.unwrap_or_default();
|
||||
log::info!("fetched {} object(s) from blob store", objects.len());
|
||||
|
||||
let mut published_versions = HashMap::<&str, Vec<&str>>::default();
|
||||
for object in &objects {
|
||||
let Some(key) = object.key.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let mut parts = key.split('/');
|
||||
let Some(_) = parts.next().filter(|part| *part == "extensions") else {
|
||||
continue;
|
||||
};
|
||||
let Some(extension_id) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(version) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
if parts.next() == Some("manifest.json") {
|
||||
published_versions
|
||||
.entry(extension_id)
|
||||
.or_default()
|
||||
.push(version);
|
||||
for object in &objects {
|
||||
let Some(key) = object.key.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let mut parts = key.split('/');
|
||||
let Some(_) = parts.next().filter(|part| *part == "extensions") else {
|
||||
continue;
|
||||
};
|
||||
let Some(extension_id) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(version) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
if parts.next() == Some("manifest.json") {
|
||||
published_versions
|
||||
.entry(extension_id.to_owned())
|
||||
.or_default()
|
||||
.push(version.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(true), Some(last_object)) = (list.is_truncated, objects.last()) {
|
||||
next_marker.clone_from(&last_object.key);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("found {} published extensions", published_versions.len());
|
||||
|
||||
let known_versions = app_state.db.get_known_extension_versions().await?;
|
||||
|
||||
let mut new_versions = HashMap::<&str, Vec<NewExtensionVersion>>::default();
|
||||
let empty = Vec::new();
|
||||
for (extension_id, published_versions) in published_versions {
|
||||
for (extension_id, published_versions) in &published_versions {
|
||||
let known_versions = known_versions.get(extension_id).unwrap_or(&empty);
|
||||
|
||||
for published_version in published_versions {
|
||||
if known_versions
|
||||
.binary_search_by_key(&published_version, String::as_str)
|
||||
.binary_search_by_key(&published_version, |known_version| known_version)
|
||||
.is_err()
|
||||
{
|
||||
if let Some(extension) = fetch_extension_manifest(
|
||||
blob_store_client,
|
||||
blob_store_bucket,
|
||||
extension_id,
|
||||
published_version,
|
||||
&extension_id,
|
||||
&published_version,
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
new_versions
|
||||
.entry(extension_id)
|
||||
.entry(&extension_id)
|
||||
.or_default()
|
||||
.push(extension);
|
||||
}
|
||||
|
||||
@@ -545,6 +545,9 @@ impl Server {
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::MultiLspQuery>,
|
||||
))
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::RestartLanguageServers>,
|
||||
))
|
||||
.add_message_handler(create_buffer_for_peer)
|
||||
.add_request_handler(update_buffer)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
|
||||
|
||||
@@ -3022,7 +3022,6 @@ async fn test_fs_operations(
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
|
||||
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
|
||||
|
||||
let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
|
||||
|
||||
let entry = project_b
|
||||
@@ -3031,6 +3030,7 @@ async fn test_fs_operations(
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -3059,6 +3059,7 @@ async fn test_fs_operations(
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -3087,6 +3088,7 @@ async fn test_fs_operations(
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -3115,20 +3117,25 @@ async fn test_fs_operations(
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.to_included()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
|
||||
@@ -5,7 +5,7 @@ use client::{proto::PeerId, Client, User, UserStore};
|
||||
use gpui::{
|
||||
actions, canvas, div, point, px, Action, AnyElement, AppContext, Element, Hsla,
|
||||
InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
|
||||
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
|
||||
StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use project::{Project, RepositoryEntry};
|
||||
use recent_projects::RecentProjects;
|
||||
@@ -17,7 +17,7 @@ use ui::{
|
||||
ButtonStyle, ContextMenu, Icon, IconButton, IconName, Indicator, TintColor, TitleBar, Tooltip,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
|
||||
use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
|
||||
const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||
@@ -487,7 +487,7 @@ impl CollabTitlebarItem {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
|
||||
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||
let entry = {
|
||||
let mut names_and_branches =
|
||||
self.project.read(cx).visible_worktrees(cx).map(|worktree| {
|
||||
@@ -503,22 +503,23 @@ impl CollabTitlebarItem {
|
||||
.and_then(RepositoryEntry::branch)
|
||||
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
|
||||
Some(
|
||||
popover_menu("project_branch_trigger")
|
||||
.trigger(
|
||||
Button::new("project_branch_trigger", branch_name)
|
||||
.color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
"Recent Branches",
|
||||
Some(&ToggleVcsMenu),
|
||||
"Local branches only",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.menu(move |cx| Self::render_vcs_popover(workspace.clone(), cx)),
|
||||
Button::new("project_branch_trigger", branch_name)
|
||||
.color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
"Recent Branches",
|
||||
Some(&ToggleVcsMenu),
|
||||
"Local branches only",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(move |_, cx| {
|
||||
let _ = workspace.update(cx, |this, cx| {
|
||||
BranchList::open(this, &Default::default(), cx)
|
||||
});
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -650,16 +651,6 @@ impl CollabTitlebarItem {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub fn render_vcs_popover(
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext<'_>,
|
||||
) -> Option<View<BranchList>> {
|
||||
let view = build_branch_list(workspace, cx).log_err()?;
|
||||
let focus_handle = view.focus_handle(cx);
|
||||
cx.focus(&focus_handle);
|
||||
Some(view)
|
||||
}
|
||||
|
||||
fn render_connection_status(
|
||||
&self,
|
||||
status: &client::Status,
|
||||
|
||||
@@ -289,6 +289,7 @@ gpui::actions!(
|
||||
ToggleLineNumbers,
|
||||
ToggleIndentGuides,
|
||||
ToggleSoftWrap,
|
||||
ToggleTabBar,
|
||||
Transpose,
|
||||
Undo,
|
||||
UndoSelection,
|
||||
|
||||
@@ -29,13 +29,9 @@ impl DebouncedDelay {
|
||||
let (sender, mut receiver) = oneshot::channel::<()>();
|
||||
self.cancel_channel = Some(sender);
|
||||
|
||||
let previous_task = self.task.take();
|
||||
drop(self.task.take());
|
||||
self.task = Some(cx.spawn(move |model, mut cx| async move {
|
||||
let mut timer = cx.background_executor().timer(delay).fuse();
|
||||
if let Some(previous_task) = previous_task {
|
||||
previous_task.await;
|
||||
}
|
||||
|
||||
futures::select_biased! {
|
||||
_ = receiver => return,
|
||||
_ = timer => {}
|
||||
|
||||
@@ -277,8 +277,55 @@ impl DisplayMap {
|
||||
block_map.insert(blocks)
|
||||
}
|
||||
|
||||
pub fn replace_blocks(&mut self, styles: HashMap<BlockId, RenderBlock>) {
|
||||
self.block_map.replace(styles);
|
||||
pub fn replace_blocks(
|
||||
&mut self,
|
||||
heights_and_renderers: HashMap<BlockId, (Option<u8>, RenderBlock)>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
//
|
||||
// Note: previous implementation of `replace_blocks` simply called
|
||||
// `self.block_map.replace(styles)` which just modified the render by replacing
|
||||
// the `RenderBlock` with the new one.
|
||||
//
|
||||
// ```rust
|
||||
// for block in &self.blocks {
|
||||
// if let Some(render) = renderers.remove(&block.id) {
|
||||
// *block.render.lock() = render;
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// If height changes however, we need to update the tree. There's a performance
|
||||
// cost to this, so we'll split the replace blocks into handling the old behavior
|
||||
// directly and the new behavior separately.
|
||||
//
|
||||
//
|
||||
let mut only_renderers = HashMap::<BlockId, RenderBlock>::default();
|
||||
let mut full_replace = HashMap::<BlockId, (u8, RenderBlock)>::default();
|
||||
for (id, (height, render)) in heights_and_renderers {
|
||||
if let Some(height) = height {
|
||||
full_replace.insert(id, (height, render));
|
||||
} else {
|
||||
only_renderers.insert(id, render);
|
||||
}
|
||||
}
|
||||
self.block_map.replace_renderers(only_renderers);
|
||||
|
||||
if full_replace.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
let mut block_map = self.block_map.write(snapshot, edits);
|
||||
block_map.replace(full_replace);
|
||||
}
|
||||
|
||||
pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
|
||||
|
||||
@@ -467,8 +467,8 @@ impl BlockMap {
|
||||
*transforms = new_transforms;
|
||||
}
|
||||
|
||||
pub fn replace(&mut self, mut renderers: HashMap<BlockId, RenderBlock>) {
|
||||
for block in &self.blocks {
|
||||
pub fn replace_renderers(&mut self, mut renderers: HashMap<BlockId, RenderBlock>) {
|
||||
for block in &mut self.blocks {
|
||||
if let Some(render) = renderers.remove(&block.id) {
|
||||
*block.render.lock() = render;
|
||||
}
|
||||
@@ -659,6 +659,48 @@ impl<'a> BlockMapWriter<'a> {
|
||||
ids
|
||||
}
|
||||
|
||||
pub fn replace(&mut self, mut heights_and_renderers: HashMap<BlockId, (u8, RenderBlock)>) {
|
||||
let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
|
||||
let buffer = wrap_snapshot.buffer_snapshot();
|
||||
let mut edits = Patch::default();
|
||||
let mut last_block_buffer_row = None;
|
||||
|
||||
for block in &mut self.0.blocks {
|
||||
if let Some((new_height, render)) = heights_and_renderers.remove(&block.id) {
|
||||
if block.height != new_height {
|
||||
let new_block = Block {
|
||||
id: block.id,
|
||||
position: block.position,
|
||||
height: new_height,
|
||||
style: block.style,
|
||||
render: Mutex::new(render),
|
||||
disposition: block.disposition,
|
||||
};
|
||||
*block = Arc::new(new_block);
|
||||
|
||||
let buffer_row = block.position.to_point(buffer).row;
|
||||
if last_block_buffer_row != Some(buffer_row) {
|
||||
last_block_buffer_row = Some(buffer_row);
|
||||
let wrap_row = wrap_snapshot
|
||||
.make_wrap_point(Point::new(buffer_row, 0), Bias::Left)
|
||||
.row();
|
||||
let start_row =
|
||||
wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0));
|
||||
let end_row = wrap_snapshot
|
||||
.next_row_boundary(WrapPoint::new(wrap_row, 0))
|
||||
.unwrap_or(wrap_snapshot.max_point().row() + 1);
|
||||
edits.push(Edit {
|
||||
old: start_row..end_row,
|
||||
new: start_row..end_row,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.0.sync(wrap_snapshot, edits);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, block_ids: HashSet<BlockId>) {
|
||||
let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
|
||||
let buffer = wrap_snapshot.buffer_snapshot();
|
||||
@@ -1305,6 +1347,111 @@ mod tests {
|
||||
assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_with_heights(cx: &mut gpui::TestAppContext) {
|
||||
let _update = cx.update(|cx| init_test(cx));
|
||||
|
||||
let text = "aaa\nbbb\nccc\nddd";
|
||||
|
||||
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
|
||||
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
|
||||
let _subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
|
||||
let (_wrap_map, wraps_snapshot) =
|
||||
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
|
||||
|
||||
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
let block_ids = writer.insert(vec![
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 0)),
|
||||
height: 1,
|
||||
disposition: BlockDisposition::Above,
|
||||
render: Box::new(|_| div().into_any()),
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 2)),
|
||||
height: 2,
|
||||
disposition: BlockDisposition::Above,
|
||||
render: Box::new(|_| div().into_any()),
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(3, 3)),
|
||||
height: 3,
|
||||
disposition: BlockDisposition::Below,
|
||||
render: Box::new(|_| div().into_any()),
|
||||
},
|
||||
]);
|
||||
|
||||
{
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
|
||||
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
|
||||
let mut hash_map = HashMap::default();
|
||||
let render: RenderBlock = Box::new(|_| div().into_any());
|
||||
hash_map.insert(block_ids[0], (2_u8, render));
|
||||
block_map_writer.replace(hash_map);
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
|
||||
{
|
||||
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
|
||||
let mut hash_map = HashMap::default();
|
||||
let render: RenderBlock = Box::new(|_| div().into_any());
|
||||
hash_map.insert(block_ids[0], (1_u8, render));
|
||||
block_map_writer.replace(hash_map);
|
||||
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
|
||||
{
|
||||
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
|
||||
let mut hash_map = HashMap::default();
|
||||
let render: RenderBlock = Box::new(|_| div().into_any());
|
||||
hash_map.insert(block_ids[0], (0_u8, render));
|
||||
block_map_writer.replace(hash_map);
|
||||
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
|
||||
{
|
||||
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
|
||||
let mut hash_map = HashMap::default();
|
||||
let render: RenderBlock = Box::new(|_| div().into_any());
|
||||
hash_map.insert(block_ids[0], (3_u8, render));
|
||||
block_map_writer.replace(hash_map);
|
||||
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
|
||||
{
|
||||
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
|
||||
|
||||
let mut hash_map = HashMap::default();
|
||||
let render: RenderBlock = Box::new(|_| div().into_any());
|
||||
hash_map.insert(block_ids[0], (3_u8, render));
|
||||
block_map_writer.replace(hash_map);
|
||||
|
||||
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
|
||||
// Same height as before, should remain the same
|
||||
assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| init_test(cx));
|
||||
|
||||
@@ -15,18 +15,17 @@
|
||||
pub mod actions;
|
||||
mod blame_entry_tooltip;
|
||||
mod blink_manager;
|
||||
mod debounced_delay;
|
||||
pub mod display_map;
|
||||
mod editor_settings;
|
||||
mod element;
|
||||
mod hunk_diff;
|
||||
mod inlay_hint_cache;
|
||||
|
||||
mod debounced_delay;
|
||||
mod git;
|
||||
mod highlight_matching_bracket;
|
||||
mod hover_links;
|
||||
mod hover_popover;
|
||||
mod hunk_diff;
|
||||
mod indent_guides;
|
||||
mod inlay_hint_cache;
|
||||
mod inline_completion_provider;
|
||||
pub mod items;
|
||||
mod mouse_context_menu;
|
||||
@@ -54,8 +53,7 @@ use convert_case::{Case, Casing};
|
||||
use debounced_delay::DebouncedDelay;
|
||||
use display_map::*;
|
||||
pub use display_map::{DisplayPoint, FoldPlaceholder};
|
||||
use editor_settings::CurrentLineHighlight;
|
||||
pub use editor_settings::EditorSettings;
|
||||
pub use editor_settings::{CurrentLineHighlight, EditorSettings};
|
||||
use element::LineWithInvisibles;
|
||||
pub use element::{
|
||||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
||||
@@ -68,10 +66,10 @@ use gpui::{
|
||||
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
|
||||
Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle,
|
||||
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton, PaintQuad,
|
||||
ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle, Styled, StyledText,
|
||||
Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, View, ViewContext,
|
||||
ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, ListSizingBehavior, Model,
|
||||
MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle,
|
||||
Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle,
|
||||
View, ViewContext, ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -113,7 +111,7 @@ use rpc::{proto::*, ErrorExt};
|
||||
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
|
||||
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use smallvec::SmallVec;
|
||||
use snippet::Snippet;
|
||||
use std::ops::Not as _;
|
||||
@@ -145,7 +143,7 @@ use workspace::notifications::{DetachAndPromptErr, NotificationId};
|
||||
use workspace::{
|
||||
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
use workspace::{OpenInTerminal, OpenTerminal, Toast};
|
||||
use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
|
||||
|
||||
use crate::hover_links::find_url;
|
||||
|
||||
@@ -481,7 +479,7 @@ pub struct Editor {
|
||||
pending_rename: Option<RenameState>,
|
||||
searchable: bool,
|
||||
cursor_shape: CursorShape,
|
||||
current_line_highlight: CurrentLineHighlight,
|
||||
current_line_highlight: Option<CurrentLineHighlight>,
|
||||
collapse_matches: bool,
|
||||
autoindent_mode: Option<AutoindentMode>,
|
||||
workspace: Option<(WeakView<Workspace>, Option<WorkspaceId>)>,
|
||||
@@ -524,6 +522,7 @@ pub struct Editor {
|
||||
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
|
||||
tasks_update_task: Option<Task<()>>,
|
||||
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -554,6 +553,20 @@ pub struct GutterDimensions {
|
||||
pub git_blame_entries_width: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl GutterDimensions {
|
||||
/// The full width of the space taken up by the gutter.
|
||||
pub fn full_width(&self) -> Pixels {
|
||||
self.margin + self.width
|
||||
}
|
||||
|
||||
/// The width of the space reserved for the fold indicators,
|
||||
/// use alongside 'justify_end' and `gutter_width` to
|
||||
/// right align content with the line numbers
|
||||
pub fn fold_area_width(&self) -> Pixels {
|
||||
self.margin + self.right_padding
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GutterDimensions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -1113,7 +1126,8 @@ impl CompletionsMenu {
|
||||
.occlude()
|
||||
.max_h(max_height)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.with_width_from_item(widest_completion_ix);
|
||||
.with_width_from_item(widest_completion_ix)
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer);
|
||||
|
||||
Popover::new()
|
||||
.child(list)
|
||||
@@ -1460,6 +1474,7 @@ impl CodeActionsMenu {
|
||||
})
|
||||
.map(|(ix, _)| ix),
|
||||
)
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer)
|
||||
.into_any_element();
|
||||
|
||||
let cursor_position = if let Some(row) = self.deployed_from_indicator {
|
||||
@@ -1753,7 +1768,7 @@ impl Editor {
|
||||
pending_rename: Default::default(),
|
||||
searchable: true,
|
||||
cursor_shape: Default::default(),
|
||||
current_line_highlight: EditorSettings::get_global(cx).current_line_highlight,
|
||||
current_line_highlight: None,
|
||||
autoindent_mode: Some(AutoindentMode::EachLine),
|
||||
collapse_matches: false,
|
||||
workspace: None,
|
||||
@@ -1810,6 +1825,7 @@ impl Editor {
|
||||
}),
|
||||
],
|
||||
tasks_update_task: None,
|
||||
previous_search_ranges: None,
|
||||
};
|
||||
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
@@ -1977,7 +1993,9 @@ impl Editor {
|
||||
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
|
||||
placeholder_text: self.placeholder_text.clone(),
|
||||
is_focused: self.focus_handle.is_focused(cx),
|
||||
current_line_highlight: self.current_line_highlight,
|
||||
current_line_highlight: self
|
||||
.current_line_highlight
|
||||
.unwrap_or_else(|| EditorSettings::get_global(cx).current_line_highlight),
|
||||
gutter_hovered: self.gutter_hovered,
|
||||
}
|
||||
}
|
||||
@@ -2067,7 +2085,10 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_current_line_highlight(&mut self, current_line_highlight: CurrentLineHighlight) {
|
||||
pub fn set_current_line_highlight(
|
||||
&mut self,
|
||||
current_line_highlight: Option<CurrentLineHighlight>,
|
||||
) {
|
||||
self.current_line_highlight = current_line_highlight;
|
||||
}
|
||||
|
||||
@@ -2798,6 +2819,9 @@ impl Editor {
|
||||
}
|
||||
|
||||
if let Some(bracket_pair) = bracket_pair {
|
||||
let autoclose = self.use_autoclose
|
||||
&& snapshot.settings_at(selection.start, cx).use_autoclose;
|
||||
|
||||
if selection.is_empty() {
|
||||
if is_bracket_pair_start {
|
||||
let prefix_len = bracket_pair.start.len() - text.len();
|
||||
@@ -2818,8 +2842,6 @@ impl Editor {
|
||||
),
|
||||
&bracket_pair.start[..prefix_len],
|
||||
));
|
||||
let autoclose = self.use_autoclose
|
||||
&& snapshot.settings_at(selection.start, cx).use_autoclose;
|
||||
if autoclose
|
||||
&& following_text_allows_autoclose
|
||||
&& preceding_text_matches_prefix
|
||||
@@ -2872,7 +2894,10 @@ impl Editor {
|
||||
}
|
||||
// If an opening bracket is 1 character long and is typed while
|
||||
// text is selected, then surround that text with the bracket pair.
|
||||
else if is_bracket_pair_start && bracket_pair.start.chars().count() == 1 {
|
||||
else if autoclose
|
||||
&& is_bracket_pair_start
|
||||
&& bracket_pair.start.chars().count() == 1
|
||||
{
|
||||
edits.push((selection.start..selection.start, text.clone()));
|
||||
edits.push((
|
||||
selection.end..selection.end,
|
||||
@@ -2995,12 +3020,7 @@ impl Editor {
|
||||
s.select(new_selections)
|
||||
});
|
||||
|
||||
if brace_inserted {
|
||||
// If we inserted a brace while composing text (i.e. typing `"` on a
|
||||
// Brazilian keyboard), exit the composing state because most likely
|
||||
// the user wanted to surround the selection.
|
||||
this.unmark_text(cx);
|
||||
} else if EditorSettings::get_global(cx).use_on_type_format {
|
||||
if !brace_inserted && EditorSettings::get_global(cx).use_on_type_format {
|
||||
if let Some(on_type_format_task) =
|
||||
this.trigger_on_type_formatting(text.to_string(), cx)
|
||||
{
|
||||
@@ -3790,6 +3810,9 @@ impl Editor {
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
let task = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||
})?;
|
||||
let completions = completions.await.log_err();
|
||||
let menu = if let Some(completions) = completions {
|
||||
let mut menu = CompletionsMenu {
|
||||
@@ -3828,7 +3851,6 @@ impl Editor {
|
||||
let delay_ms = EditorSettings::get_global(cx)
|
||||
.completion_documentation_secondary_query_debounce;
|
||||
let delay = Duration::from_millis(delay_ms);
|
||||
|
||||
editor
|
||||
.completion_documentation_pre_resolve_debounce
|
||||
.fire_new(delay, cx, |editor, cx| {
|
||||
@@ -3849,8 +3871,6 @@ impl Editor {
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
|
||||
|
||||
let mut context_menu = this.context_menu.write();
|
||||
match context_menu.as_ref() {
|
||||
None => {}
|
||||
@@ -9248,11 +9268,15 @@ impl Editor {
|
||||
for (block_id, diagnostic) in &active_diagnostics.blocks {
|
||||
new_styles.insert(
|
||||
*block_id,
|
||||
diagnostic_block_renderer(diagnostic.clone(), is_valid),
|
||||
(
|
||||
None,
|
||||
diagnostic_block_renderer(diagnostic.clone(), is_valid),
|
||||
),
|
||||
);
|
||||
}
|
||||
self.display_map
|
||||
.update(cx, |display_map, _| display_map.replace_blocks(new_styles));
|
||||
self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.replace_blocks(new_styles, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9609,12 +9633,12 @@ impl Editor {
|
||||
|
||||
pub fn replace_blocks(
|
||||
&mut self,
|
||||
blocks: HashMap<BlockId, RenderBlock>,
|
||||
blocks: HashMap<BlockId, (Option<u8>, RenderBlock)>,
|
||||
autoscroll: Option<Autoscroll>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.display_map
|
||||
.update(cx, |display_map, _| display_map.replace_blocks(blocks));
|
||||
.update(cx, |display_map, cx| display_map.replace_blocks(blocks, cx));
|
||||
if let Some(autoscroll) = autoscroll {
|
||||
self.request_autoscroll(autoscroll, cx);
|
||||
}
|
||||
@@ -9775,20 +9799,31 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_indent_guides(&mut self, _: &ToggleIndentGuides, cx: &mut ViewContext<Self>) {
|
||||
let currently_enabled = self.should_show_indent_guides(cx);
|
||||
self.show_indent_guides = Some(!currently_enabled);
|
||||
cx.notify();
|
||||
pub fn toggle_tab_bar(&mut self, _: &ToggleTabBar, cx: &mut ViewContext<Self>) {
|
||||
let Some(workspace) = self.workspace() else {
|
||||
return;
|
||||
};
|
||||
let fs = workspace.read(cx).app_state().fs.clone();
|
||||
let current_show = TabBarSettings::get_global(cx).show;
|
||||
update_settings_file::<TabBarSettings>(fs, cx, move |setting| {
|
||||
setting.show = Some(!current_show);
|
||||
});
|
||||
}
|
||||
|
||||
fn should_show_indent_guides(&self, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.show_indent_guides.unwrap_or_else(|| {
|
||||
pub fn toggle_indent_guides(&mut self, _: &ToggleIndentGuides, cx: &mut ViewContext<Self>) {
|
||||
let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.settings_at(0, cx)
|
||||
.indent_guides
|
||||
.enabled
|
||||
})
|
||||
});
|
||||
self.show_indent_guides = Some(!currently_enabled);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn should_show_indent_guides(&self) -> Option<bool> {
|
||||
self.show_indent_guides
|
||||
}
|
||||
|
||||
pub fn toggle_line_numbers(&mut self, _: &ToggleLineNumbers, cx: &mut ViewContext<Self>) {
|
||||
@@ -10241,6 +10276,27 @@ impl Editor {
|
||||
self.background_highlights_in_range(start..end, &snapshot, theme)
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn search_background_highlights(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<Range<Point>> {
|
||||
let snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
|
||||
let highlights = self
|
||||
.background_highlights
|
||||
.get(&TypeId::of::<items::BufferSearchHighlights>());
|
||||
|
||||
if let Some((_color, ranges)) = highlights {
|
||||
ranges
|
||||
.iter()
|
||||
.map(|range| range.start.to_point(&snapshot)..range.end.to_point(&snapshot))
|
||||
.collect_vec()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
fn document_highlights_for_position<'a>(
|
||||
&'a self,
|
||||
position: Anchor,
|
||||
@@ -10589,7 +10645,6 @@ impl Editor {
|
||||
let editor_settings = EditorSettings::get_global(cx);
|
||||
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
|
||||
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
|
||||
self.current_line_highlight = editor_settings.current_line_highlight;
|
||||
|
||||
if self.mode == EditorMode::Full {
|
||||
let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled();
|
||||
|
||||
@@ -20,6 +20,7 @@ use language::{
|
||||
FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
|
||||
Point,
|
||||
};
|
||||
use language_settings::IndentGuideSettings;
|
||||
use multi_buffer::MultiBufferIndentGuide;
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
@@ -11505,6 +11506,7 @@ fn assert_indent_guides(
|
||||
let snapshot = editor.snapshot(cx).display_snapshot;
|
||||
let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
|
||||
MultiBufferRow(range.start)..MultiBufferRow(range.end),
|
||||
true,
|
||||
&snapshot,
|
||||
cx,
|
||||
);
|
||||
@@ -11543,6 +11545,21 @@ fn assert_indent_guides(
|
||||
assert_eq!(indent_guides, expected, "Indent guides do not match");
|
||||
}
|
||||
|
||||
fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
|
||||
IndentGuide {
|
||||
buffer_id,
|
||||
start_row,
|
||||
end_row,
|
||||
depth,
|
||||
tab_size: 4,
|
||||
settings: IndentGuideSettings {
|
||||
enabled: true,
|
||||
line_width: 1,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
|
||||
let (buffer_id, mut cx) = setup_indent_guides_editor(
|
||||
@@ -11555,12 +11572,7 @@ async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..3,
|
||||
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -11576,12 +11588,7 @@ async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -11604,9 +11611,9 @@ async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
0..8,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 6, 0, 4),
|
||||
IndentGuide::new(buffer_id, 3, 3, 1, 4),
|
||||
IndentGuide::new(buffer_id, 5, 5, 1, 4),
|
||||
indent_guide(buffer_id, 1, 6, 0),
|
||||
indent_guide(buffer_id, 3, 3, 1),
|
||||
indent_guide(buffer_id, 5, 5, 1),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11630,8 +11637,8 @@ async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
0..5,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
indent_guide(buffer_id, 1, 3, 0),
|
||||
indent_guide(buffer_id, 2, 2, 1),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11652,12 +11659,7 @@ async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..5,
|
||||
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -11683,9 +11685,9 @@ async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
0..11,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 9, 0, 4),
|
||||
IndentGuide::new(buffer_id, 6, 6, 1, 4),
|
||||
IndentGuide::new(buffer_id, 8, 8, 1, 4),
|
||||
indent_guide(buffer_id, 1, 9, 0),
|
||||
indent_guide(buffer_id, 6, 6, 1),
|
||||
indent_guide(buffer_id, 8, 8, 1),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11715,9 +11717,9 @@ async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
1..11,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 9, 0, 4),
|
||||
IndentGuide::new(buffer_id, 6, 6, 1, 4),
|
||||
IndentGuide::new(buffer_id, 8, 8, 1, 4),
|
||||
indent_guide(buffer_id, 1, 9, 0),
|
||||
indent_guide(buffer_id, 6, 6, 1),
|
||||
indent_guide(buffer_id, 8, 8, 1),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11747,9 +11749,9 @@ async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
1..10,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 9, 0, 4),
|
||||
IndentGuide::new(buffer_id, 6, 6, 1, 4),
|
||||
IndentGuide::new(buffer_id, 8, 8, 1, 4),
|
||||
indent_guide(buffer_id, 1, 9, 0),
|
||||
indent_guide(buffer_id, 6, 6, 1),
|
||||
indent_guide(buffer_id, 8, 8, 1),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11775,9 +11777,9 @@ async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
1..10,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 4, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 3, 1, 4),
|
||||
IndentGuide::new(buffer_id, 3, 3, 2, 4),
|
||||
indent_guide(buffer_id, 1, 4, 0),
|
||||
indent_guide(buffer_id, 2, 3, 1),
|
||||
indent_guide(buffer_id, 3, 3, 2),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11802,8 +11804,8 @@ async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext)
|
||||
assert_indent_guides(
|
||||
0..6,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 2, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
indent_guide(buffer_id, 1, 2, 0),
|
||||
indent_guide(buffer_id, 2, 2, 1),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11825,12 +11827,7 @@ async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext)
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_indent_guides(
|
||||
0..1,
|
||||
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
|
||||
None,
|
||||
&mut cx,
|
||||
);
|
||||
assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -11852,8 +11849,8 @@ async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
|
||||
assert_indent_guides(
|
||||
0..6,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 6, 0, 4),
|
||||
IndentGuide::new(buffer_id, 3, 4, 1, 4),
|
||||
indent_guide(buffer_id, 1, 6, 0),
|
||||
indent_guide(buffer_id, 3, 4, 1),
|
||||
],
|
||||
None,
|
||||
&mut cx,
|
||||
@@ -11880,7 +11877,7 @@ async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
assert_indent_guides(
|
||||
0..3,
|
||||
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
|
||||
vec![indent_guide(buffer_id, 1, 1, 0)],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
);
|
||||
@@ -11909,8 +11906,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
indent_guide(buffer_id, 1, 3, 0),
|
||||
indent_guide(buffer_id, 2, 2, 1),
|
||||
],
|
||||
Some(vec![1]),
|
||||
&mut cx,
|
||||
@@ -11925,8 +11922,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
indent_guide(buffer_id, 1, 3, 0),
|
||||
indent_guide(buffer_id, 2, 2, 1),
|
||||
],
|
||||
Some(vec![1]),
|
||||
&mut cx,
|
||||
@@ -11941,8 +11938,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
|
||||
assert_indent_guides(
|
||||
0..4,
|
||||
vec![
|
||||
IndentGuide::new(buffer_id, 1, 3, 0, 4),
|
||||
IndentGuide::new(buffer_id, 2, 2, 1, 4),
|
||||
indent_guide(buffer_id, 1, 3, 0),
|
||||
indent_guide(buffer_id, 2, 2, 1),
|
||||
],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
@@ -11971,7 +11968,7 @@ async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
assert_indent_guides(
|
||||
0..5,
|
||||
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
|
||||
vec![indent_guide(buffer_id, 1, 3, 0)],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
);
|
||||
@@ -11997,7 +11994,7 @@ async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppCont
|
||||
|
||||
assert_indent_guides(
|
||||
0..3,
|
||||
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
|
||||
vec![indent_guide(buffer_id, 1, 2, 0)],
|
||||
Some(vec![0]),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::{
|
||||
IndentGuideBackgroundColoring, IndentGuideColoring, ShowWhitespaceSetting,
|
||||
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
|
||||
@@ -318,6 +318,7 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::open_excerpts);
|
||||
register_action(view, cx, Editor::open_excerpts_in_split);
|
||||
register_action(view, cx, Editor::toggle_soft_wrap);
|
||||
register_action(view, cx, Editor::toggle_tab_bar);
|
||||
register_action(view, cx, Editor::toggle_line_numbers);
|
||||
register_action(view, cx, Editor::toggle_indent_guides);
|
||||
register_action(view, cx, Editor::toggle_inlay_hints);
|
||||
@@ -1125,9 +1126,7 @@ impl EditorElement {
|
||||
ix as f32 * line_height - (scroll_pixel_position.y % line_height),
|
||||
);
|
||||
let centering_offset = point(
|
||||
(gutter_dimensions.right_padding + gutter_dimensions.margin
|
||||
- fold_indicator_size.width)
|
||||
/ 2.,
|
||||
(gutter_dimensions.fold_area_width() - fold_indicator_size.width) / 2.,
|
||||
(line_height - fold_indicator_size.height) / 2.,
|
||||
);
|
||||
let origin = gutter_hitbox.origin + position + centering_offset;
|
||||
@@ -1222,34 +1221,41 @@ impl EditorElement {
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
|
||||
let git_gutter_setting = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.git_gutter
|
||||
.unwrap_or_default();
|
||||
buffer_snapshot
|
||||
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
|
||||
.map(|hunk| diff_hunk_to_display(&hunk, snapshot))
|
||||
.dedup()
|
||||
.map(|hunk| {
|
||||
let hitbox = if let DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} = &hunk
|
||||
{
|
||||
let was_expanded = expanded_hunk_display_rows
|
||||
.get(&display_row_range.start)
|
||||
.map(|expanded_end_row| expanded_end_row == &display_row_range.end)
|
||||
.unwrap_or(false);
|
||||
if was_expanded {
|
||||
None
|
||||
.map(|hunk| match git_gutter_setting {
|
||||
GitGutterSetting::TrackedFiles => {
|
||||
let hitbox = if let DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} = &hunk
|
||||
{
|
||||
let was_expanded = expanded_hunk_display_rows
|
||||
.get(&display_row_range.start)
|
||||
.map(|expanded_end_row| expanded_end_row == &display_row_range.end)
|
||||
.unwrap_or(false);
|
||||
if was_expanded {
|
||||
None
|
||||
} else {
|
||||
let hunk_bounds = Self::diff_hunk_bounds(
|
||||
&snapshot,
|
||||
line_height,
|
||||
gutter_hitbox.bounds,
|
||||
&hunk,
|
||||
);
|
||||
Some(cx.insert_hitbox(hunk_bounds, true))
|
||||
}
|
||||
} else {
|
||||
let hunk_bounds = Self::diff_hunk_bounds(
|
||||
&snapshot,
|
||||
line_height,
|
||||
gutter_hitbox.bounds,
|
||||
&hunk,
|
||||
);
|
||||
Some(cx.insert_hitbox(hunk_bounds, true))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(hunk, hitbox)
|
||||
None
|
||||
};
|
||||
(hunk, hitbox)
|
||||
}
|
||||
GitGutterSetting::Hide => (hunk, None),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1438,6 +1444,7 @@ impl EditorElement {
|
||||
single_indent_width,
|
||||
depth: indent_guide.depth,
|
||||
active: active_indent_guide_indices.contains(&i),
|
||||
settings: indent_guide.settings,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -2730,14 +2737,6 @@ impl EditorElement {
|
||||
return;
|
||||
};
|
||||
|
||||
let settings = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.settings_at(0, cx)
|
||||
.indent_guides;
|
||||
|
||||
let faded_color = |color: Hsla, alpha: f32| {
|
||||
let mut faded = color;
|
||||
faded.a = alpha;
|
||||
@@ -2746,6 +2745,7 @@ impl EditorElement {
|
||||
|
||||
for indent_guide in indent_guides {
|
||||
let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
|
||||
let settings = indent_guide.settings;
|
||||
|
||||
// TODO fixed for now, expose them through themes later
|
||||
const INDENT_AWARE_ALPHA: f32 = 0.2;
|
||||
@@ -2753,7 +2753,7 @@ impl EditorElement {
|
||||
const INDENT_AWARE_BACKGROUND_ALPHA: f32 = 0.1;
|
||||
const INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA: f32 = 0.2;
|
||||
|
||||
let line_color = match (&settings.coloring, indent_guide.active) {
|
||||
let line_color = match (settings.coloring, indent_guide.active) {
|
||||
(IndentGuideColoring::Disabled, _) => None,
|
||||
(IndentGuideColoring::Fixed, false) => {
|
||||
Some(cx.theme().colors().editor_indent_guide)
|
||||
@@ -2769,7 +2769,7 @@ impl EditorElement {
|
||||
}
|
||||
};
|
||||
|
||||
let background_color = match (&settings.background_coloring, indent_guide.active) {
|
||||
let background_color = match (settings.background_coloring, indent_guide.active) {
|
||||
(IndentGuideBackgroundColoring::Disabled, _) => None,
|
||||
(IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
|
||||
indent_accent_colors,
|
||||
@@ -4072,6 +4072,7 @@ impl LineWithInvisibles {
|
||||
if non_whitespace_added || !inside_wrapped_string {
|
||||
invisibles.push(Invisible::Tab {
|
||||
line_start_offset: line.len(),
|
||||
line_end_offset: line.len() + line_chunk.len(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -4187,16 +4188,15 @@ impl LineWithInvisibles {
|
||||
whitespace_setting: ShowWhitespaceSetting,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let allowed_invisibles_regions = match whitespace_setting {
|
||||
ShowWhitespaceSetting::None => return,
|
||||
ShowWhitespaceSetting::Selection => Some(selection_ranges),
|
||||
ShowWhitespaceSetting::All => None,
|
||||
};
|
||||
|
||||
for invisible in &self.invisibles {
|
||||
let (&token_offset, invisible_symbol) = match invisible {
|
||||
Invisible::Tab { line_start_offset } => (line_start_offset, &layout.tab_invisible),
|
||||
Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible),
|
||||
let extract_whitespace_info = |invisible: &Invisible| {
|
||||
let (token_offset, token_end_offset, invisible_symbol) = match invisible {
|
||||
Invisible::Tab {
|
||||
line_start_offset,
|
||||
line_end_offset,
|
||||
} => (*line_start_offset, *line_end_offset, &layout.tab_invisible),
|
||||
Invisible::Whitespace { line_offset } => {
|
||||
(*line_offset, line_offset + 1, &layout.space_invisible)
|
||||
}
|
||||
};
|
||||
|
||||
let x_offset = self.x_for_index(token_offset);
|
||||
@@ -4208,17 +4208,73 @@ impl LineWithInvisibles {
|
||||
line_y,
|
||||
);
|
||||
|
||||
if let Some(allowed_regions) = allowed_invisibles_regions {
|
||||
let invisible_point = DisplayPoint::new(row, token_offset as u32);
|
||||
if !allowed_regions
|
||||
(
|
||||
[token_offset, token_end_offset],
|
||||
Box::new(move |cx: &mut WindowContext| {
|
||||
invisible_symbol.paint(origin, line_height, cx).log_err();
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
let invisible_iter = self.invisibles.iter().map(extract_whitespace_info);
|
||||
match whitespace_setting {
|
||||
ShowWhitespaceSetting::None => return,
|
||||
ShowWhitespaceSetting::All => invisible_iter.for_each(|(_, paint)| paint(cx)),
|
||||
ShowWhitespaceSetting::Selection => invisible_iter.for_each(|([start, _], paint)| {
|
||||
let invisible_point = DisplayPoint::new(row, start as u32);
|
||||
if !selection_ranges
|
||||
.iter()
|
||||
.any(|region| region.start <= invisible_point && invisible_point < region.end)
|
||||
{
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
paint(cx);
|
||||
}),
|
||||
|
||||
// For a whitespace to be on a boundary, any of the following conditions need to be met:
|
||||
// - It is a tab
|
||||
// - It is adjacent to an edge (start or end)
|
||||
// - It is adjacent to a whitespace (left or right)
|
||||
ShowWhitespaceSetting::Boundary => {
|
||||
// We'll need to keep track of the last invisible we've seen and then check if we are adjacent to it for some of
|
||||
// the above cases.
|
||||
// Note: We zip in the original `invisibles` to check for tab equality
|
||||
let mut last_seen: Option<(bool, usize, Box<dyn Fn(&mut WindowContext)>)> = None;
|
||||
for (([start, end], paint), invisible) in
|
||||
invisible_iter.zip_eq(self.invisibles.iter())
|
||||
{
|
||||
let should_render = match (&last_seen, invisible) {
|
||||
(_, Invisible::Tab { .. }) => true,
|
||||
(Some((_, last_end, _)), _) => *last_end == start,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if should_render || start == 0 || end == self.len {
|
||||
paint(cx);
|
||||
|
||||
// Since we are scanning from the left, we will skip over the first available whitespace that is part
|
||||
// of a boundary between non-whitespace segments, so we correct by manually redrawing it if needed.
|
||||
if let Some((should_render_last, last_end, paint_last)) = last_seen {
|
||||
// Note that we need to make sure that the last one is actually adjacent
|
||||
if !should_render_last && last_end == start {
|
||||
paint_last(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manually render anything within a selection
|
||||
let invisible_point = DisplayPoint::new(row, start as u32);
|
||||
if selection_ranges.iter().any(|region| {
|
||||
region.start <= invisible_point && invisible_point < region.end
|
||||
}) {
|
||||
paint(cx);
|
||||
}
|
||||
|
||||
last_seen = Some((should_render, end, paint));
|
||||
}
|
||||
}
|
||||
invisible_symbol.paint(origin, line_height, cx).log_err();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn x_for_index(&self, index: usize) -> Pixels {
|
||||
@@ -4308,8 +4364,18 @@ impl LineWithInvisibles {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Invisible {
|
||||
Tab { line_start_offset: usize },
|
||||
Whitespace { line_offset: usize },
|
||||
/// A tab character
|
||||
///
|
||||
/// A tab character is internally represented by spaces (configured by the user's tab width)
|
||||
/// aligned to the nearest column, so it's necessary to store the start and end offset for
|
||||
/// adjacency checks.
|
||||
Tab {
|
||||
line_start_offset: usize,
|
||||
line_end_offset: usize,
|
||||
},
|
||||
Whitespace {
|
||||
line_offset: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl EditorElement {
|
||||
@@ -4635,7 +4701,7 @@ impl Element for EditorElement {
|
||||
&mut scroll_width,
|
||||
&gutter_dimensions,
|
||||
em_width,
|
||||
gutter_dimensions.width + gutter_dimensions.margin,
|
||||
gutter_dimensions.full_width(),
|
||||
line_height,
|
||||
&line_layouts,
|
||||
cx,
|
||||
@@ -5286,6 +5352,7 @@ pub struct IndentGuideLayout {
|
||||
single_indent_width: Pixels,
|
||||
depth: u32,
|
||||
active: bool,
|
||||
settings: IndentGuideSettings,
|
||||
}
|
||||
|
||||
pub struct CursorLayout {
|
||||
@@ -5853,15 +5920,18 @@ mod tests {
|
||||
let expected_invisibles = vec![
|
||||
Invisible::Tab {
|
||||
line_start_offset: 0,
|
||||
line_end_offset: TAB_SIZE as usize,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: TAB_SIZE as usize,
|
||||
},
|
||||
Invisible::Tab {
|
||||
line_start_offset: TAB_SIZE as usize + 1,
|
||||
line_end_offset: TAB_SIZE as usize * 2,
|
||||
},
|
||||
Invisible::Tab {
|
||||
line_start_offset: TAB_SIZE as usize * 2 + 1,
|
||||
line_end_offset: TAB_SIZE as usize * 3,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: TAB_SIZE as usize * 3 + 1,
|
||||
@@ -5915,10 +5985,11 @@ mod tests {
|
||||
#[gpui::test]
|
||||
fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
|
||||
let tab_size = 4;
|
||||
let input_text = "a\tbcd ".repeat(9);
|
||||
let input_text = "a\tbcd ".repeat(9);
|
||||
let repeated_invisibles = [
|
||||
Invisible::Tab {
|
||||
line_start_offset: 1,
|
||||
line_end_offset: tab_size as usize,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 3,
|
||||
@@ -5929,6 +6000,12 @@ mod tests {
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 5,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 6,
|
||||
},
|
||||
Invisible::Whitespace {
|
||||
line_offset: tab_size as usize + 7,
|
||||
},
|
||||
];
|
||||
let expected_invisibles = std::iter::once(repeated_invisibles)
|
||||
.cycle()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
hover_popover::{self, InlayHover},
|
||||
scroll::ScrollAmount,
|
||||
Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, InlayId,
|
||||
PointForPosition, SelectPhase,
|
||||
};
|
||||
@@ -38,7 +39,11 @@ impl RangeInEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
|
||||
pub fn point_within_range(
|
||||
&self,
|
||||
trigger_point: &TriggerPoint,
|
||||
snapshot: &EditorSnapshot,
|
||||
) -> bool {
|
||||
match (self, trigger_point) {
|
||||
(Self::Text(range), TriggerPoint::Text(point)) => {
|
||||
let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
|
||||
@@ -169,6 +174,21 @@ impl Editor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn scroll_hover(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) -> bool {
|
||||
let selection = self.selections.newest_anchor().head();
|
||||
let snapshot = self.snapshot(cx);
|
||||
|
||||
let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
|
||||
popover
|
||||
.symbol_range
|
||||
.point_within_range(&TriggerPoint::Text(selection), &snapshot)
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
popover.scroll(amount, cx);
|
||||
true
|
||||
}
|
||||
|
||||
fn cmd_click_reveal_task(
|
||||
&mut self,
|
||||
point: PointForPosition,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use crate::{
|
||||
display_map::{InlayOffset, ToDisplayPoint},
|
||||
hover_links::{InlayHighlight, RangeInEditor},
|
||||
scroll::ScrollAmount,
|
||||
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
|
||||
EditorStyle, ExcerptId, Hover, RangeToAnchorExt,
|
||||
};
|
||||
use futures::{stream::FuturesUnordered, FutureExt};
|
||||
use gpui::{
|
||||
div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task,
|
||||
ViewContext, WeakView,
|
||||
ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled,
|
||||
Task, ViewContext, WeakView,
|
||||
};
|
||||
use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
|
||||
|
||||
@@ -118,6 +119,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
|
||||
let hover_popover = InfoPopover {
|
||||
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -317,6 +319,7 @@ fn show_hover(
|
||||
InfoPopover {
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -423,7 +426,7 @@ async fn parse_blocks(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct HoverState {
|
||||
pub info_popovers: Vec<InfoPopover>,
|
||||
pub diagnostic_popover: Option<DiagnosticPopover>,
|
||||
@@ -487,10 +490,11 @@ impl HoverState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct InfoPopover {
|
||||
symbol_range: RangeInEditor,
|
||||
parsed_content: ParsedMarkdown,
|
||||
pub symbol_range: RangeInEditor,
|
||||
pub parsed_content: ParsedMarkdown,
|
||||
pub scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
impl InfoPopover {
|
||||
@@ -504,23 +508,33 @@ impl InfoPopover {
|
||||
div()
|
||||
.id("info_popover")
|
||||
.elevation_2(cx)
|
||||
.p_2()
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
// Prevent a mouse down/move on the popover from being propagated to the editor,
|
||||
// because that would dismiss the popover.
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.child(crate::render_parsed_markdown(
|
||||
.child(div().p_2().child(crate::render_parsed_markdown(
|
||||
"content",
|
||||
&self.parsed_content,
|
||||
style,
|
||||
workspace,
|
||||
cx,
|
||||
))
|
||||
)))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
|
||||
let mut current = self.scroll_handle.offset();
|
||||
current.y -= amount.pixels(
|
||||
cx.line_height(),
|
||||
self.scroll_handle.bounds().size.height - px(16.),
|
||||
) / 2.0;
|
||||
cx.notify();
|
||||
self.scroll_handle.set_offset(current);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -10,7 +10,7 @@ use language::Buffer;
|
||||
use multi_buffer::{
|
||||
Anchor, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint,
|
||||
};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::SettingsStore;
|
||||
use text::{BufferId, Point};
|
||||
use ui::{
|
||||
div, ActiveTheme, Context as _, IntoElement, ParentElement, Styled, ViewContext, VisualContext,
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
editor_settings::CurrentLineHighlight,
|
||||
git::{diff_hunk_to_display, DisplayDiffHunk},
|
||||
hunk_status, hunks_for_selections, BlockDisposition, BlockId, BlockProperties, BlockStyle,
|
||||
DiffRowHighlight, Editor, EditorSettings, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt,
|
||||
DiffRowHighlight, Editor, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt,
|
||||
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
|
||||
};
|
||||
|
||||
@@ -320,7 +320,7 @@ impl Editor {
|
||||
div()
|
||||
.bg(deleted_hunk_color)
|
||||
.size_full()
|
||||
.pl(gutter_dimensions.width + gutter_dimensions.margin)
|
||||
.pl(gutter_dimensions.full_width())
|
||||
.child(editor_with_deleted_text.clone())
|
||||
.into_any_element()
|
||||
}),
|
||||
@@ -591,7 +591,7 @@ fn editor_with_deleted_text(
|
||||
let subscription_editor = parent_editor.clone();
|
||||
editor._subscriptions.extend([
|
||||
cx.on_blur(&editor.focus_handle, |editor, cx| {
|
||||
editor.set_current_line_highlight(CurrentLineHighlight::None);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.try_cancel();
|
||||
});
|
||||
@@ -602,14 +602,14 @@ fn editor_with_deleted_text(
|
||||
{
|
||||
parent_editor.read(cx).current_line_highlight
|
||||
} else {
|
||||
EditorSettings::get_global(cx).current_line_highlight
|
||||
None
|
||||
};
|
||||
editor.set_current_line_highlight(restored_highlight);
|
||||
cx.notify();
|
||||
}),
|
||||
cx.observe_global::<SettingsStore>(|editor, cx| {
|
||||
if !editor.is_focused(cx) {
|
||||
editor.set_current_line_highlight(CurrentLineHighlight::None);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{ops::Range, time::Duration};
|
||||
|
||||
use collections::HashSet;
|
||||
use gpui::{AppContext, Task};
|
||||
use language::BufferRow;
|
||||
use language::{language_settings::language_settings, BufferRow};
|
||||
use multi_buffer::{MultiBufferIndentGuide, MultiBufferRow};
|
||||
use text::{BufferId, LineIndent, Point};
|
||||
use ui::ViewContext;
|
||||
@@ -37,13 +37,26 @@ impl Editor {
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<Vec<MultiBufferIndentGuide>> {
|
||||
let enabled = self.should_show_indent_guides(cx);
|
||||
let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
language_settings(buffer.read(cx).language(), buffer.read(cx).file(), cx)
|
||||
.indent_guides
|
||||
.enabled
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if enabled {
|
||||
Some(indent_guides_in_range(visible_buffer_range, snapshot, cx))
|
||||
} else {
|
||||
None
|
||||
if !show_indent_guides {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(indent_guides_in_range(
|
||||
visible_buffer_range,
|
||||
self.should_show_indent_guides() == Some(true),
|
||||
snapshot,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn find_active_indent_guide_indices(
|
||||
@@ -77,9 +90,14 @@ impl Editor {
|
||||
|
||||
if state.should_refresh() {
|
||||
state.cursor_row = cursor_row;
|
||||
let snapshot = snapshot.clone();
|
||||
state.dirty = false;
|
||||
|
||||
if indent_guides.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let snapshot = snapshot.clone();
|
||||
|
||||
let task = cx
|
||||
.background_executor()
|
||||
.spawn(resolve_indented_range(snapshot, cursor_row));
|
||||
@@ -131,6 +149,7 @@ impl Editor {
|
||||
|
||||
pub fn indent_guides_in_range(
|
||||
visible_buffer_range: Range<MultiBufferRow>,
|
||||
ignore_disabled_for_language: bool,
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &AppContext,
|
||||
) -> Vec<MultiBufferIndentGuide> {
|
||||
@@ -143,7 +162,7 @@ pub fn indent_guides_in_range(
|
||||
|
||||
snapshot
|
||||
.buffer_snapshot
|
||||
.indent_guides_in_range(start_anchor..end_anchor, cx)
|
||||
.indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
|
||||
.into_iter()
|
||||
.filter(|indent_guide| {
|
||||
// Filter out indent guides that are inside a fold
|
||||
|
||||
@@ -13,8 +13,7 @@ use gpui::{
|
||||
VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
|
||||
Point, SelectionGoal,
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
|
||||
};
|
||||
use multi_buffer::AnchorRangeExt;
|
||||
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
||||
@@ -1008,6 +1007,25 @@ impl SearchableItem for Editor {
|
||||
self.has_background_highlights::<SearchWithinRange>()
|
||||
}
|
||||
|
||||
fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
|
||||
if self.has_filtered_search_ranges() {
|
||||
self.previous_search_ranges = self
|
||||
.clear_background_highlights::<SearchWithinRange>(cx)
|
||||
.map(|(_, ranges)| ranges)
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let ranges = self.selections.disjoint_anchor_ranges();
|
||||
if ranges.iter().any(|range| range.start != range.end) {
|
||||
self.set_search_within_ranges(&ranges, cx);
|
||||
} else if let Some(previous_search_ranges) = self.previous_search_ranges.take() {
|
||||
self.set_search_within_ranges(&previous_search_ranges, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
|
||||
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
|
||||
let snapshot = &self.snapshot(cx).buffer_snapshot;
|
||||
@@ -1016,9 +1034,14 @@ impl SearchableItem for Editor {
|
||||
match setting {
|
||||
SeedQuerySetting::Never => String::new(),
|
||||
SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => {
|
||||
snapshot
|
||||
let text: String = snapshot
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect()
|
||||
.collect();
|
||||
if text.contains('\n') {
|
||||
String::new()
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
SeedQuerySetting::Selection => String::new(),
|
||||
SeedQuerySetting::Always => {
|
||||
@@ -1135,58 +1158,64 @@ impl SearchableItem for Editor {
|
||||
let search_within_ranges = self
|
||||
.background_highlights
|
||||
.get(&TypeId::of::<SearchWithinRange>())
|
||||
.map(|(_color, ranges)| {
|
||||
ranges
|
||||
.iter()
|
||||
.map(|range| range.to_offset(&buffer))
|
||||
.collect::<Vec<_>>()
|
||||
.map_or(vec![], |(_color, ranges)| {
|
||||
ranges.iter().map(|range| range.clone()).collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
|
||||
if let Some(search_within_ranges) = search_within_ranges {
|
||||
for range in search_within_ranges {
|
||||
let offset = range.start;
|
||||
ranges.extend(
|
||||
query
|
||||
.search(excerpt_buffer, Some(range))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
buffer.anchor_after(range.start + offset)
|
||||
..buffer.anchor_before(range.end + offset)
|
||||
}),
|
||||
);
|
||||
}
|
||||
let search_within_ranges = if search_within_ranges.is_empty() {
|
||||
vec![None]
|
||||
} else {
|
||||
ranges.extend(query.search(excerpt_buffer, None).await.into_iter().map(
|
||||
|range| buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||
));
|
||||
search_within_ranges
|
||||
.into_iter()
|
||||
.map(|range| Some(range.to_offset(&buffer)))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
for range in search_within_ranges {
|
||||
let buffer = &buffer;
|
||||
ranges.extend(
|
||||
query
|
||||
.search(excerpt_buffer, range.clone())
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|matched_range| {
|
||||
let offset = range.clone().map(|r| r.start).unwrap_or(0);
|
||||
buffer.anchor_after(matched_range.start + offset)
|
||||
..buffer.anchor_before(matched_range.end + offset)
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
|
||||
if let Some(next_excerpt) = excerpt.next {
|
||||
let excerpt_range =
|
||||
next_excerpt.range.context.to_offset(&next_excerpt.buffer);
|
||||
ranges.extend(
|
||||
query
|
||||
.search(&next_excerpt.buffer, Some(excerpt_range.clone()))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let start = next_excerpt
|
||||
.buffer
|
||||
.anchor_after(excerpt_range.start + range.start);
|
||||
let end = next_excerpt
|
||||
.buffer
|
||||
.anchor_before(excerpt_range.start + range.end);
|
||||
buffer.anchor_in_excerpt(next_excerpt.id, start).unwrap()
|
||||
..buffer.anchor_in_excerpt(next_excerpt.id, end).unwrap()
|
||||
}),
|
||||
);
|
||||
}
|
||||
let search_within_ranges = if search_within_ranges.is_empty() {
|
||||
vec![buffer.anchor_before(0)..buffer.anchor_after(buffer.len())]
|
||||
} else {
|
||||
search_within_ranges
|
||||
};
|
||||
|
||||
for (excerpt_id, search_buffer, search_range) in
|
||||
buffer.excerpts_in_ranges(search_within_ranges)
|
||||
{
|
||||
ranges.extend(
|
||||
query
|
||||
.search(&search_buffer, Some(search_range.clone()))
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|match_range| {
|
||||
let start = search_buffer
|
||||
.anchor_after(search_range.start + match_range.start);
|
||||
let end = search_buffer
|
||||
.anchor_before(search_range.start + match_range.end);
|
||||
buffer.anchor_in_excerpt(excerpt_id, start).unwrap()
|
||||
..buffer.anchor_in_excerpt(excerpt_id, end).unwrap()
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ranges
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::Editor;
|
||||
use serde::Deserialize;
|
||||
use ui::{px, Pixels};
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub enum ScrollAmount {
|
||||
// Scroll N lines (positive is towards the end of the document)
|
||||
Line(f32),
|
||||
@@ -25,4 +26,11 @@ impl ScrollAmount {
|
||||
.unwrap_or(0.),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pixels(&self, line_height: Pixels, height: Pixels) -> Pixels {
|
||||
match self {
|
||||
ScrollAmount::Line(x) => px(line_height.0 * x),
|
||||
ScrollAmount::Page(x) => px(height.0 * x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +273,13 @@ impl SelectionsCollection {
|
||||
self.all(cx).last().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn disjoint_anchor_ranges(&self) -> Vec<Range<Anchor>> {
|
||||
self.disjoint_anchors()
|
||||
.iter()
|
||||
.map(|s| s.start..s.end)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
|
||||
&self,
|
||||
|
||||
@@ -39,7 +39,7 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
&self,
|
||||
_query: String,
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<String>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
@@ -100,6 +100,7 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
}
|
||||
}),
|
||||
}],
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ impl ExtensionStore {
|
||||
let reload_tx = this.reload_tx.clone();
|
||||
let installed_dir = this.installed_dir.clone();
|
||||
async move {
|
||||
let mut paths = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
|
||||
let (mut paths, _) = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
|
||||
while let Some(paths) = paths.next().await {
|
||||
for path in paths {
|
||||
let Ok(event_path) = path.strip_prefix(&installed_dir) else {
|
||||
|
||||
@@ -12,7 +12,7 @@ use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, canvas, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
|
||||
actions, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
|
||||
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
|
||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
@@ -938,24 +938,10 @@ impl Render for ExtensionsPage {
|
||||
let view = cx.view().clone();
|
||||
let scroll_handle = self.list.clone();
|
||||
this.child(
|
||||
canvas(
|
||||
move |bounds, cx| {
|
||||
let mut list = uniform_list::<_, ExtensionCard, _>(
|
||||
view,
|
||||
"entries",
|
||||
count,
|
||||
Self::render_extensions,
|
||||
)
|
||||
.size_full()
|
||||
.pb_4()
|
||||
.track_scroll(scroll_handle)
|
||||
.into_any_element();
|
||||
list.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
|
||||
list
|
||||
},
|
||||
|_bounds, mut list, cx| list.paint(cx),
|
||||
)
|
||||
.size_full(),
|
||||
uniform_list(view, "entries", count, Self::render_extensions)
|
||||
.flex_grow()
|
||||
.pb_4()
|
||||
.track_scroll(scroll_handle),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -36,6 +36,11 @@ use smol::io::AsyncReadExt;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::ffi::OsStr;
|
||||
|
||||
pub trait Watcher: Send + Sync {
|
||||
fn add(&self, path: &Path) -> Result<()>;
|
||||
fn remove(&self, path: &Path) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Fs: Send + Sync {
|
||||
async fn create_dir(&self, path: &Path) -> Result<()>;
|
||||
@@ -79,7 +84,10 @@ pub trait Fs: Send + Sync {
|
||||
&self,
|
||||
path: &Path,
|
||||
latency: Duration,
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>;
|
||||
) -> (
|
||||
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
|
||||
Arc<dyn Watcher>,
|
||||
);
|
||||
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
|
||||
fn is_fake(&self) -> bool;
|
||||
@@ -126,6 +134,13 @@ pub struct RealFs {
|
||||
git_binary_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct RealWatcher {
|
||||
#[cfg(target_os = "linux")]
|
||||
root_path: PathBuf,
|
||||
#[cfg(target_os = "linux")]
|
||||
fs_watcher: parking_lot::Mutex<notify::INotifyWatcher>,
|
||||
}
|
||||
|
||||
impl RealFs {
|
||||
pub fn new(
|
||||
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
@@ -409,7 +424,10 @@ impl Fs for RealFs {
|
||||
&self,
|
||||
path: &Path,
|
||||
latency: Duration,
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
|
||||
) -> (
|
||||
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
|
||||
Arc<dyn Watcher>,
|
||||
) {
|
||||
use fsevent::EventStream;
|
||||
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
@@ -421,22 +439,76 @@ impl Fs for RealFs {
|
||||
});
|
||||
});
|
||||
|
||||
Box::pin(rx.chain(futures::stream::once(async move {
|
||||
drop(handle);
|
||||
vec![]
|
||||
})))
|
||||
(
|
||||
Box::pin(rx.chain(futures::stream::once(async move {
|
||||
drop(handle);
|
||||
vec![]
|
||||
}))),
|
||||
Arc::new(RealWatcher {}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn watch(
|
||||
&self,
|
||||
path: &Path,
|
||||
_latency: Duration,
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
|
||||
use notify::{event::EventKind, event::ModifyKind, Watcher};
|
||||
// todo(linux): This spawns two threads, while the macOS impl
|
||||
// only spawns one. Can we use a OnceLock or some such to make
|
||||
// this better
|
||||
) -> (
|
||||
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
|
||||
Arc<dyn Watcher>,
|
||||
) {
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
|
||||
let file_watcher = notify::recommended_watcher({
|
||||
let tx = tx.clone();
|
||||
move |event: Result<notify::Event, _>| {
|
||||
if let Some(event) = event.log_err() {
|
||||
tx.try_send(event.paths).ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("Could not start file watcher");
|
||||
|
||||
let watcher = Arc::new(RealWatcher {
|
||||
root_path: path.to_path_buf(),
|
||||
fs_watcher: parking_lot::Mutex::new(file_watcher),
|
||||
});
|
||||
|
||||
watcher.add(path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
|
||||
|
||||
// watch the parent dir so we can tell when settings.json is created
|
||||
if let Some(parent) = path.parent() {
|
||||
watcher.add(parent).log_err();
|
||||
}
|
||||
|
||||
(
|
||||
Box::pin(rx.filter_map({
|
||||
let watcher = watcher.clone();
|
||||
move |mut paths| {
|
||||
paths.retain(|path| path.starts_with(&watcher.root_path));
|
||||
async move {
|
||||
if paths.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(paths)
|
||||
}
|
||||
}
|
||||
}
|
||||
})),
|
||||
watcher,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn watch(
|
||||
&self,
|
||||
path: &Path,
|
||||
_latency: Duration,
|
||||
) -> (
|
||||
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
|
||||
Arc<dyn Watcher>,
|
||||
) {
|
||||
use notify::Watcher;
|
||||
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
|
||||
@@ -452,56 +524,15 @@ impl Fs for RealFs {
|
||||
|
||||
file_watcher
|
||||
.watch(path, notify::RecursiveMode::Recursive)
|
||||
.ok(); // It's ok if this fails, the parent watcher will add it.
|
||||
.log_err();
|
||||
|
||||
let mut parent_watcher = notify::recommended_watcher({
|
||||
let watched_path = path.to_path_buf();
|
||||
let tx = tx.clone();
|
||||
move |event: Result<notify::Event, _>| {
|
||||
if let Some(event) = event.ok() {
|
||||
if event.paths.into_iter().any(|path| *path == watched_path) {
|
||||
match event.kind {
|
||||
EventKind::Modify(ev) => {
|
||||
if matches!(ev, ModifyKind::Name(_)) {
|
||||
file_watcher
|
||||
.watch(
|
||||
watched_path.as_path(),
|
||||
notify::RecursiveMode::Recursive,
|
||||
)
|
||||
.log_err();
|
||||
let _ = tx.try_send(vec![watched_path.clone()]).ok();
|
||||
}
|
||||
}
|
||||
EventKind::Create(_) => {
|
||||
file_watcher
|
||||
.watch(watched_path.as_path(), notify::RecursiveMode::Recursive)
|
||||
.log_err();
|
||||
let _ = tx.try_send(vec![watched_path.clone()]).ok();
|
||||
}
|
||||
EventKind::Remove(_) => {
|
||||
file_watcher.unwatch(&watched_path).log_err();
|
||||
let _ = tx.try_send(vec![watched_path.clone()]).ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("Could not start file watcher");
|
||||
|
||||
parent_watcher
|
||||
.watch(
|
||||
path.parent()
|
||||
.expect("Watching root is probably not what you want"),
|
||||
notify::RecursiveMode::NonRecursive,
|
||||
)
|
||||
.expect("Could not start watcher on parent directory");
|
||||
|
||||
Box::pin(rx.chain(futures::stream::once(async move {
|
||||
drop(parent_watcher);
|
||||
vec![]
|
||||
})))
|
||||
(
|
||||
Box::pin(rx.chain(futures::stream::once(async move {
|
||||
drop(file_watcher);
|
||||
vec![]
|
||||
}))),
|
||||
Arc::new(RealWatcher {}),
|
||||
)
|
||||
}
|
||||
|
||||
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
|
||||
@@ -560,6 +591,36 @@ impl Fs for RealFs {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
impl Watcher for RealWatcher {
|
||||
fn add(&self, _: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&self, _: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl Watcher for RealWatcher {
|
||||
fn add(&self, path: &Path) -> Result<()> {
|
||||
use notify::Watcher;
|
||||
|
||||
self.fs_watcher
|
||||
.lock()
|
||||
.watch(path, notify::RecursiveMode::NonRecursive)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&self, path: &Path) -> Result<()> {
|
||||
use notify::Watcher;
|
||||
|
||||
self.fs_watcher.lock().unwatch(path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeFs {
|
||||
// Use an unfair lock to ensure tests are deterministic.
|
||||
@@ -1073,6 +1134,20 @@ impl FakeFsEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
struct FakeWatcher {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Watcher for FakeWatcher {
|
||||
fn add(&self, _: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(&self, _: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[async_trait::async_trait]
|
||||
impl Fs for FakeFs {
|
||||
@@ -1468,20 +1543,26 @@ impl Fs for FakeFs {
|
||||
&self,
|
||||
path: &Path,
|
||||
_: Duration,
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
|
||||
) -> (
|
||||
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
|
||||
Arc<dyn Watcher>,
|
||||
) {
|
||||
self.simulate_random_delay().await;
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
self.state.lock().event_txs.push(tx);
|
||||
let path = path.to_path_buf();
|
||||
let executor = self.executor.clone();
|
||||
Box::pin(futures::StreamExt::filter(rx, move |events| {
|
||||
let result = events.iter().any(|evt_path| evt_path.starts_with(&path));
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
executor.simulate_random_delay().await;
|
||||
result
|
||||
}
|
||||
}))
|
||||
(
|
||||
Box::pin(futures::StreamExt::filter(rx, move |events| {
|
||||
let result = events.iter().any(|evt_path| evt_path.starts_with(&path));
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
executor.simulate_random_delay().await;
|
||||
result
|
||||
}
|
||||
})),
|
||||
Arc::new(FakeWatcher {}),
|
||||
)
|
||||
}
|
||||
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>> {
|
||||
|
||||
@@ -99,7 +99,6 @@ objc = "0.2"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
|
||||
flume = "0.11"
|
||||
#TODO: use these on all platforms
|
||||
blade-graphics.workspace = true
|
||||
blade-macros.workspace = true
|
||||
blade-util.workspace = true
|
||||
|
||||
@@ -29,10 +29,11 @@ use crate::{
|
||||
current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
|
||||
AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, Context,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
|
||||
Keystroke, LayoutId, Menu, MenuItem, PathPromptOptions, Pixels, Platform, PlatformDisplay,
|
||||
Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
|
||||
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, View, ViewContext,
|
||||
Window, WindowAppearance, WindowContext, WindowHandle, WindowId,
|
||||
Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, Point, PromptBuilder, PromptHandle, PromptLevel, Render,
|
||||
RenderablePromptHandle, Reservation, SharedString, SubscriberSet, Subscription, SvgRenderer,
|
||||
Task, TextSystem, View, ViewContext, Window, WindowAppearance, WindowContext, WindowHandle,
|
||||
WindowId,
|
||||
};
|
||||
|
||||
mod async_context;
|
||||
@@ -734,7 +735,11 @@ impl AppContext {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
self.update_window(window, |_, cx| cx.draw()).unwrap();
|
||||
self.update_window(window, |_, cx| {
|
||||
println!("flush_effects. cx.draw()");
|
||||
cx.draw()
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if self.pending_effects.is_empty() {
|
||||
@@ -1167,6 +1172,11 @@ impl AppContext {
|
||||
self.platform.set_menus(menus, &self.keymap.borrow());
|
||||
}
|
||||
|
||||
/// Gets the menu bar for this application.
|
||||
pub fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
self.platform.get_menus()
|
||||
}
|
||||
|
||||
/// Sets the right click menu for the app icon in the dock
|
||||
pub fn set_dock_menu(&mut self, menus: Vec<MenuItem>) {
|
||||
self.platform.set_dock_menu(menus, &self.keymap.borrow());
|
||||
|
||||
@@ -2437,7 +2437,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
struct ScrollHandleState {
|
||||
offset: Rc<RefCell<Point<Pixels>>>,
|
||||
bounds: Bounds<Pixels>,
|
||||
@@ -2449,7 +2449,7 @@ struct ScrollHandleState {
|
||||
/// A handle to the scrollable aspects of an element.
|
||||
/// Used for accessing scroll state, like the current scroll offset,
|
||||
/// and for mutating the scroll state, like scrolling to a specific child.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScrollHandle(Rc<RefCell<ScrollHandleState>>);
|
||||
|
||||
impl Default for ScrollHandle {
|
||||
@@ -2526,6 +2526,14 @@ impl ScrollHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the offset explicitly. The offset is the distance from the top left of the
|
||||
/// parent container to the top left of the first child.
|
||||
/// As you scroll further down the offset becomes more negative.
|
||||
pub fn set_offset(&self, mut position: Point<Pixels>) {
|
||||
let state = self.0.borrow();
|
||||
*state.offset.borrow_mut() = position;
|
||||
}
|
||||
|
||||
/// Get the logical scroll top, based on a child index and a pixel offset.
|
||||
pub fn logical_scroll_top(&self) -> (usize, Pixels) {
|
||||
let ix = self.top_item();
|
||||
|
||||
@@ -80,7 +80,7 @@ pub struct ListScrollEvent {
|
||||
}
|
||||
|
||||
/// The sizing behavior to apply during layout.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ListSizingBehavior {
|
||||
/// The list should calculate its size based on the size of its items.
|
||||
Infer,
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
|
||||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId,
|
||||
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels,
|
||||
Render, ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
|
||||
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId,
|
||||
ListSizingBehavior, Pixels, Render, ScrollHandle, Size, StyleRefinement, Styled, View,
|
||||
ViewContext, WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
@@ -55,6 +56,7 @@ where
|
||||
..Default::default()
|
||||
},
|
||||
scroll_handle: None,
|
||||
sizing_behavior: ListSizingBehavior::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +68,7 @@ pub struct UniformList {
|
||||
Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
|
||||
interactivity: Interactivity,
|
||||
scroll_handle: Option<UniformListScrollHandle>,
|
||||
sizing_behavior: ListSizingBehavior,
|
||||
}
|
||||
|
||||
/// Frame state used by the [UniformList].
|
||||
@@ -120,24 +123,35 @@ impl Element for UniformList {
|
||||
let item_size = self.measure_item(None, cx);
|
||||
let layout_id = self
|
||||
.interactivity
|
||||
.request_layout(global_id, cx, |style, cx| {
|
||||
cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| {
|
||||
let desired_height = item_size.height * max_items;
|
||||
let width = known_dimensions
|
||||
.width
|
||||
.unwrap_or(match available_space.width {
|
||||
AvailableSpace::Definite(x) => x,
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
item_size.width
|
||||
}
|
||||
});
|
||||
|
||||
let height = match available_space.height {
|
||||
AvailableSpace::Definite(height) => desired_height.min(height),
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
|
||||
};
|
||||
size(width, height)
|
||||
})
|
||||
.request_layout(global_id, cx, |style, cx| match self.sizing_behavior {
|
||||
ListSizingBehavior::Infer => {
|
||||
cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||
cx.request_measured_layout(
|
||||
style,
|
||||
move |known_dimensions, available_space, _cx| {
|
||||
let desired_height = item_size.height * max_items;
|
||||
let width = known_dimensions.width.unwrap_or(match available_space
|
||||
.width
|
||||
{
|
||||
AvailableSpace::Definite(x) => x,
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
item_size.width
|
||||
}
|
||||
});
|
||||
let height = match available_space.height {
|
||||
AvailableSpace::Definite(height) => desired_height.min(height),
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
desired_height
|
||||
}
|
||||
};
|
||||
size(width, height)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
ListSizingBehavior::Auto => cx.with_text_style(style.text_style().cloned(), |cx| {
|
||||
cx.request_layout(style, None)
|
||||
}),
|
||||
});
|
||||
|
||||
(
|
||||
@@ -280,6 +294,12 @@ impl UniformList {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the sizing behavior, similar to the `List` element.
|
||||
pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
|
||||
self.sizing_behavior = behavior;
|
||||
self
|
||||
}
|
||||
|
||||
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
|
||||
if self.item_count == 0 {
|
||||
return Size::default();
|
||||
|
||||
@@ -712,7 +712,7 @@ impl Size<Length> {
|
||||
/// assert_eq!(bounds.origin, origin);
|
||||
/// assert_eq!(bounds.size, size);
|
||||
/// ```
|
||||
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
|
||||
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Hash)]
|
||||
#[refineable(Debug)]
|
||||
#[repr(C)]
|
||||
pub struct Bounds<T: Clone + Default + Debug> {
|
||||
|
||||
@@ -304,6 +304,14 @@ impl KeyBindingContextPredicate {
|
||||
source,
|
||||
))
|
||||
}
|
||||
_ if is_vim_operator_char(next) => {
|
||||
let (operator, rest) = source.split_at(1);
|
||||
source = skip_whitespace(rest);
|
||||
Ok((
|
||||
KeyBindingContextPredicate::Identifier(operator.to_string().into()),
|
||||
source,
|
||||
))
|
||||
}
|
||||
_ => Err(anyhow!("unexpected character {next:?}")),
|
||||
}
|
||||
}
|
||||
@@ -347,6 +355,10 @@ fn is_identifier_char(c: char) -> bool {
|
||||
c.is_alphanumeric() || c == '_' || c == '-'
|
||||
}
|
||||
|
||||
fn is_vim_operator_char(c: char) -> bool {
|
||||
c == '>' || c == '<'
|
||||
}
|
||||
|
||||
fn skip_whitespace(source: &str) -> &str {
|
||||
let len = source
|
||||
.find(|c: char| !c.is_whitespace())
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// todo(linux): remove
|
||||
#![cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
// todo(windows): remove
|
||||
#![cfg_attr(windows, allow(dead_code))]
|
||||
|
||||
@@ -135,6 +133,10 @@ pub(crate) trait Platform: 'static {
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
|
||||
fn add_recent_document(&self, _path: &Path) {}
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
@@ -203,7 +205,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn content_size(&self) -> Size<Pixels>;
|
||||
fn scale_factor(&self) -> f32;
|
||||
fn appearance(&self) -> WindowAppearance;
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay>;
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
|
||||
fn mouse_position(&self) -> Point<Pixels>;
|
||||
fn modifiers(&self) -> Modifiers;
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
|
||||
@@ -413,6 +415,7 @@ impl PlatformInputHandler {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
|
||||
self.cx
|
||||
.update(|cx| self.handler.text_for_range(range_utf16, cx))
|
||||
@@ -573,13 +576,17 @@ pub(crate) struct WindowParams {
|
||||
pub titlebar: Option<TitlebarOptions>,
|
||||
|
||||
/// The kind of window to create
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub kind: WindowKind,
|
||||
|
||||
/// Whether the window should be movable by the user
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub is_movable: bool,
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub focus: bool,
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub show: bool,
|
||||
|
||||
pub display_id: Option<DisplayId>,
|
||||
@@ -797,10 +804,6 @@ pub enum CursorStyle {
|
||||
/// corresponds to the CSS curosr value `row-resize`
|
||||
ResizeRow,
|
||||
|
||||
/// A cursor indicating that something will disappear if moved here
|
||||
/// Does not correspond to a CSS cursor value
|
||||
DisappearingItem,
|
||||
|
||||
/// A text input cursor for vertical layout
|
||||
/// corresponds to the CSS cursor value `vertical-text`
|
||||
IBeamCursorForVerticalLayout,
|
||||
@@ -865,6 +868,7 @@ impl ClipboardItem {
|
||||
.and_then(|m| serde_json::from_str(m).ok())
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub(crate) fn text_hash(text: &str) -> u64 {
|
||||
let mut hasher = SeaHasher::new();
|
||||
text.hash(&mut hasher);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{Action, AppContext, Platform};
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -10,6 +12,16 @@ pub struct Menu<'a> {
|
||||
pub items: Vec<MenuItem<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Menu<'a> {
|
||||
/// Create an OwnedMenu from this Menu
|
||||
pub fn owned(self) -> OwnedMenu {
|
||||
OwnedMenu {
|
||||
name: self.name.to_string().into(),
|
||||
items: self.items.into_iter().map(|item| item.owned()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The different kinds of items that can be in a menu
|
||||
pub enum MenuItem<'a> {
|
||||
/// A separator between items
|
||||
@@ -60,6 +72,73 @@ impl<'a> MenuItem<'a> {
|
||||
os_action: Some(os_action),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an OwnedMenuItem from this MenuItem
|
||||
pub fn owned(self) -> OwnedMenuItem {
|
||||
match self {
|
||||
MenuItem::Separator => OwnedMenuItem::Separator,
|
||||
MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()),
|
||||
MenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
} => OwnedMenuItem::Action {
|
||||
name: name.into(),
|
||||
action,
|
||||
os_action,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A menu of the application, either a main menu or a submenu
|
||||
#[derive(Clone)]
|
||||
pub struct OwnedMenu {
|
||||
/// The name of the menu
|
||||
pub name: Cow<'static, str>,
|
||||
|
||||
/// The items in the menu
|
||||
pub items: Vec<OwnedMenuItem>,
|
||||
}
|
||||
|
||||
/// The different kinds of items that can be in a menu
|
||||
pub enum OwnedMenuItem {
|
||||
/// A separator between items
|
||||
Separator,
|
||||
|
||||
/// A submenu
|
||||
Submenu(OwnedMenu),
|
||||
|
||||
/// An action that can be performed
|
||||
Action {
|
||||
/// The name of this menu item
|
||||
name: String,
|
||||
|
||||
/// the action to perform when this menu item is selected
|
||||
action: Box<dyn Action>,
|
||||
|
||||
/// The OS Action that corresponds to this action, if any
|
||||
/// See [`OsAction`] for more information
|
||||
os_action: Option<OsAction>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Clone for OwnedMenuItem {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
OwnedMenuItem::Separator => OwnedMenuItem::Separator,
|
||||
OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()),
|
||||
OwnedMenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
} => OwnedMenuItem::Action {
|
||||
name: name.clone(),
|
||||
action: action.boxed_clone(),
|
||||
os_action: *os_action,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: As part of the global selections refactor, these should
|
||||
|
||||
@@ -20,7 +20,9 @@ use std::{mem, sync::Arc};
|
||||
|
||||
const MAX_FRAME_TIME_MS: u32 = 1000;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub type Context = ();
|
||||
#[cfg(target_os = "macos")]
|
||||
pub type Renderer = BladeRenderer;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -294,7 +294,7 @@ impl CosmicTextSystemState {
|
||||
.0,
|
||||
)
|
||||
.clone()
|
||||
.unwrap();
|
||||
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
|
||||
Ok(Bounds {
|
||||
origin: point(image.placement.left.into(), (-image.placement.top).into()),
|
||||
size: size(image.placement.width.into(), image.placement.height.into()),
|
||||
@@ -314,7 +314,7 @@ impl CosmicTextSystemState {
|
||||
let bitmap_size = glyph_bounds.size;
|
||||
let font = &self.loaded_fonts_store[params.font_id.0];
|
||||
let font_system = &mut self.font_system;
|
||||
let image = self
|
||||
let mut image = self
|
||||
.swash_cache
|
||||
.get_image(
|
||||
font_system,
|
||||
@@ -328,7 +328,14 @@ impl CosmicTextSystemState {
|
||||
.0,
|
||||
)
|
||||
.clone()
|
||||
.unwrap();
|
||||
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
|
||||
|
||||
if params.is_emoji {
|
||||
// Convert from RGBA to BGRA.
|
||||
for pixel in image.data.chunks_exact_mut(4) {
|
||||
pixel.swap(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((bitmap_size, image.data))
|
||||
}
|
||||
@@ -394,13 +401,20 @@ impl CosmicTextSystemState {
|
||||
for glyph in &layout.glyphs {
|
||||
let font_id = glyph.font_id;
|
||||
let font_id = self.font_id_for_cosmic_id(font_id);
|
||||
let is_emoji = self.is_emoji(font_id);
|
||||
let mut glyphs = SmallVec::new();
|
||||
|
||||
// HACK: Prevent crash caused by variation selectors.
|
||||
if glyph.glyph_id == 3 && is_emoji {
|
||||
continue;
|
||||
}
|
||||
|
||||
// todo(linux) this is definitely wrong, each glyph in glyphs from cosmic-text is a cluster with one glyph, ShapedRun takes a run of glyphs with the same font and direction
|
||||
glyphs.push(ShapedGlyph {
|
||||
id: GlyphId(glyph.glyph_id as u32),
|
||||
position: point((glyph.x).into(), glyph.y.into()),
|
||||
index: glyph.start,
|
||||
is_emoji: self.is_emoji(font_id),
|
||||
is_emoji,
|
||||
});
|
||||
|
||||
runs.push(crate::ShapedRun { font_id, glyphs });
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// todo(linux): remove
|
||||
#![allow(unused)]
|
||||
|
||||
mod dispatcher;
|
||||
mod headless;
|
||||
mod platform;
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
// todo(linux): remove
|
||||
#![allow(unused_variables)]
|
||||
|
||||
use crate::{PlatformDispatcher, TaskLabel};
|
||||
use async_task::Runnable;
|
||||
use calloop::{
|
||||
@@ -63,7 +57,7 @@ impl LinuxDispatcher {
|
||||
timer_handle
|
||||
.insert_source(
|
||||
calloop::timer::Timer::from_duration(timer.duration),
|
||||
move |e, _, _| {
|
||||
move |_, _, _| {
|
||||
if let Some(runnable) = runnable.take() {
|
||||
runnable.run();
|
||||
}
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use calloop::{EventLoop, LoopHandle};
|
||||
use collections::HashMap;
|
||||
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams,
|
||||
};
|
||||
|
||||
use calloop::{
|
||||
generic::{FdWrapper, Generic},
|
||||
RegistrationToken,
|
||||
};
|
||||
use crate::{AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, WindowParams};
|
||||
|
||||
pub struct HeadlessClientState {
|
||||
pub(crate) loop_handle: LoopHandle<'static, HeadlessClient>,
|
||||
pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, HeadlessClient>>,
|
||||
pub(crate) common: LinuxCommon,
|
||||
}
|
||||
@@ -37,15 +26,17 @@ impl HeadlessClient {
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
handle.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
handle
|
||||
.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
HeadlessClient(Rc::new(RefCell::new(HeadlessClientState {
|
||||
event_loop: Some(event_loop),
|
||||
loop_handle: handle,
|
||||
_loop_handle: handle,
|
||||
common,
|
||||
})))
|
||||
}
|
||||
@@ -64,7 +55,7 @@ impl LinuxClient for HeadlessClient {
|
||||
None
|
||||
}
|
||||
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
fn display(&self, _id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -72,10 +63,14 @@ impl LinuxClient for HeadlessClient {
|
||||
return Err(anyhow::anyhow!("neither DISPLAY, nor WAYLAND_DISPLAY found. You can still run zed for remote development with --dev-server-token."));
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
None
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
_handle: AnyWindowHandle,
|
||||
params: WindowParams,
|
||||
_params: WindowParams,
|
||||
) -> Box<dyn PlatformWindow> {
|
||||
unimplemented!()
|
||||
}
|
||||
@@ -84,9 +79,9 @@ impl LinuxClient for HeadlessClient {
|
||||
|
||||
fn open_uri(&self, _uri: &str) {}
|
||||
|
||||
fn write_to_primary(&self, item: crate::ClipboardItem) {}
|
||||
fn write_to_primary(&self, _item: crate::ClipboardItem) {}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {}
|
||||
fn write_to_clipboard(&self, _item: crate::ClipboardItem) {}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
None
|
||||
|
||||
@@ -38,9 +38,9 @@ use crate::platform::linux::xdg_desktop_portal::{should_auto_hide_scrollbars, wi
|
||||
use crate::{
|
||||
px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CosmicTextSystem, CursorStyle,
|
||||
DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers,
|
||||
PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem,
|
||||
PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task, WindowAppearance,
|
||||
WindowOptions, WindowParams,
|
||||
OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler,
|
||||
PlatformTextSystem, PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task,
|
||||
WindowAppearance, WindowOptions, WindowParams,
|
||||
};
|
||||
|
||||
use super::x11::X11Client;
|
||||
@@ -72,6 +72,7 @@ pub trait LinuxClient {
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
fn active_window(&self) -> Option<AnyWindowHandle>;
|
||||
fn run(&self);
|
||||
}
|
||||
|
||||
@@ -93,6 +94,7 @@ pub(crate) struct LinuxCommon {
|
||||
pub(crate) auto_hide_scrollbars: bool,
|
||||
pub(crate) callbacks: PlatformHandlers,
|
||||
pub(crate) signal: LoopSignal,
|
||||
pub(crate) menus: Vec<OwnedMenu>,
|
||||
}
|
||||
|
||||
impl LinuxCommon {
|
||||
@@ -118,6 +120,7 @@ impl LinuxCommon {
|
||||
auto_hide_scrollbars,
|
||||
callbacks,
|
||||
signal,
|
||||
menus: Vec::new(),
|
||||
};
|
||||
|
||||
(common, main_receiver)
|
||||
@@ -210,18 +213,21 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
}
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn activate(&self, ignoring_other_apps: bool) {}
|
||||
|
||||
// todo(linux)
|
||||
fn hide(&self) {}
|
||||
|
||||
fn hide_other_apps(&self) {
|
||||
log::warn!("hide_other_apps is not implemented on Linux, ignoring the call")
|
||||
fn activate(&self, ignoring_other_apps: bool) {
|
||||
log::info!("activate is not implemented on Linux, ignoring the call")
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn unhide_other_apps(&self) {}
|
||||
fn hide(&self) {
|
||||
log::info!("hide is not implemented on Linux, ignoring the call")
|
||||
}
|
||||
|
||||
fn hide_other_apps(&self) {
|
||||
log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
|
||||
}
|
||||
|
||||
fn unhide_other_apps(&self) {
|
||||
log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
|
||||
}
|
||||
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
self.primary_display()
|
||||
@@ -231,9 +237,8 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
self.displays()
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
None
|
||||
self.active_window()
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
@@ -387,15 +392,22 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
Ok(exe_path)
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
|
||||
fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
|
||||
self.with_common(|common| {
|
||||
common.menus = menus.into_iter().map(|menu| menu.owned()).collect();
|
||||
})
|
||||
}
|
||||
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
self.with_common(|common| Some(common.menus.clone()))
|
||||
}
|
||||
|
||||
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {}
|
||||
|
||||
fn local_timezone(&self) -> UtcOffset {
|
||||
UtcOffset::UTC
|
||||
}
|
||||
|
||||
//todo(linux)
|
||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
|
||||
Err(anyhow::Error::msg(
|
||||
"Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
|
||||
@@ -549,7 +561,6 @@ impl CursorStyle {
|
||||
CursorStyle::ResizeUpDown => Shape::NsResize,
|
||||
CursorStyle::ResizeColumn => Shape::ColResize,
|
||||
CursorStyle::ResizeRow => Shape::RowResize,
|
||||
CursorStyle::DisappearingItem => Shape::Grabbing, // todo(linux) - couldn't find equivalent icon in linux
|
||||
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
|
||||
CursorStyle::OperationNotAllowed => Shape::NotAllowed,
|
||||
CursorStyle::DragLink => Shape::Alias,
|
||||
@@ -577,7 +588,6 @@ impl CursorStyle {
|
||||
CursorStyle::ResizeUpDown => "ns-resize",
|
||||
CursorStyle::ResizeColumn => "col-resize",
|
||||
CursorStyle::ResizeRow => "row-resize",
|
||||
CursorStyle::DisappearingItem => "grabbing", // todo(linux) - couldn't find equivalent icon in linux
|
||||
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
|
||||
CursorStyle::OperationNotAllowed => "not-allowed",
|
||||
CursorStyle::DragLink => "alias",
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use core::hash;
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::ffi::OsString;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::hash::Hash;
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use async_task::Runnable;
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
use calloop::{EventLoop, LoopHandle};
|
||||
use calloop_wayland_source::WaylandSource;
|
||||
@@ -16,7 +13,7 @@ use collections::HashMap;
|
||||
use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary};
|
||||
use copypasta::ClipboardProvider;
|
||||
use filedescriptor::Pipe;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
use util::ResultExt;
|
||||
use wayland_backend::client::ObjectId;
|
||||
@@ -26,9 +23,8 @@ use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContent
|
||||
use wayland_client::protocol::wl_callback::{self, WlCallback};
|
||||
use wayland_client::protocol::wl_data_device_manager::DndAction;
|
||||
use wayland_client::protocol::wl_pointer::AxisSource;
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::protocol::{
|
||||
wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region,
|
||||
wl_data_device, wl_data_device_manager, wl_data_offer, wl_output, wl_region,
|
||||
};
|
||||
use wayland_client::{
|
||||
delegate_noop,
|
||||
@@ -38,7 +34,6 @@ use wayland_client::{
|
||||
},
|
||||
Connection, Dispatch, Proxy, QueueHandle,
|
||||
};
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::{
|
||||
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
|
||||
};
|
||||
@@ -62,7 +57,8 @@ use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
|
||||
|
||||
use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
|
||||
use super::window::{ImeInput, WaylandWindowState, WaylandWindowStatePtr};
|
||||
use super::display::WaylandDisplay;
|
||||
use super::window::{ImeInput, WaylandWindowStatePtr};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::wayland::cursor::Cursor;
|
||||
use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
|
||||
@@ -71,7 +67,7 @@ use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSour
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::PlatformWindow;
|
||||
use crate::{
|
||||
point, px, Bounds, FileDropEvent, ForegroundExecutor, MouseExitEvent, WindowAppearance,
|
||||
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
|
||||
SCROLL_LINES,
|
||||
};
|
||||
use crate::{
|
||||
@@ -143,11 +139,42 @@ impl Globals {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct InProgressOutput {
|
||||
name: Option<String>,
|
||||
scale: Option<i32>,
|
||||
position: Option<Point<DevicePixels>>,
|
||||
size: Option<Size<DevicePixels>>,
|
||||
}
|
||||
|
||||
impl InProgressOutput {
|
||||
fn complete(&self) -> Option<Output> {
|
||||
if let Some((position, size)) = self.position.zip(self.size) {
|
||||
let scale = self.scale.unwrap_or(1);
|
||||
Some(Output {
|
||||
name: self.name.clone(),
|
||||
scale,
|
||||
bounds: Bounds::new(position, size),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Output {
|
||||
pub name: Option<String>,
|
||||
pub scale: i32,
|
||||
pub bounds: Bounds<DevicePixels>,
|
||||
}
|
||||
|
||||
pub(crate) struct WaylandClientState {
|
||||
serial_tracker: SerialTracker,
|
||||
globals: Globals,
|
||||
wl_seat: wl_seat::WlSeat, // todo(linux): multi-seat support
|
||||
wl_seat: wl_seat::WlSeat, // TODO: Multi seat support
|
||||
wl_pointer: Option<wl_pointer::WlPointer>,
|
||||
wl_keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||
cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
|
||||
data_device: Option<wl_data_device::WlDataDevice>,
|
||||
text_input: Option<zwp_text_input_v3::ZwpTextInputV3>,
|
||||
@@ -156,15 +183,16 @@ pub(crate) struct WaylandClientState {
|
||||
// Surface to Window mapping
|
||||
windows: HashMap<ObjectId, WaylandWindowStatePtr>,
|
||||
// Output to scale mapping
|
||||
output_scales: HashMap<ObjectId, i32>,
|
||||
outputs: HashMap<ObjectId, Output>,
|
||||
in_progress_outputs: HashMap<ObjectId, InProgressOutput>,
|
||||
keymap_state: Option<xkb::State>,
|
||||
compose_state: Option<xkb::compose::State>,
|
||||
drag: DragState,
|
||||
click: ClickState,
|
||||
repeat: KeyRepeat,
|
||||
modifiers: Modifiers,
|
||||
pub modifiers: Modifiers,
|
||||
axis_source: AxisSource,
|
||||
mouse_location: Option<Point<Pixels>>,
|
||||
pub mouse_location: Option<Point<Pixels>>,
|
||||
continuous_scroll_delta: Option<Point<Pixels>>,
|
||||
discrete_scroll_delta: Option<Point<f32>>,
|
||||
vertical_modifier: f32,
|
||||
@@ -210,7 +238,7 @@ pub(crate) struct KeyRepeat {
|
||||
pub struct WaylandClientStatePtr(Weak<RefCell<WaylandClientState>>);
|
||||
|
||||
impl WaylandClientStatePtr {
|
||||
fn get_client(&self) -> Rc<RefCell<WaylandClientState>> {
|
||||
pub fn get_client(&self) -> Rc<RefCell<WaylandClientState>> {
|
||||
self.0
|
||||
.upgrade()
|
||||
.expect("The pointer should always be valid when dispatching in wayland")
|
||||
@@ -302,7 +330,6 @@ impl Drop for WaylandClient {
|
||||
}
|
||||
|
||||
const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3;
|
||||
const WL_OUTPUT_VERSION: u32 = 2;
|
||||
|
||||
fn wl_seat_version(version: u32) -> u32 {
|
||||
// We rely on the wl_pointer.frame event
|
||||
@@ -319,6 +346,20 @@ fn wl_seat_version(version: u32) -> u32 {
|
||||
version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION)
|
||||
}
|
||||
|
||||
fn wl_output_version(version: u32) -> u32 {
|
||||
const WL_OUTPUT_MIN_VERSION: u32 = 2;
|
||||
const WL_OUTPUT_MAX_VERSION: u32 = 4;
|
||||
|
||||
if version < WL_OUTPUT_MIN_VERSION {
|
||||
panic!(
|
||||
"wl_output below required version: {} < {}",
|
||||
version, WL_OUTPUT_MIN_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
version.clamp(WL_OUTPUT_MIN_VERSION, WL_OUTPUT_MAX_VERSION)
|
||||
}
|
||||
|
||||
impl WaylandClient {
|
||||
pub(crate) fn new() -> Self {
|
||||
let conn = Connection::connect_to_env().unwrap();
|
||||
@@ -328,7 +369,7 @@ impl WaylandClient {
|
||||
let qh = event_queue.handle();
|
||||
|
||||
let mut seat: Option<wl_seat::WlSeat> = None;
|
||||
let mut outputs = HashMap::default();
|
||||
let mut in_progress_outputs = HashMap::default();
|
||||
globals.contents().with_list(|list| {
|
||||
for global in list {
|
||||
match &global.interface[..] {
|
||||
@@ -343,11 +384,11 @@ impl WaylandClient {
|
||||
"wl_output" => {
|
||||
let output = globals.registry().bind::<wl_output::WlOutput, _, _>(
|
||||
global.name,
|
||||
WL_OUTPUT_VERSION,
|
||||
wl_output_version(global.version),
|
||||
&qh,
|
||||
(),
|
||||
);
|
||||
outputs.insert(output.id(), 1);
|
||||
in_progress_outputs.insert(output.id(), InProgressOutput::default());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -361,11 +402,13 @@ impl WaylandClient {
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let handle = event_loop.handle();
|
||||
handle.insert_source(main_receiver, |event, _, _: &mut WaylandClientStatePtr| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
handle
|
||||
.insert_source(main_receiver, |event, _, _: &mut WaylandClientStatePtr| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let seat = seat.unwrap();
|
||||
let globals = Globals::new(
|
||||
@@ -384,33 +427,37 @@ impl WaylandClient {
|
||||
|
||||
let cursor = Cursor::new(&conn, &globals, 24);
|
||||
|
||||
handle.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
if let Some(client) = client.0.upgrade() {
|
||||
let mut client = client.borrow_mut();
|
||||
handle
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
if let Some(client) = client.0.upgrade() {
|
||||
let mut client = client.borrow_mut();
|
||||
|
||||
client.common.appearance = appearance;
|
||||
client.common.appearance = appearance;
|
||||
|
||||
for (_, window) in &mut client.windows {
|
||||
window.set_appearance(appearance);
|
||||
for (_, window) in &mut client.windows {
|
||||
window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut state = Rc::new(RefCell::new(WaylandClientState {
|
||||
serial_tracker: SerialTracker::new(),
|
||||
globals,
|
||||
wl_seat: seat,
|
||||
wl_pointer: None,
|
||||
wl_keyboard: None,
|
||||
cursor_shape_device: None,
|
||||
data_device,
|
||||
text_input: None,
|
||||
pre_edit_text: None,
|
||||
composing: false,
|
||||
output_scales: outputs,
|
||||
outputs: HashMap::default(),
|
||||
in_progress_outputs,
|
||||
windows: HashMap::default(),
|
||||
common,
|
||||
keymap_state: None,
|
||||
@@ -459,7 +506,9 @@ impl WaylandClient {
|
||||
pending_open_uri: None,
|
||||
}));
|
||||
|
||||
WaylandSource::new(conn, event_queue).insert(handle);
|
||||
WaylandSource::new(conn, event_queue)
|
||||
.insert(handle)
|
||||
.unwrap();
|
||||
|
||||
Self(state)
|
||||
}
|
||||
@@ -467,11 +516,34 @@ impl WaylandClient {
|
||||
|
||||
impl LinuxClient for WaylandClient {
|
||||
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
Vec::new()
|
||||
self.0
|
||||
.borrow()
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|(id, output)| {
|
||||
Rc::new(WaylandDisplay {
|
||||
id: id.clone(),
|
||||
name: output.name.clone(),
|
||||
bounds: output.bounds,
|
||||
}) as Rc<dyn PlatformDisplay>
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
unimplemented!()
|
||||
self.0
|
||||
.borrow()
|
||||
.outputs
|
||||
.iter()
|
||||
.find_map(|(object_id, output)| {
|
||||
(object_id.protocol_id() == id.0).then(|| {
|
||||
Rc::new(WaylandDisplay {
|
||||
id: object_id.clone(),
|
||||
name: output.name.clone(),
|
||||
bounds: output.bounds,
|
||||
}) as Rc<dyn PlatformDisplay>
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
@@ -486,6 +558,7 @@ impl LinuxClient for WaylandClient {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let (window, surface_id) = WaylandWindow::new(
|
||||
handle,
|
||||
state.globals.clone(),
|
||||
WaylandClientStatePtr(Rc::downgrade(&self.0)),
|
||||
params,
|
||||
@@ -566,7 +639,8 @@ impl LinuxClient for WaylandClient {
|
||||
.primary
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_contents(item.text);
|
||||
.set_contents(item.text)
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
@@ -575,7 +649,8 @@ impl LinuxClient for WaylandClient {
|
||||
.clipboard
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_contents(item.text);
|
||||
.set_contents(item.text)
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
@@ -605,6 +680,14 @@ impl LinuxClient for WaylandClient {
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.keyboard_focused_window
|
||||
.as_ref()
|
||||
.map(|window| window.handle())
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStatePtr {
|
||||
@@ -626,18 +709,37 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
|
||||
version,
|
||||
} => match &interface[..] {
|
||||
"wl_seat" => {
|
||||
state.wl_pointer = None;
|
||||
registry.bind::<wl_seat::WlSeat, _, _>(name, wl_seat_version(version), qh, ());
|
||||
if let Some(wl_pointer) = state.wl_pointer.take() {
|
||||
wl_pointer.release();
|
||||
}
|
||||
if let Some(wl_keyboard) = state.wl_keyboard.take() {
|
||||
wl_keyboard.release();
|
||||
}
|
||||
state.wl_seat.release();
|
||||
state.wl_seat = registry.bind::<wl_seat::WlSeat, _, _>(
|
||||
name,
|
||||
wl_seat_version(version),
|
||||
qh,
|
||||
(),
|
||||
);
|
||||
}
|
||||
"wl_output" => {
|
||||
let output =
|
||||
registry.bind::<wl_output::WlOutput, _, _>(name, WL_OUTPUT_VERSION, qh, ());
|
||||
let output = registry.bind::<wl_output::WlOutput, _, _>(
|
||||
name,
|
||||
wl_output_version(version),
|
||||
qh,
|
||||
(),
|
||||
);
|
||||
|
||||
state.output_scales.insert(output.id(), 1);
|
||||
state
|
||||
.in_progress_outputs
|
||||
.insert(output.id(), InProgressOutput::default());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
wl_registry::Event::GlobalRemove { name: _ } => {}
|
||||
wl_registry::Event::GlobalRemove { name: _ } => {
|
||||
// TODO: handle global removal
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -667,7 +769,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
||||
event: wl_callback::Event,
|
||||
surface_id: &ObjectId,
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = state.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
@@ -677,7 +779,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
||||
drop(state);
|
||||
|
||||
match event {
|
||||
wl_callback::Event::Done { callback_data } => {
|
||||
wl_callback::Event::Done { .. } => {
|
||||
window.frame(true);
|
||||
}
|
||||
_ => {}
|
||||
@@ -707,10 +809,10 @@ impl Dispatch<wl_surface::WlSurface, ()> for WaylandClientStatePtr {
|
||||
let Some(window) = get_window(&mut state, &surface.id()) else {
|
||||
return;
|
||||
};
|
||||
let scales = state.output_scales.clone();
|
||||
let outputs = state.outputs.clone();
|
||||
drop(state);
|
||||
|
||||
window.handle_surface_event(event, scales);
|
||||
window.handle_surface_event(event, outputs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,13 +828,28 @@ impl Dispatch<wl_output::WlOutput, ()> for WaylandClientStatePtr {
|
||||
let mut client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
let Some(mut output_scale) = state.output_scales.get_mut(&output.id()) else {
|
||||
let Some(mut in_progress_output) = state.in_progress_outputs.get_mut(&output.id()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match event {
|
||||
wl_output::Event::Name { name } => {
|
||||
in_progress_output.name = Some(name);
|
||||
}
|
||||
wl_output::Event::Scale { factor } => {
|
||||
*output_scale = factor;
|
||||
in_progress_output.scale = Some(factor);
|
||||
}
|
||||
wl_output::Event::Geometry { x, y, .. } => {
|
||||
in_progress_output.position = Some(point(DevicePixels(x), DevicePixels(y)))
|
||||
}
|
||||
wl_output::Event::Mode { width, height, .. } => {
|
||||
in_progress_output.size = Some(size(DevicePixels(width), DevicePixels(height)))
|
||||
}
|
||||
wl_output::Event::Done => {
|
||||
if let Some(complete) = in_progress_output.complete() {
|
||||
state.outputs.insert(output.id(), complete);
|
||||
}
|
||||
state.in_progress_outputs.remove(&output.id());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -742,7 +859,7 @@ impl Dispatch<wl_output::WlOutput, ()> for WaylandClientStatePtr {
|
||||
impl Dispatch<xdg_surface::XdgSurface, ObjectId> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
xdg_surface: &xdg_surface::XdgSurface,
|
||||
_: &xdg_surface::XdgSurface,
|
||||
event: xdg_surface::Event,
|
||||
surface_id: &ObjectId,
|
||||
_: &Connection,
|
||||
@@ -761,7 +878,7 @@ impl Dispatch<xdg_surface::XdgSurface, ObjectId> for WaylandClientStatePtr {
|
||||
impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
xdg_toplevel: &xdg_toplevel::XdgToplevel,
|
||||
_: &xdg_toplevel::XdgToplevel,
|
||||
event: <xdg_toplevel::XdgToplevel as Proxy>::Event,
|
||||
surface_id: &ObjectId,
|
||||
_: &Connection,
|
||||
@@ -824,8 +941,8 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
state: &mut Self,
|
||||
seat: &wl_seat::WlSeat,
|
||||
event: wl_seat::Event,
|
||||
data: &(),
|
||||
conn: &Connection,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
) {
|
||||
if let wl_seat::Event::Capabilities {
|
||||
@@ -835,12 +952,19 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
let client = state.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
if capabilities.contains(wl_seat::Capability::Keyboard) {
|
||||
seat.get_keyboard(qh, ());
|
||||
let keyboard = seat.get_keyboard(qh, ());
|
||||
|
||||
state.text_input = state
|
||||
.globals
|
||||
.text_input_manager
|
||||
.as_ref()
|
||||
.map(|text_input_manager| text_input_manager.get_text_input(&seat, qh, ()));
|
||||
|
||||
if let Some(wl_keyboard) = &state.wl_keyboard {
|
||||
wl_keyboard.release();
|
||||
}
|
||||
|
||||
state.wl_keyboard = Some(keyboard);
|
||||
}
|
||||
if capabilities.contains(wl_seat::Capability::Pointer) {
|
||||
let pointer = seat.get_pointer(qh, ());
|
||||
@@ -849,6 +973,11 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
.cursor_shape_manager
|
||||
.as_ref()
|
||||
.map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ()));
|
||||
|
||||
if let Some(wl_pointer) = &state.wl_pointer {
|
||||
wl_pointer.release();
|
||||
}
|
||||
|
||||
state.wl_pointer = Some(pointer);
|
||||
}
|
||||
}
|
||||
@@ -858,11 +987,11 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
|
||||
impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
keyboard: &wl_keyboard::WlKeyboard,
|
||||
_: &wl_keyboard::WlKeyboard,
|
||||
event: wl_keyboard::Event,
|
||||
data: &(),
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let mut client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
@@ -1018,8 +1147,8 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
state.compose_state = Some(compose);
|
||||
}
|
||||
let input = PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke: keystroke,
|
||||
is_held: false, // todo(linux)
|
||||
keystroke: keystroke.clone(),
|
||||
is_held: false,
|
||||
});
|
||||
|
||||
state.repeat.current_id += 1;
|
||||
@@ -1030,8 +1159,11 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(state.repeat.delay), {
|
||||
let input = input.clone();
|
||||
move |event, _metadata, this| {
|
||||
let input = PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: true,
|
||||
});
|
||||
move |_event, _metadata, this| {
|
||||
let mut client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let is_repeating = id == state.repeat.current_id
|
||||
@@ -1080,18 +1212,18 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
|
||||
this: &mut Self,
|
||||
text_input: &zwp_text_input_v3::ZwpTextInputV3,
|
||||
event: <zwp_text_input_v3::ZwpTextInputV3 as Proxy>::Event,
|
||||
data: &(),
|
||||
conn: &Connection,
|
||||
qhandle: &QueueHandle<Self>,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
match event {
|
||||
zwp_text_input_v3::Event::Enter { surface } => {
|
||||
zwp_text_input_v3::Event::Enter { .. } => {
|
||||
drop(state);
|
||||
this.enable_ime();
|
||||
}
|
||||
zwp_text_input_v3::Event::Leave { surface } => {
|
||||
zwp_text_input_v3::Event::Leave { .. } => {
|
||||
drop(state);
|
||||
this.disable_ime();
|
||||
}
|
||||
@@ -1119,11 +1251,7 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
}
|
||||
zwp_text_input_v3::Event::PreeditString {
|
||||
text,
|
||||
cursor_begin,
|
||||
cursor_end,
|
||||
} => {
|
||||
zwp_text_input_v3::Event::PreeditString { text, .. } => {
|
||||
state.composing = true;
|
||||
state.pre_edit_text = text;
|
||||
}
|
||||
@@ -1183,9 +1311,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
this: &mut Self,
|
||||
wl_pointer: &wl_pointer::WlPointer,
|
||||
event: wl_pointer::Event,
|
||||
data: &(),
|
||||
conn: &Connection,
|
||||
qh: &QueueHandle<Self>,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let mut client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
@@ -1220,7 +1348,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
window.set_focused(true);
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Leave { surface, .. } => {
|
||||
wl_pointer::Event::Leave { .. } => {
|
||||
if let Some(focused_window) = state.mouse_focused_window.clone() {
|
||||
let input = PlatformInput::MouseExited(MouseExitEvent {
|
||||
position: state.mouse_location.unwrap(),
|
||||
@@ -1237,7 +1365,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Motion {
|
||||
time,
|
||||
surface_x,
|
||||
surface_y,
|
||||
..
|
||||
@@ -1280,7 +1407,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
wl_pointer::ButtonState::Pressed => {
|
||||
if let Some(window) = state.keyboard_focused_window.clone() {
|
||||
if state.composing && state.text_input.is_some() {
|
||||
let text_input = state.text_input.as_ref().unwrap();
|
||||
drop(state);
|
||||
// text_input_v3 don't have something like a reset function
|
||||
this.disable_ime();
|
||||
@@ -1351,7 +1477,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
state.axis_source = axis_source;
|
||||
}
|
||||
wl_pointer::Event::Axis {
|
||||
time,
|
||||
axis: WEnum::Value(axis),
|
||||
value,
|
||||
..
|
||||
@@ -1364,13 +1489,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
|
||||
_ => 1.0,
|
||||
};
|
||||
let supports_relative_direction =
|
||||
wl_pointer.version() >= wl_pointer::EVT_AXIS_RELATIVE_DIRECTION_SINCE;
|
||||
state.scroll_event_received = true;
|
||||
let scroll_delta = state
|
||||
.continuous_scroll_delta
|
||||
.get_or_insert(point(px(0.0), px(0.0)));
|
||||
// TODO: Make nice feeling kinetic scrolling that integrates with the platform's scroll settings
|
||||
let modifier = 3.0;
|
||||
match axis {
|
||||
wl_pointer::Axis::VerticalScroll => {
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
use std::fmt::Debug;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use uuid::Uuid;
|
||||
use wayland_backend::client::ObjectId;
|
||||
|
||||
use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay, Size};
|
||||
use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct WaylandDisplay {}
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct WaylandDisplay {
|
||||
/// The ID of the wl_output object
|
||||
pub id: ObjectId,
|
||||
pub name: Option<String>,
|
||||
pub bounds: Bounds<DevicePixels>,
|
||||
}
|
||||
|
||||
impl PlatformDisplay for WaylandDisplay {
|
||||
// todo(linux)
|
||||
fn id(&self) -> DisplayId {
|
||||
DisplayId(123) // return some fake data so it doesn't panic
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn uuid(&self) -> anyhow::Result<Uuid> {
|
||||
Ok(Uuid::from_bytes([0; 16])) // return some fake data so it doesn't panic
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn bounds(&self) -> Bounds<DevicePixels> {
|
||||
Bounds {
|
||||
origin: Default::default(),
|
||||
size: Size {
|
||||
width: DevicePixels(1000),
|
||||
height: DevicePixels(500),
|
||||
},
|
||||
} // return some fake data so it doesn't panic
|
||||
impl Hash for WaylandDisplay {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformDisplay for WaylandDisplay {
|
||||
fn id(&self) -> DisplayId {
|
||||
DisplayId(self.id.protocol_id())
|
||||
}
|
||||
|
||||
fn uuid(&self) -> anyhow::Result<Uuid> {
|
||||
if let Some(name) = &self.name {
|
||||
Ok(Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes()))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Wayland display does not have a name"))
|
||||
}
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Bounds<DevicePixels> {
|
||||
self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||
@@ -14,15 +12,11 @@ pub(crate) enum SerialKind {
|
||||
#[derive(Debug)]
|
||||
struct SerialData {
|
||||
serial: u32,
|
||||
time: Instant,
|
||||
}
|
||||
|
||||
impl SerialData {
|
||||
fn new(value: u32) -> Self {
|
||||
Self {
|
||||
serial: value,
|
||||
time: Instant::now(),
|
||||
}
|
||||
Self { serial: value }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,41 +46,4 @@ impl SerialTracker {
|
||||
.map(|serial_data| serial_data.serial)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Returns the newest serial of any of the provided [`SerialKind`]
|
||||
pub fn get_newest_of(&self, kinds: &[SerialKind]) -> u32 {
|
||||
kinds
|
||||
.iter()
|
||||
.filter_map(|kind| self.serials.get(&kind))
|
||||
.max_by_key(|serial_data| serial_data.time)
|
||||
.map(|serial_data| serial_data.serial)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serial_tracker() {
|
||||
let mut tracker = SerialTracker::new();
|
||||
|
||||
tracker.update(SerialKind::KeyPress, 100);
|
||||
tracker.update(SerialKind::MousePress, 50);
|
||||
tracker.update(SerialKind::MouseEnter, 300);
|
||||
|
||||
assert_eq!(
|
||||
tracker.get_newest_of(&[SerialKind::KeyPress, SerialKind::MousePress]),
|
||||
50
|
||||
);
|
||||
assert_eq!(tracker.get(SerialKind::DataDevice), 0);
|
||||
|
||||
tracker.update(SerialKind::KeyPress, 2000);
|
||||
assert_eq!(tracker.get(SerialKind::KeyPress), 2000);
|
||||
assert_eq!(
|
||||
tracker.get_newest_of(&[SerialKind::KeyPress, SerialKind::MousePress]),
|
||||
2000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
use std::any::Any;
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::ffi::c_void;
|
||||
use std::num::NonZeroU32;
|
||||
use std::ops::{Deref, Range};
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::HashMap;
|
||||
use futures::channel::oneshot::Receiver;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use raw_window_handle as rwh;
|
||||
use wayland_backend::client::ObjectId;
|
||||
use wayland_client::protocol::wl_region::WlRegion;
|
||||
use wayland_client::WEnum;
|
||||
use wayland_client::{protocol::wl_surface, Proxy};
|
||||
use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1;
|
||||
use wayland_protocols::wp::viewporter::client::wp_viewport;
|
||||
use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1;
|
||||
use wayland_protocols::xdg::shell::client::xdg_surface;
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self, WmCapabilities};
|
||||
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
|
||||
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
|
||||
|
||||
use crate::platform::blade::{BladeRenderer, BladeSurfaceConfig};
|
||||
use crate::platform::linux::wayland::display::WaylandDisplay;
|
||||
@@ -29,9 +26,9 @@ use crate::platform::linux::wayland::serial::SerialKind;
|
||||
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
|
||||
use crate::scene::Scene;
|
||||
use crate::{
|
||||
px, size, Bounds, DevicePixels, Globals, Modifiers, Pixels, PlatformDisplay, PlatformInput,
|
||||
Point, PromptLevel, Size, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowParams,
|
||||
px, size, AnyWindowHandle, Bounds, DevicePixels, Globals, Modifiers, Output, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, PromptLevel, Size, WaylandClientStatePtr,
|
||||
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowParams,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -75,7 +72,8 @@ pub struct WaylandWindowState {
|
||||
blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
outputs: HashSet<ObjectId>,
|
||||
outputs: HashMap<ObjectId, Output>,
|
||||
display: Option<(ObjectId, Output)>,
|
||||
globals: Globals,
|
||||
renderer: BladeRenderer,
|
||||
bounds: Bounds<u32>,
|
||||
@@ -83,10 +81,11 @@ pub struct WaylandWindowState {
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
decoration_state: WaylandDecorationState,
|
||||
fullscreen: bool,
|
||||
restore_bounds: Bounds<DevicePixels>,
|
||||
maximized: bool,
|
||||
windowed_bounds: Bounds<DevicePixels>,
|
||||
client: WaylandClientStatePtr,
|
||||
callbacks: Callbacks,
|
||||
handle: AnyWindowHandle,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -98,6 +97,7 @@ pub struct WaylandWindowStatePtr {
|
||||
impl WaylandWindowState {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
handle: AnyWindowHandle,
|
||||
surface: wl_surface::WlSurface,
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
@@ -150,18 +150,20 @@ impl WaylandWindowState {
|
||||
toplevel,
|
||||
viewport,
|
||||
globals,
|
||||
outputs: HashSet::default(),
|
||||
outputs: HashMap::default(),
|
||||
display: None,
|
||||
renderer: BladeRenderer::new(gpu, config),
|
||||
bounds,
|
||||
scale: 1.0,
|
||||
input_handler: None,
|
||||
decoration_state: WaylandDecorationState::Client,
|
||||
fullscreen: false,
|
||||
restore_bounds: Bounds::default(),
|
||||
maximized: false,
|
||||
callbacks: Callbacks::default(),
|
||||
windowed_bounds: options.bounds,
|
||||
client,
|
||||
appearance,
|
||||
handle,
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,6 +219,7 @@ impl WaylandWindow {
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
handle: AnyWindowHandle,
|
||||
globals: Globals,
|
||||
client: WaylandClientStatePtr,
|
||||
params: WindowParams,
|
||||
@@ -227,6 +230,7 @@ impl WaylandWindow {
|
||||
.wm_base
|
||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||
toplevel.set_min_size(200, 200);
|
||||
|
||||
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
||||
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
||||
@@ -253,6 +257,7 @@ impl WaylandWindow {
|
||||
|
||||
let this = Self(WaylandWindowStatePtr {
|
||||
state: Rc::new(RefCell::new(WaylandWindowState::new(
|
||||
handle,
|
||||
surface.clone(),
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
@@ -274,6 +279,10 @@ impl WaylandWindow {
|
||||
}
|
||||
|
||||
impl WaylandWindowStatePtr {
|
||||
pub fn handle(&self) -> AnyWindowHandle {
|
||||
self.state.borrow().handle
|
||||
}
|
||||
|
||||
pub fn surface(&self) -> wl_surface::WlSurface {
|
||||
self.state.borrow().surface.clone()
|
||||
}
|
||||
@@ -340,23 +349,32 @@ impl WaylandWindowStatePtr {
|
||||
pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool {
|
||||
match event {
|
||||
xdg_toplevel::Event::Configure {
|
||||
width,
|
||||
height,
|
||||
mut width,
|
||||
mut height,
|
||||
states,
|
||||
} => {
|
||||
let width = NonZeroU32::new(width as u32);
|
||||
let height = NonZeroU32::new(height as u32);
|
||||
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
|
||||
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.maximized = maximized;
|
||||
let got_unmaximized = state.maximized && !maximized;
|
||||
state.fullscreen = fullscreen;
|
||||
if fullscreen || maximized {
|
||||
state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32));
|
||||
state.maximized = maximized;
|
||||
|
||||
if got_unmaximized {
|
||||
width = state.windowed_bounds.size.width.0;
|
||||
height = state.windowed_bounds.size.height.0;
|
||||
} else if width != 0 && height != 0 && !fullscreen && !maximized {
|
||||
state.windowed_bounds = Bounds {
|
||||
origin: Point::default(),
|
||||
size: size(width.into(), height.into()),
|
||||
};
|
||||
}
|
||||
|
||||
let width = NonZeroU32::new(width as u32);
|
||||
let height = NonZeroU32::new(height as u32);
|
||||
drop(state);
|
||||
self.resize(width, height);
|
||||
self.set_fullscreen(fullscreen);
|
||||
|
||||
false
|
||||
}
|
||||
@@ -381,58 +399,48 @@ impl WaylandWindowStatePtr {
|
||||
pub fn handle_surface_event(
|
||||
&self,
|
||||
event: wl_surface::Event,
|
||||
output_scales: HashMap<ObjectId, i32>,
|
||||
outputs: HashMap<ObjectId, Output>,
|
||||
) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
|
||||
// We use `WpFractionalScale` instead to set the scale if it's available
|
||||
if state.globals.fractional_scale_manager.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
match event {
|
||||
wl_surface::Event::Enter { output } => {
|
||||
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||
if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||
let id = output.id();
|
||||
|
||||
let Some(output) = outputs.get(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.outputs.insert(id, output.clone());
|
||||
|
||||
let scale = primary_output_scale(&mut state);
|
||||
|
||||
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||
if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||
state.surface.set_buffer_scale(scale);
|
||||
drop(state);
|
||||
self.rescale(scale as f32);
|
||||
}
|
||||
|
||||
state.outputs.insert(output.id());
|
||||
|
||||
let mut scale = 1;
|
||||
for output in state.outputs.iter() {
|
||||
if let Some(s) = output_scales.get(output) {
|
||||
scale = scale.max(*s)
|
||||
}
|
||||
}
|
||||
|
||||
state.surface.set_buffer_scale(scale);
|
||||
drop(state);
|
||||
self.rescale(scale as f32);
|
||||
}
|
||||
wl_surface::Event::Leave { output } => {
|
||||
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||
if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||
return;
|
||||
}
|
||||
|
||||
state.outputs.remove(&output.id());
|
||||
|
||||
let mut scale = 1;
|
||||
for output in state.outputs.iter() {
|
||||
if let Some(s) = output_scales.get(output) {
|
||||
scale = scale.max(*s)
|
||||
}
|
||||
}
|
||||
let scale = primary_output_scale(&mut state);
|
||||
|
||||
state.surface.set_buffer_scale(scale);
|
||||
drop(state);
|
||||
self.rescale(scale as f32);
|
||||
// We use `PreferredBufferScale` instead to set the scale if it's available
|
||||
if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
|
||||
state.surface.set_buffer_scale(scale);
|
||||
drop(state);
|
||||
self.rescale(scale as f32);
|
||||
}
|
||||
}
|
||||
wl_surface::Event::PreferredBufferScale { factor } => {
|
||||
state.surface.set_buffer_scale(factor);
|
||||
drop(state);
|
||||
self.rescale(factor as f32);
|
||||
// We use `WpFractionalScale` instead to set the scale if it's available
|
||||
if state.globals.fractional_scale_manager.is_none() {
|
||||
state.surface.set_buffer_scale(factor);
|
||||
drop(state);
|
||||
self.rescale(factor as f32);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -534,11 +542,6 @@ impl WaylandWindowStatePtr {
|
||||
self.set_size_and_scale(None, None, Some(scale));
|
||||
}
|
||||
|
||||
pub fn set_fullscreen(&self, fullscreen: bool) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.fullscreen = fullscreen;
|
||||
}
|
||||
|
||||
/// Notifies the window of the state of the decorations.
|
||||
///
|
||||
/// # Note
|
||||
@@ -577,6 +580,7 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn set_focused(&self, focus: bool) {
|
||||
self.state.borrow_mut().active = focus;
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change {
|
||||
fun(focus);
|
||||
}
|
||||
@@ -592,6 +596,23 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
fn primary_output_scale(state: &mut RefMut<WaylandWindowState>) -> i32 {
|
||||
let mut scale = 1;
|
||||
let mut current_output = state.display.take();
|
||||
for (id, output) in state.outputs.iter() {
|
||||
if let Some((_, output_data)) = ¤t_output {
|
||||
if output.scale > output_data.scale {
|
||||
current_output = Some((id.clone(), output.clone()));
|
||||
}
|
||||
} else {
|
||||
current_output = Some((id.clone(), output.clone()));
|
||||
}
|
||||
scale = scale.max(output.scale);
|
||||
}
|
||||
state.display = current_output;
|
||||
scale
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for WaylandWindow {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
|
||||
unimplemented!()
|
||||
@@ -612,14 +633,12 @@ impl PlatformWindow for WaylandWindow {
|
||||
self.borrow().maximized
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
// check if it is right
|
||||
fn window_bounds(&self) -> WindowBounds {
|
||||
let state = self.borrow();
|
||||
if state.fullscreen {
|
||||
WindowBounds::Fullscreen(state.restore_bounds)
|
||||
WindowBounds::Fullscreen(state.windowed_bounds)
|
||||
} else if state.maximized {
|
||||
WindowBounds::Maximized(state.restore_bounds)
|
||||
WindowBounds::Maximized(state.windowed_bounds)
|
||||
} else {
|
||||
WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p as i32)))
|
||||
}
|
||||
@@ -641,19 +660,27 @@ impl PlatformWindow for WaylandWindow {
|
||||
self.borrow().appearance
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||
Rc::new(WaylandDisplay {})
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
self.borrow().display.as_ref().map(|(id, display)| {
|
||||
Rc::new(WaylandDisplay {
|
||||
id: id.clone(),
|
||||
name: display.name.clone(),
|
||||
bounds: display.bounds,
|
||||
}) as Rc<dyn PlatformDisplay>
|
||||
})
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
Point::default()
|
||||
self.borrow()
|
||||
.client
|
||||
.get_client()
|
||||
.borrow()
|
||||
.mouse_location
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn modifiers(&self) -> Modifiers {
|
||||
crate::Modifiers::default()
|
||||
self.borrow().client.get_client().borrow().modifiers
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||
@@ -666,21 +693,20 @@ impl PlatformWindow for WaylandWindow {
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
level: PromptLevel,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
_level: PromptLevel,
|
||||
_msg: &str,
|
||||
_detail: Option<&str>,
|
||||
_answers: &[&str],
|
||||
) -> Option<Receiver<usize>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
// todo(linux)
|
||||
log::info!("Wayland does not support this API");
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn is_active(&self) -> bool {
|
||||
false
|
||||
self.borrow().active
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
@@ -712,8 +738,8 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
if let Some(ref blur_manager) = state.globals.blur_manager {
|
||||
if (background_appearance == WindowBackgroundAppearance::Blurred) {
|
||||
if (state.blur.is_none()) {
|
||||
if background_appearance == WindowBackgroundAppearance::Blurred {
|
||||
if state.blur.is_none() {
|
||||
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
|
||||
blur.set_region(Some(®ion));
|
||||
state.blur = Some(blur);
|
||||
@@ -731,12 +757,12 @@ impl PlatformWindow for WaylandWindow {
|
||||
region.destroy();
|
||||
}
|
||||
|
||||
fn set_edited(&mut self, edited: bool) {
|
||||
// todo(linux)
|
||||
fn set_edited(&mut self, _edited: bool) {
|
||||
log::info!("ignoring macOS specific set_edited");
|
||||
}
|
||||
|
||||
fn show_character_palette(&self) {
|
||||
// todo(linux)
|
||||
log::info!("ignoring macOS specific show_character_palette");
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
@@ -754,7 +780,6 @@ impl PlatformWindow for WaylandWindow {
|
||||
|
||||
fn toggle_fullscreen(&self) {
|
||||
let mut state = self.borrow_mut();
|
||||
state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32));
|
||||
if !state.fullscreen {
|
||||
state.toplevel.set_fullscreen(None);
|
||||
} else {
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use calloop::generic::{FdWrapper, Generic};
|
||||
use calloop::{channel, EventLoop, LoopHandle, RegistrationToken};
|
||||
use calloop::{EventLoop, LoopHandle, RegistrationToken};
|
||||
|
||||
use collections::HashMap;
|
||||
use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext};
|
||||
use copypasta::ClipboardProvider;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use util::ResultExt;
|
||||
use x11rb::connection::{Connection, RequestConnection};
|
||||
use x11rb::cursor;
|
||||
use x11rb::errors::ConnectionError;
|
||||
use x11rb::protocol::randr::ConnectionExt as _;
|
||||
use x11rb::protocol::xinput::{ConnectionExt, ScrollClass};
|
||||
use x11rb::protocol::xinput::{ConnectionExt, KeyCode};
|
||||
use x11rb::protocol::xkb::ConnectionExt as _;
|
||||
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
|
||||
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _, KeyButMask};
|
||||
use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event};
|
||||
use x11rb::resource_manager::Database;
|
||||
use x11rb::xcb_ffi::XCBConnection;
|
||||
use xim::{x11rb::X11rbClient, Client};
|
||||
use xim::{AHashMap, AttributeName, ClientHandler, InputStyle};
|
||||
use xim::{AttributeName, InputStyle};
|
||||
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
|
||||
use xkbcommon::xkb as xkbc;
|
||||
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow, WaylandClientState};
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId,
|
||||
ForegroundExecutor, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay,
|
||||
PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowAppearance, WindowParams, X11Window,
|
||||
Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point,
|
||||
ScrollDelta, Size, TouchPhase, WindowParams, X11Window, XimXCBConnection,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -54,6 +52,12 @@ pub(crate) struct WindowRef {
|
||||
refresh_event_token: RegistrationToken,
|
||||
}
|
||||
|
||||
impl WindowRef {
|
||||
pub fn handle(&self) -> AnyWindowHandle {
|
||||
self.window.state.borrow().handle
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for WindowRef {
|
||||
type Target = X11WindowStatePtr;
|
||||
|
||||
@@ -92,6 +96,13 @@ impl From<xim::ClientError> for EventHandlerError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct KeyEvent {
|
||||
time: Instant,
|
||||
state: KeyButMask,
|
||||
code: KeyCode,
|
||||
}
|
||||
|
||||
pub struct X11ClientState {
|
||||
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
|
||||
@@ -104,13 +115,14 @@ pub struct X11ClientState {
|
||||
|
||||
pub(crate) xcb_connection: Rc<XCBConnection>,
|
||||
pub(crate) x_root_index: usize,
|
||||
pub(crate) resource_database: Database,
|
||||
pub(crate) _resource_database: Database,
|
||||
pub(crate) atoms: XcbAtoms,
|
||||
pub(crate) windows: HashMap<xproto::Window, WindowRef>,
|
||||
pub(crate) focused_window: Option<xproto::Window>,
|
||||
pub(crate) xkb: xkbc::State,
|
||||
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
|
||||
pub(crate) ximc: Option<X11rbClient<XimXCBConnection>>,
|
||||
pub(crate) xim_handler: Option<XimHandler>,
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
pub(crate) compose_state: xkbc::compose::State,
|
||||
pub(crate) pre_edit_text: Option<String>,
|
||||
@@ -126,6 +138,8 @@ pub struct X11ClientState {
|
||||
pub(crate) common: LinuxCommon,
|
||||
pub(crate) clipboard: X11ClipboardContext<Clipboard>,
|
||||
pub(crate) primary: X11ClipboardContext<Primary>,
|
||||
|
||||
last_key_event: Option<KeyEvent>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -159,11 +173,13 @@ impl X11Client {
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
handle.insert_source(main_receiver, |event, _, _: &mut X11Client| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
handle
|
||||
.insert_source(main_receiver, |event, _, _: &mut X11Client| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
runnable.run();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
|
||||
xcb_connection
|
||||
@@ -248,23 +264,7 @@ impl X11Client {
|
||||
xkbc::compose::State::new(&table, xkbc::compose::STATE_NO_FLAGS)
|
||||
};
|
||||
|
||||
let screen = xcb_connection.setup().roots.get(x_root_index).unwrap();
|
||||
|
||||
// Values from `Database::GET_RESOURCE_DATABASE`
|
||||
let resource_manager = xcb_connection
|
||||
.get_property(
|
||||
false,
|
||||
screen.root,
|
||||
xproto::AtomEnum::RESOURCE_MANAGER,
|
||||
xproto::AtomEnum::STRING,
|
||||
0,
|
||||
100_000_000,
|
||||
)
|
||||
.unwrap();
|
||||
let resource_manager = resource_manager.reply().unwrap();
|
||||
|
||||
// todo(linux): read hostname
|
||||
let resource_database = Database::new_from_default(&resource_manager, "HOSTNAME".into());
|
||||
let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap();
|
||||
|
||||
let scale_factor = resource_database
|
||||
.get_value("Xft.dpi", "Xft.dpi")
|
||||
@@ -283,11 +283,11 @@ impl X11Client {
|
||||
|
||||
let xcb_connection = Rc::new(xcb_connection);
|
||||
|
||||
let (xim_tx, xim_rx) = channel::channel::<XimCallbackEvent>();
|
||||
|
||||
let ximc = X11rbClient::init(Rc::clone(&xcb_connection), x_root_index, None).ok();
|
||||
let xim_xcb_connection =
|
||||
XimXCBConnection::new(Rc::clone(&xcb_connection), Rc::new(Cell::new(true)));
|
||||
let ximc = X11rbClient::init(xim_xcb_connection.clone(), x_root_index, None).ok();
|
||||
let xim_handler = if ximc.is_some() {
|
||||
Some(XimHandler::new(xim_tx))
|
||||
Some(XimHandler::new())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -305,15 +305,51 @@ impl X11Client {
|
||||
{
|
||||
let xcb_connection = xcb_connection.clone();
|
||||
move |_readiness, _, client| {
|
||||
// println!("---------------------------------------------------");
|
||||
// let mut events_count = 0;
|
||||
// let start = Instant::now();
|
||||
|
||||
xim_xcb_connection.set_can_flush(false);
|
||||
while let Some(event) = xcb_connection.poll_for_event()? {
|
||||
// events_count += 1;
|
||||
|
||||
// let event_start = Instant::now();
|
||||
// println!(
|
||||
// "--> event {}: {} (sequnce: {:?})",
|
||||
// events_count,
|
||||
// ev_name,
|
||||
// event.wire_sequence_number()
|
||||
// );
|
||||
|
||||
// let _drop = util::defer(move || {
|
||||
// println!(
|
||||
// "<-- event {}: {}, took: {:?}",
|
||||
// events_count,
|
||||
// ev_name,
|
||||
// event_start.elapsed()
|
||||
// );
|
||||
// });
|
||||
|
||||
let mut state = client.0.borrow_mut();
|
||||
|
||||
let last_key_event = match event {
|
||||
Event::KeyPress(ev) | Event::KeyRelease(ev) => {
|
||||
state.last_key_event.replace(KeyEvent { time: Instant::now(), state: ev.state, code: ev.detail });
|
||||
None
|
||||
}
|
||||
_ => state.last_key_event.clone()
|
||||
};
|
||||
// let last_key_event_time = state.last_key_event.clone();
|
||||
|
||||
if state.ximc.is_none() || state.xim_handler.is_none() {
|
||||
drop(state);
|
||||
client.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
|
||||
let xim_connected = xim_handler.connected;
|
||||
drop(state);
|
||||
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
|
||||
@@ -323,11 +359,61 @@ impl X11Client {
|
||||
false
|
||||
}
|
||||
};
|
||||
if let Some(xim_event) = xim_handler.last_callback_event.take() {
|
||||
match xim_event {
|
||||
XimCallbackEvent::XimXEvent(
|
||||
event @ Event::KeyPress(key_event),
|
||||
)
|
||||
| XimCallbackEvent::XimXEvent(
|
||||
event @ Event::KeyRelease(key_event),
|
||||
) => {
|
||||
// let ev_name = event_name(&event);
|
||||
// println!("---> XimXEvent: {}", ev_name);
|
||||
|
||||
if let Some(last_key_event) = last_key_event {
|
||||
// println!(
|
||||
// "last_key_event_time: {}",
|
||||
// last_key_event_time
|
||||
// );
|
||||
// println!(
|
||||
// "event.time: {} (sequence: {})",
|
||||
// key_event.time, key_event.sequence
|
||||
// );
|
||||
// let lag_ms = last_key_event_time.saturating_sub(key_event.time);
|
||||
// if lag_ms > 100
|
||||
let lag_ms = last_key_event.time.elapsed();
|
||||
if (lag_ms > Duration::from_millis(100) && last_key_event.state == key_event.state && last_key_event.code == key_event.detail) ||
|
||||
(lag_ms > Duration::from_millis(50) && last_key_event.state != key_event.state && last_key_event.code != key_event.detail)
|
||||
{
|
||||
let name = event_name(&event);
|
||||
println!(">>>>>>>>>>>>>> dropping old event (name: {}, sequence: {}, lag_ms: {:?}) <<<<<<<<<<<<<<<<<<<<<", name, key_event.sequence, lag_ms);
|
||||
// drop(state);
|
||||
// continue;
|
||||
} else {
|
||||
client.handle_event(event);
|
||||
}
|
||||
} else {
|
||||
client.handle_event(event);
|
||||
}
|
||||
}
|
||||
XimCallbackEvent::XimXEvent(event) => {
|
||||
client.handle_event(event);
|
||||
}
|
||||
XimCallbackEvent::XimCommitEvent(window, text) => {
|
||||
client.xim_handle_commit(window, text);
|
||||
}
|
||||
XimCallbackEvent::XimPreeditEvent(window, text) => {
|
||||
client.xim_handle_preedit(window, text);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let mut state = client.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
drop(state);
|
||||
if xim_filtered {
|
||||
// println!(" -> xim filtered");
|
||||
continue;
|
||||
}
|
||||
if xim_connected {
|
||||
@@ -336,45 +422,61 @@ impl X11Client {
|
||||
client.handle_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
// println!(
|
||||
// "-------------- events_count: {:?}, took: {:?} ----------\n\n",
|
||||
// events_count,
|
||||
// start.elapsed()
|
||||
// );
|
||||
|
||||
xim_xcb_connection.set_can_flush(true);
|
||||
xim_xcb_connection.flush()?;
|
||||
|
||||
Ok(calloop::PostAction::Continue)
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("Failed to initialize x11 event source");
|
||||
// handle
|
||||
// .insert_source(xim_rx, {
|
||||
// move |chan_event, _, client| match chan_event {
|
||||
// channel::Event::Msg(xim_event) => {
|
||||
// // println!(".......... XimCallBackEvent ...............");
|
||||
// match xim_event {
|
||||
// XimCallbackEvent::XimXEvent(event) => {
|
||||
// println!("--> XimXEvent");
|
||||
// client.handle_event(event);
|
||||
// }
|
||||
// XimCallbackEvent::XimCommitEvent(window, text) => {
|
||||
// client.xim_handle_commit(window, text);
|
||||
// }
|
||||
// XimCallbackEvent::XimPreeditEvent(window, text) => {
|
||||
// client.xim_handle_preedit(window, text);
|
||||
// }
|
||||
// };
|
||||
// // println!("......... DONE: XimCallBackEvent ...............");
|
||||
// }
|
||||
// channel::Event::Closed => {
|
||||
// log::error!("XIM Event Sender dropped")
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .expect("Failed to initialize XIM event source");
|
||||
handle
|
||||
.insert_source(xim_rx, {
|
||||
move |chan_event, _, client| match chan_event {
|
||||
channel::Event::Msg(xim_event) => {
|
||||
match (xim_event) {
|
||||
XimCallbackEvent::XimXEvent(event) => {
|
||||
client.handle_event(event);
|
||||
}
|
||||
XimCallbackEvent::XimCommitEvent(window, text) => {
|
||||
client.xim_handle_commit(window, text);
|
||||
}
|
||||
XimCallbackEvent::XimPreeditEvent(window, text) => {
|
||||
client.xim_handle_preedit(window, text);
|
||||
}
|
||||
};
|
||||
}
|
||||
channel::Event::Closed => {
|
||||
log::error!("XIM Event Sender dropped")
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
client.with_common(|common| common.appearance = appearance);
|
||||
for (_, window) in &mut client.0.borrow_mut().windows {
|
||||
window.window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("Failed to initialize XIM event source");
|
||||
handle.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
client.with_common(|common| common.appearance = appearance);
|
||||
for (_, window) in &mut client.0.borrow_mut().windows {
|
||||
window.window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
.unwrap();
|
||||
|
||||
X11Client(Rc::new(RefCell::new(X11ClientState {
|
||||
modifiers: Modifiers::default(),
|
||||
event_loop: Some(event_loop),
|
||||
loop_handle: handle,
|
||||
common,
|
||||
@@ -385,7 +487,7 @@ impl X11Client {
|
||||
|
||||
xcb_connection,
|
||||
x_root_index,
|
||||
resource_database,
|
||||
_resource_database: resource_database,
|
||||
atoms,
|
||||
windows: HashMap::default(),
|
||||
focused_window: None,
|
||||
@@ -407,6 +509,8 @@ impl X11Client {
|
||||
|
||||
clipboard,
|
||||
primary,
|
||||
|
||||
last_key_event: None,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -446,7 +550,8 @@ impl X11Client {
|
||||
});
|
||||
}
|
||||
}
|
||||
ximc.create_ic(xim_handler.im_id, ic_attributes.build());
|
||||
ximc.create_ic(xim_handler.im_id, ic_attributes.build())
|
||||
.ok();
|
||||
state = self.0.borrow_mut();
|
||||
state.xim_handler = Some(xim_handler);
|
||||
state.ximc = Some(ximc);
|
||||
@@ -457,7 +562,7 @@ impl X11Client {
|
||||
state.composing = false;
|
||||
if let Some(mut ximc) = state.ximc.take() {
|
||||
let xim_handler = state.xim_handler.as_ref().unwrap();
|
||||
ximc.destroy_ic(xim_handler.im_id, xim_handler.ic_id);
|
||||
ximc.destroy_ic(xim_handler.im_id, xim_handler.ic_id).ok();
|
||||
state.ximc = Some(ximc);
|
||||
}
|
||||
}
|
||||
@@ -471,6 +576,8 @@ impl X11Client {
|
||||
}
|
||||
|
||||
fn handle_event(&self, event: Event) -> Option<()> {
|
||||
// println!("handle_event: {:?}", event_name(&event));
|
||||
|
||||
match event {
|
||||
Event::ClientMessage(event) => {
|
||||
let window = self.get_window(event.window)?;
|
||||
@@ -503,6 +610,7 @@ impl X11Client {
|
||||
Event::Expose(event) => {
|
||||
let window = self.get_window(event.window)?;
|
||||
window.refresh();
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
Event::FocusIn(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
@@ -535,6 +643,7 @@ impl X11Client {
|
||||
);
|
||||
let modifiers = Modifiers::from_xkb(&state.xkb);
|
||||
let focused_window_id = state.focused_window?;
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
|
||||
let focused_window = self.get_window(focused_window_id)?;
|
||||
@@ -547,6 +656,8 @@ impl X11Client {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
state.modifiers = modifiers;
|
||||
|
||||
let keystroke = {
|
||||
let code = event.detail.into();
|
||||
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
@@ -590,6 +701,7 @@ impl X11Client {
|
||||
keystroke
|
||||
};
|
||||
drop(state);
|
||||
// println!("keydown. keystroke.key: {:?}", keystroke.key);
|
||||
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: false,
|
||||
@@ -600,6 +712,8 @@ impl X11Client {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
state.modifiers = modifiers;
|
||||
|
||||
let keystroke = {
|
||||
let code = event.detail.into();
|
||||
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
@@ -618,6 +732,8 @@ impl X11Client {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let modifiers = modifiers_from_xinput_info(event.mods);
|
||||
state.modifiers = modifiers;
|
||||
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
@@ -664,8 +780,10 @@ impl X11Client {
|
||||
}
|
||||
Event::XinputButtonRelease(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let state = self.0.borrow();
|
||||
let mut state = self.0.borrow_mut();
|
||||
let modifiers = modifiers_from_xinput_info(event.mods);
|
||||
state.modifiers = modifiers;
|
||||
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
@@ -683,14 +801,15 @@ impl X11Client {
|
||||
}
|
||||
Event::XinputMotion(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
let state = self.0.borrow();
|
||||
let mut state = self.0.borrow_mut();
|
||||
let pressed_button = pressed_button_from_mask(event.button_mask[0]);
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
);
|
||||
drop(state);
|
||||
let modifiers = modifiers_from_xinput_info(event.mods);
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
|
||||
let axisvalues = event
|
||||
.axisvalues
|
||||
@@ -761,18 +880,19 @@ impl X11Client {
|
||||
valuator_idx += 1;
|
||||
}
|
||||
}
|
||||
Event::XinputLeave(event) => {
|
||||
Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
||||
self.0.borrow_mut().scroll_x = None; // Set last scroll to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
|
||||
self.0.borrow_mut().scroll_y = None;
|
||||
|
||||
let window = self.get_window(event.event)?;
|
||||
let state = self.0.borrow();
|
||||
let mut state = self.0.borrow_mut();
|
||||
let pressed_button = pressed_button_from_mask(event.buttons[0]);
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
);
|
||||
let modifiers = modifiers_from_xinput_info(event.mods);
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
|
||||
window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent {
|
||||
@@ -788,13 +908,20 @@ impl X11Client {
|
||||
}
|
||||
|
||||
fn xim_handle_event(&self, event: Event) -> Option<()> {
|
||||
let name = event_name(&event);
|
||||
match event {
|
||||
Event::KeyPress(event) | Event::KeyRelease(event) => {
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
drop(state);
|
||||
xim_handler.window = event.event;
|
||||
|
||||
println!(
|
||||
"--> ximc.forward_event({}), sequence: {}",
|
||||
name, event.sequence
|
||||
);
|
||||
ximc.forward_event(
|
||||
xim_handler.im_id,
|
||||
xim_handler.ic_id,
|
||||
@@ -855,7 +982,8 @@ impl X11Client {
|
||||
);
|
||||
})
|
||||
.build();
|
||||
ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes);
|
||||
ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes)
|
||||
.ok();
|
||||
}
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
@@ -904,13 +1032,14 @@ impl LinuxClient for X11Client {
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
_handle: AnyWindowHandle,
|
||||
handle: AnyWindowHandle,
|
||||
params: WindowParams,
|
||||
) -> Box<dyn PlatformWindow> {
|
||||
let mut state = self.0.borrow_mut();
|
||||
let x_window = state.xcb_connection.generate_id().unwrap();
|
||||
|
||||
let window = X11Window::new(
|
||||
handle,
|
||||
X11ClientStatePtr(Rc::downgrade(&self.0)),
|
||||
state.common.foreground_executor.clone(),
|
||||
params,
|
||||
@@ -1034,11 +1163,11 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
|
||||
fn write_to_primary(&self, item: crate::ClipboardItem) {
|
||||
self.0.borrow_mut().primary.set_contents(item.text);
|
||||
self.0.borrow_mut().primary.set_contents(item.text).ok();
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
self.0.borrow_mut().clipboard.set_contents(item.text);
|
||||
self.0.borrow_mut().clipboard.set_contents(item.text).ok();
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
@@ -1075,6 +1204,16 @@ impl LinuxClient for X11Client {
|
||||
|
||||
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
let state = self.0.borrow();
|
||||
state.focused_window.and_then(|focused_window| {
|
||||
state
|
||||
.windows
|
||||
.get(&focused_window)
|
||||
.map(|window| window.handle())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Adatpted from:
|
||||
@@ -1089,3 +1228,111 @@ pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
|
||||
value.integral as f32 + value.frac as f32 / u32::MAX as f32
|
||||
}
|
||||
|
||||
fn event_name(event: &Event) -> &'static str {
|
||||
match event {
|
||||
Event::Unknown(_) => "Event::Unknown",
|
||||
Event::Error(_) => "Event::Error",
|
||||
Event::ButtonPress(_) => "Event::ButtonPress",
|
||||
Event::ButtonRelease(_) => "Event::ButtonRelease",
|
||||
Event::CirculateNotify(_) => "Event::CirculateNotify",
|
||||
Event::CirculateRequest(_) => "Event::CirculateRequest",
|
||||
Event::ClientMessage(_) => "Event::ClientMessage",
|
||||
Event::ColormapNotify(_) => "Event::ColormapNotify",
|
||||
Event::ConfigureNotify(_) => "Event::ConfigureNotify",
|
||||
Event::ConfigureRequest(_) => "Event::ConfigureRequest",
|
||||
Event::CreateNotify(_) => "Event::CreateNotify",
|
||||
Event::DestroyNotify(_) => "Event::DestroyNotify",
|
||||
Event::EnterNotify(_) => "Event::EnterNotify",
|
||||
Event::Expose(_) => "Event::Expose",
|
||||
Event::FocusIn(_) => "Event::FocusIn",
|
||||
Event::FocusOut(_) => "Event::FocusOut",
|
||||
Event::GeGeneric(_) => "Event::GeGeneric",
|
||||
Event::GraphicsExposure(_) => "Event::GraphicsExposure",
|
||||
Event::GravityNotify(_) => "Event::GravityNotify",
|
||||
Event::KeyPress(_) => "Event::KeyPress",
|
||||
Event::KeyRelease(_) => "Event::KeyRelease",
|
||||
Event::KeymapNotify(_) => "Event::KeymapNotify",
|
||||
Event::LeaveNotify(_) => "Event::LeaveNotify",
|
||||
Event::MapNotify(_) => "Event::MapNotify",
|
||||
Event::MapRequest(_) => "Event::MapRequest",
|
||||
Event::MappingNotify(_) => "Event::MappingNotify",
|
||||
Event::MotionNotify(_) => "Event::MotionNotify",
|
||||
Event::NoExposure(_) => "Event::NoExposure",
|
||||
Event::PropertyNotify(_) => "Event::PropertyNotify",
|
||||
Event::ReparentNotify(_) => "Event::ReparentNotify",
|
||||
Event::ResizeRequest(_) => "Event::ResizeRequest",
|
||||
Event::SelectionClear(_) => "Event::SelectionClear",
|
||||
Event::SelectionNotify(_) => "Event::SelectionNotify",
|
||||
Event::SelectionRequest(_) => "Event::SelectionRequest",
|
||||
Event::UnmapNotify(_) => "Event::UnmapNotify",
|
||||
Event::VisibilityNotify(_) => "Event::VisibilityNotify",
|
||||
Event::RandrNotify(_) => "Event::RandrNotify",
|
||||
Event::RandrScreenChangeNotify(_) => "Event::RandrScreenChangeNotify",
|
||||
Event::ShapeNotify(_) => "Event::ShapeNotify",
|
||||
Event::XfixesCursorNotify(_) => "Event::XfixesCursorNotify",
|
||||
Event::XfixesSelectionNotify(_) => "Event::XfixesSelectionNotify",
|
||||
Event::XinputBarrierHit(_) => "Event::XinputBarrierHit",
|
||||
Event::XinputBarrierLeave(_) => "Event::XinputBarrierLeave",
|
||||
Event::XinputButtonPress(_) => "Event::XinputButtonPress",
|
||||
Event::XinputButtonRelease(_) => "Event::XinputButtonRelease",
|
||||
Event::XinputChangeDeviceNotify(_) => "Event::XinputChangeDeviceNotify",
|
||||
Event::XinputDeviceButtonPress(_) => "Event::XinputDeviceButtonPress",
|
||||
Event::XinputDeviceButtonRelease(_) => "Event::XinputDeviceButtonRelease",
|
||||
Event::XinputDeviceButtonStateNotify(_) => "Event::XinputDeviceButtonStateNotify",
|
||||
Event::XinputDeviceChanged(_) => "Event::XinputDeviceChanged",
|
||||
Event::XinputDeviceFocusIn(_) => "Event::XinputDeviceFocusIn",
|
||||
Event::XinputDeviceFocusOut(_) => "Event::XinputDeviceFocusOut",
|
||||
Event::XinputDeviceKeyPress(_) => "Event::XinputDeviceKeyPress",
|
||||
Event::XinputDeviceKeyRelease(_) => "Event::XinputDeviceKeyRelease",
|
||||
Event::XinputDeviceKeyStateNotify(_) => "Event::XinputDeviceKeyStateNotify",
|
||||
Event::XinputDeviceMappingNotify(_) => "Event::XinputDeviceMappingNotify",
|
||||
Event::XinputDeviceMotionNotify(_) => "Event::XinputDeviceMotionNotify",
|
||||
Event::XinputDevicePresenceNotify(_) => "Event::XinputDevicePresenceNotify",
|
||||
Event::XinputDevicePropertyNotify(_) => "Event::XinputDevicePropertyNotify",
|
||||
Event::XinputDeviceStateNotify(_) => "Event::XinputDeviceStateNotify",
|
||||
Event::XinputDeviceValuator(_) => "Event::XinputDeviceValuator",
|
||||
Event::XinputEnter(_) => "Event::XinputEnter",
|
||||
Event::XinputFocusIn(_) => "Event::XinputFocusIn",
|
||||
Event::XinputFocusOut(_) => "Event::XinputFocusOut",
|
||||
Event::XinputGesturePinchBegin(_) => "Event::XinputGesturePinchBegin",
|
||||
Event::XinputGesturePinchEnd(_) => "Event::XinputGesturePinchEnd",
|
||||
Event::XinputGesturePinchUpdate(_) => "Event::XinputGesturePinchUpdate",
|
||||
Event::XinputGestureSwipeBegin(_) => "Event::XinputGestureSwipeBegin",
|
||||
Event::XinputGestureSwipeEnd(_) => "Event::XinputGestureSwipeEnd",
|
||||
Event::XinputGestureSwipeUpdate(_) => "Event::XinputGestureSwipeUpdate",
|
||||
Event::XinputHierarchy(_) => "Event::XinputHierarchy",
|
||||
Event::XinputKeyPress(_) => "Event::XinputKeyPress",
|
||||
Event::XinputKeyRelease(_) => "Event::XinputKeyRelease",
|
||||
Event::XinputLeave(_) => "Event::XinputLeave",
|
||||
Event::XinputMotion(_) => "Event::XinputMotion",
|
||||
Event::XinputProperty(_) => "Event::XinputProperty",
|
||||
Event::XinputProximityIn(_) => "Event::XinputProximityIn",
|
||||
Event::XinputProximityOut(_) => "Event::XinputProximityOut",
|
||||
Event::XinputRawButtonPress(_) => "Event::XinputRawButtonPress",
|
||||
Event::XinputRawButtonRelease(_) => "Event::XinputRawButtonRelease",
|
||||
Event::XinputRawKeyPress(_) => "Event::XinputRawKeyPress",
|
||||
Event::XinputRawKeyRelease(_) => "Event::XinputRawKeyRelease",
|
||||
Event::XinputRawMotion(_) => "Event::XinputRawMotion",
|
||||
Event::XinputRawTouchBegin(_) => "Event::XinputRawTouchBegin",
|
||||
Event::XinputRawTouchEnd(_) => "Event::XinputRawTouchEnd",
|
||||
Event::XinputRawTouchUpdate(_) => "Event::XinputRawTouchUpdate",
|
||||
Event::XinputTouchBegin(_) => "Event::XinputTouchBegin",
|
||||
Event::XinputTouchEnd(_) => "Event::XinputTouchEnd",
|
||||
Event::XinputTouchOwnership(_) => "Event::XinputTouchOwnership",
|
||||
Event::XinputTouchUpdate(_) => "Event::XinputTouchUpdate",
|
||||
Event::XkbAccessXNotify(_) => "Event::XkbAccessXNotify",
|
||||
Event::XkbActionMessage(_) => "Event::XkbActionMessage",
|
||||
Event::XkbBellNotify(_) => "Event::XkbBellNotify",
|
||||
Event::XkbCompatMapNotify(_) => "Event::XkbCompatMapNotify",
|
||||
Event::XkbControlsNotify(_) => "Event::XkbControlsNotify",
|
||||
Event::XkbExtensionDeviceNotify(_) => "Event::XkbExtensionDeviceNotify",
|
||||
Event::XkbIndicatorMapNotify(_) => "Event::XkbIndicatorMapNotify",
|
||||
Event::XkbIndicatorStateNotify(_) => "Event::XkbIndicatorStateNotify",
|
||||
Event::XkbMapNotify(_) => "Event::XkbMapNotify",
|
||||
Event::XkbNamesNotify(_) => "Event::XkbNamesNotify",
|
||||
Event::XkbNewKeyboardNotify(_) => "Event::XkbNewKeyboardNotify",
|
||||
Event::XkbStateNotify(_) => "Event::XkbStateNotify",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,35 @@
|
||||
// todo(linux): remove
|
||||
#![allow(unused)]
|
||||
|
||||
use crate::{
|
||||
platform::blade::{BladeRenderer, BladeSurfaceConfig},
|
||||
size, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels, Platform, PlatformAtlas,
|
||||
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel,
|
||||
Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions,
|
||||
WindowParams, X11Client, X11ClientState, X11ClientStatePtr,
|
||||
size, AnyWindowHandle, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels,
|
||||
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
|
||||
PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowParams, X11ClientStatePtr,
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use parking_lot::Mutex;
|
||||
use raw_window_handle as rwh;
|
||||
use util::ResultExt;
|
||||
use x11rb::{
|
||||
connection::{Connection as _, RequestConnection as _},
|
||||
connection::Connection,
|
||||
protocol::{
|
||||
render,
|
||||
xinput::{self, ConnectionExt as _},
|
||||
xproto::{
|
||||
self, Atom, ClientMessageEvent, ConnectionExt as _, CreateWindowAux, EventMask,
|
||||
TranslateCoordinatesReply,
|
||||
self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
|
||||
},
|
||||
},
|
||||
resource_manager::Database,
|
||||
wrapper::ConnectionExt as _,
|
||||
xcb_ffi::XCBConnection,
|
||||
};
|
||||
|
||||
use std::ops::Deref;
|
||||
use std::rc::Weak;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
collections::HashMap,
|
||||
cell::RefCell,
|
||||
ffi::c_void,
|
||||
iter::Zip,
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::Div,
|
||||
ptr::NonNull,
|
||||
rc::Rc,
|
||||
sync::{self, Arc},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use super::{X11Display, XINPUT_MASTER_DEVICE};
|
||||
@@ -165,29 +154,29 @@ pub struct Callbacks {
|
||||
appearance_changed: Option<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
pub(crate) struct X11WindowState {
|
||||
pub struct X11WindowState {
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
x_root_window: xproto::Window,
|
||||
raw: RawWindow,
|
||||
_raw: RawWindow,
|
||||
bounds: Bounds<i32>,
|
||||
scale_factor: f32,
|
||||
renderer: BladeRenderer,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
appearance: WindowAppearance,
|
||||
pub handle: AnyWindowHandle,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct X11WindowStatePtr {
|
||||
pub(crate) state: Rc<RefCell<X11WindowState>>,
|
||||
pub state: Rc<RefCell<X11WindowState>>,
|
||||
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
|
||||
xcb_connection: Rc<XCBConnection>,
|
||||
x_window: xproto::Window,
|
||||
}
|
||||
|
||||
// todo(linux): Remove other RawWindowHandle implementation
|
||||
impl rwh::HasWindowHandle for RawWindow {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
|
||||
let non_zero = NonZeroU32::new(self.window_id).unwrap();
|
||||
@@ -218,6 +207,7 @@ impl rwh::HasDisplayHandle for X11Window {
|
||||
impl X11WindowState {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
handle: AnyWindowHandle,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
@@ -272,8 +262,6 @@ impl X11WindowState {
|
||||
.event_mask(
|
||||
xproto::EventMask::EXPOSURE
|
||||
| xproto::EventMask::STRUCTURE_NOTIFY
|
||||
| xproto::EventMask::ENTER_WINDOW
|
||||
| xproto::EventMask::LEAVE_WINDOW
|
||||
| xproto::EventMask::FOCUS_CHANGE
|
||||
| xproto::EventMask::KEY_PRESS
|
||||
| xproto::EventMask::KEY_RELEASE,
|
||||
@@ -372,7 +360,7 @@ impl X11WindowState {
|
||||
client,
|
||||
executor,
|
||||
display: Rc::new(X11Display::new(xcb_connection, x_screen_index).unwrap()),
|
||||
raw,
|
||||
_raw: raw,
|
||||
x_root_window: visual_set.root,
|
||||
bounds: params.bounds.map(|v| v.0),
|
||||
scale_factor,
|
||||
@@ -380,6 +368,7 @@ impl X11WindowState {
|
||||
atoms: *atoms,
|
||||
input_handler: None,
|
||||
appearance,
|
||||
handle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,14 +409,15 @@ impl Drop for X11Window {
|
||||
}
|
||||
|
||||
enum WmHintPropertyState {
|
||||
Remove = 0,
|
||||
Add = 1,
|
||||
// Remove = 0,
|
||||
// Add = 1,
|
||||
Toggle = 2,
|
||||
}
|
||||
|
||||
impl X11Window {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
handle: AnyWindowHandle,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
@@ -440,6 +430,7 @@ impl X11Window {
|
||||
) -> Self {
|
||||
Self(X11WindowStatePtr {
|
||||
state: Rc::new(RefCell::new(X11WindowState::new(
|
||||
handle,
|
||||
client,
|
||||
executor,
|
||||
params,
|
||||
@@ -541,8 +532,15 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn handle_input(&self, input: PlatformInput) {
|
||||
// println!("handle_input called");
|
||||
// let start = Instant::now();
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
|
||||
// println!(
|
||||
// "handle_input. got input callback. elapsed: {:?}",
|
||||
// start.elapsed()
|
||||
// );
|
||||
if !fun(input.clone()).propagate {
|
||||
// println!("handle_input. return here. elapsed: {:?}", start.elapsed());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -617,13 +615,21 @@ impl X11WindowStatePtr {
|
||||
|
||||
pub fn configure(&self, bounds: Bounds<i32>) {
|
||||
let mut resize_args = None;
|
||||
let do_move;
|
||||
let is_resize;
|
||||
{
|
||||
let mut state = self.state.borrow_mut();
|
||||
let old_bounds = mem::replace(&mut state.bounds, bounds);
|
||||
do_move = old_bounds.origin != bounds.origin;
|
||||
// todo(linux): use normal GPUI types here, refactor out the double
|
||||
// viewport check and extra casts ( )
|
||||
|
||||
is_resize = bounds.size.width != state.bounds.size.width
|
||||
|| bounds.size.height != state.bounds.size.height;
|
||||
|
||||
// If it's a resize event (only width/height changed), we ignore `bounds.origin`
|
||||
// because it contains wrong values.
|
||||
if is_resize {
|
||||
state.bounds.size = bounds.size;
|
||||
} else {
|
||||
state.bounds = bounds;
|
||||
}
|
||||
|
||||
let gpu_size = query_render_extent(&self.xcb_connection, self.x_window);
|
||||
if state.renderer.viewport_size() != gpu_size {
|
||||
state
|
||||
@@ -639,7 +645,7 @@ impl X11WindowStatePtr {
|
||||
fun(content_size, scale_factor)
|
||||
}
|
||||
}
|
||||
if do_move {
|
||||
if !is_resize {
|
||||
if let Some(ref mut fun) = callbacks.moved {
|
||||
fun()
|
||||
}
|
||||
@@ -698,8 +704,8 @@ impl PlatformWindow for X11Window {
|
||||
self.0.state.borrow().appearance
|
||||
}
|
||||
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||
self.0.state.borrow().display.clone()
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
Some(self.0.state.borrow().display.clone())
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
@@ -713,9 +719,15 @@ impl PlatformWindow for X11Window {
|
||||
Point::new((reply.root_x as u32).into(), (reply.root_y as u32).into())
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn modifiers(&self) -> Modifiers {
|
||||
Modifiers::default()
|
||||
self.0
|
||||
.state
|
||||
.borrow()
|
||||
.client
|
||||
.0
|
||||
.upgrade()
|
||||
.map(|ref_cell| ref_cell.borrow().modifiers)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
|
||||
@@ -792,8 +804,9 @@ impl PlatformWindow for X11Window {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// todo(linux)
|
||||
fn set_edited(&mut self, edited: bool) {}
|
||||
fn set_edited(&mut self, _edited: bool) {
|
||||
log::info!("ignoring macOS specific set_edited");
|
||||
}
|
||||
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
let mut inner = self.0.state.borrow_mut();
|
||||
@@ -801,14 +814,8 @@ impl PlatformWindow for X11Window {
|
||||
inner.renderer.update_transparency(transparent);
|
||||
}
|
||||
|
||||
// todo(linux), this corresponds to `orderFrontCharacterPalette` on macOS,
|
||||
// but it looks like the equivalent for Linux is GTK specific:
|
||||
//
|
||||
// https://docs.gtk.org/gtk3/signal.Entry.insert-emoji.html
|
||||
//
|
||||
// This API might need to change, or we might need to build an emoji picker into GPUI
|
||||
fn show_character_palette(&self) {
|
||||
unimplemented!()
|
||||
log::info!("ignoring macOS specific show_character_palette");
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use std::cell::RefCell;
|
||||
use std::default::Default;
|
||||
use std::rc::Rc;
|
||||
use std::{cell::Cell, default::Default, rc::Rc};
|
||||
|
||||
use calloop::channel;
|
||||
|
||||
use x11rb::protocol::{xproto, Event};
|
||||
use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle, Point};
|
||||
|
||||
use crate::{Keystroke, PlatformInput, X11ClientState};
|
||||
use x11rb::{
|
||||
connection::{Connection, RequestConnection},
|
||||
cookie::{CookieWithFds, VoidCookie},
|
||||
protocol::{xproto, Event},
|
||||
xcb_ffi::XCBConnection,
|
||||
};
|
||||
use xim::{
|
||||
x11rb::HasConnection, AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle,
|
||||
};
|
||||
|
||||
pub enum XimCallbackEvent {
|
||||
XimXEvent(x11rb::protocol::Event),
|
||||
@@ -18,19 +21,21 @@ pub enum XimCallbackEvent {
|
||||
pub struct XimHandler {
|
||||
pub im_id: u16,
|
||||
pub ic_id: u16,
|
||||
pub xim_tx: channel::Sender<XimCallbackEvent>,
|
||||
// pub xim_tx: channel::Sender<XimCallbackEvent>,
|
||||
pub connected: bool,
|
||||
pub window: xproto::Window,
|
||||
pub last_callback_event: Option<XimCallbackEvent>,
|
||||
}
|
||||
|
||||
impl XimHandler {
|
||||
pub fn new(xim_tx: channel::Sender<XimCallbackEvent>) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
im_id: Default::default(),
|
||||
ic_id: Default::default(),
|
||||
xim_tx,
|
||||
// xim_tx,
|
||||
connected: false,
|
||||
window: Default::default(),
|
||||
last_callback_event: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,10 +89,12 @@ impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler
|
||||
_input_context_id: u16,
|
||||
text: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
self.xim_tx.send(XimCallbackEvent::XimCommitEvent(
|
||||
self.window,
|
||||
String::from(text),
|
||||
));
|
||||
self.last_callback_event
|
||||
.replace(XimCallbackEvent::XimCommitEvent(
|
||||
self.window,
|
||||
String::from(text),
|
||||
));
|
||||
// .ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -99,14 +106,22 @@ impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler
|
||||
_flag: xim::ForwardEventFlag,
|
||||
xev: C::XEvent,
|
||||
) -> Result<(), ClientError> {
|
||||
match (xev.response_type) {
|
||||
match xev.response_type {
|
||||
x11rb::protocol::xproto::KEY_PRESS_EVENT => {
|
||||
self.xim_tx
|
||||
.send(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)));
|
||||
// println!(
|
||||
// "XimHandler. handle_forward_event(KeyPress). sequence: {}",
|
||||
// xev.sequence
|
||||
// );
|
||||
self.last_callback_event
|
||||
.replace(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)));
|
||||
}
|
||||
x11rb::protocol::xproto::KEY_RELEASE_EVENT => {
|
||||
self.xim_tx
|
||||
.send(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)));
|
||||
// println!(
|
||||
// "XimHandler. handle_forward_event(KeyRelease), sequence: {}",
|
||||
// xev.sequence
|
||||
// );
|
||||
self.last_callback_event
|
||||
.replace(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -145,10 +160,185 @@ impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler
|
||||
// XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified,
|
||||
// but interchangeable as above
|
||||
// Currently there's no way to support these.
|
||||
let mark_range = self.xim_tx.send(XimCallbackEvent::XimPreeditEvent(
|
||||
self.window,
|
||||
String::from(preedit_string),
|
||||
));
|
||||
self.last_callback_event
|
||||
.replace(XimCallbackEvent::XimPreeditEvent(
|
||||
self.window,
|
||||
String::from(preedit_string),
|
||||
));
|
||||
// self.xim_tx
|
||||
// .send()
|
||||
// .ok();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct XimXCBConnection(Rc<XCBConnection>, Rc<Cell<bool>>);
|
||||
|
||||
impl XimXCBConnection {
|
||||
pub fn new(connection: Rc<XCBConnection>, can_flush: Rc<Cell<bool>>) -> Self {
|
||||
Self(connection, can_flush)
|
||||
}
|
||||
|
||||
pub fn set_can_flush(&self, can_flush: bool) {
|
||||
self.1.set(can_flush);
|
||||
}
|
||||
}
|
||||
|
||||
impl HasConnection for XimXCBConnection {
|
||||
type Connection = Self;
|
||||
|
||||
fn conn(&self) -> &Self::Connection {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection for XimXCBConnection {
|
||||
fn wait_for_raw_event_with_sequence(
|
||||
&self,
|
||||
) -> Result<x11rb::connection::RawEventAndSeqNumber<Self::Buf>, x11rb::errors::ConnectionError>
|
||||
{
|
||||
self.0.wait_for_raw_event_with_sequence()
|
||||
}
|
||||
|
||||
fn poll_for_raw_event_with_sequence(
|
||||
&self,
|
||||
) -> Result<
|
||||
Option<x11rb::connection::RawEventAndSeqNumber<Self::Buf>>,
|
||||
x11rb::errors::ConnectionError,
|
||||
> {
|
||||
self.0.poll_for_raw_event_with_sequence()
|
||||
}
|
||||
|
||||
fn flush(&self) -> Result<(), x11rb::errors::ConnectionError> {
|
||||
if self.1.get() {
|
||||
// println!("*real* flush");
|
||||
self.0.flush()
|
||||
} else {
|
||||
// println!("fake flush");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(&self) -> &xproto::Setup {
|
||||
self.0.setup()
|
||||
}
|
||||
|
||||
fn generate_id(&self) -> Result<u32, x11rb::errors::ReplyOrIdError> {
|
||||
self.0.generate_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestConnection for XimXCBConnection {
|
||||
type Buf = <XCBConnection as RequestConnection>::Buf;
|
||||
|
||||
fn send_request_with_reply<R>(
|
||||
&self,
|
||||
bufs: &[std::io::IoSlice<'_>],
|
||||
fds: Vec<x11rb::utils::RawFdContainer>,
|
||||
) -> Result<x11rb::cookie::Cookie<'_, Self, R>, x11rb::errors::ConnectionError>
|
||||
where
|
||||
R: x11rb::x11_utils::TryParse,
|
||||
{
|
||||
self.0
|
||||
.send_request_with_reply::<R>(bufs, fds)
|
||||
.map(|cookie| x11rb::cookie::Cookie::new(self, cookie.sequence_number()))
|
||||
}
|
||||
|
||||
fn send_request_with_reply_with_fds<R>(
|
||||
&self,
|
||||
bufs: &[std::io::IoSlice<'_>],
|
||||
fds: Vec<x11rb::utils::RawFdContainer>,
|
||||
) -> Result<CookieWithFds<'_, Self, R>, x11rb::errors::ConnectionError>
|
||||
where
|
||||
R: x11rb::x11_utils::TryParseFd,
|
||||
{
|
||||
self.0
|
||||
.send_request_with_reply_with_fds::<R>(bufs, fds)
|
||||
.map(|cookie| CookieWithFds::new(self, cookie.sequence_number()))
|
||||
}
|
||||
|
||||
fn send_request_without_reply(
|
||||
&self,
|
||||
bufs: &[std::io::IoSlice<'_>],
|
||||
fds: Vec<x11rb::utils::RawFdContainer>,
|
||||
) -> Result<VoidCookie<'_, Self>, x11rb::errors::ConnectionError> {
|
||||
self.0
|
||||
.send_request_without_reply(bufs, fds)
|
||||
.map(|cookie| VoidCookie::new(self, cookie.sequence_number()))
|
||||
}
|
||||
|
||||
fn discard_reply(
|
||||
&self,
|
||||
sequence: x11rb::connection::SequenceNumber,
|
||||
kind: x11rb::connection::RequestKind,
|
||||
mode: x11rb::connection::DiscardMode,
|
||||
) {
|
||||
self.0.discard_reply(sequence, kind, mode)
|
||||
}
|
||||
|
||||
fn prefetch_extension_information(
|
||||
&self,
|
||||
extension_name: &'static str,
|
||||
) -> Result<(), x11rb::errors::ConnectionError> {
|
||||
self.0.prefetch_extension_information(extension_name)
|
||||
}
|
||||
|
||||
fn extension_information(
|
||||
&self,
|
||||
extension_name: &'static str,
|
||||
) -> Result<Option<x11rb::x11_utils::ExtensionInformation>, x11rb::errors::ConnectionError>
|
||||
{
|
||||
self.0.extension_information(extension_name)
|
||||
}
|
||||
|
||||
fn wait_for_reply_or_raw_error(
|
||||
&self,
|
||||
sequence: x11rb::connection::SequenceNumber,
|
||||
) -> Result<x11rb::connection::ReplyOrError<Self::Buf>, x11rb::errors::ConnectionError> {
|
||||
self.0.wait_for_reply_or_raw_error(sequence)
|
||||
}
|
||||
|
||||
fn wait_for_reply(
|
||||
&self,
|
||||
sequence: x11rb::connection::SequenceNumber,
|
||||
) -> Result<Option<Self::Buf>, x11rb::errors::ConnectionError> {
|
||||
self.0.wait_for_reply(sequence)
|
||||
}
|
||||
|
||||
fn wait_for_reply_with_fds_raw(
|
||||
&self,
|
||||
sequence: x11rb::connection::SequenceNumber,
|
||||
) -> Result<
|
||||
x11rb::connection::ReplyOrError<x11rb::connection::BufWithFds<Self::Buf>, Self::Buf>,
|
||||
x11rb::errors::ConnectionError,
|
||||
> {
|
||||
self.0.wait_for_reply_with_fds_raw(sequence)
|
||||
}
|
||||
|
||||
fn check_for_raw_error(
|
||||
&self,
|
||||
sequence: x11rb::connection::SequenceNumber,
|
||||
) -> Result<Option<Self::Buf>, x11rb::errors::ConnectionError> {
|
||||
self.0.check_for_raw_error(sequence)
|
||||
}
|
||||
|
||||
fn prefetch_maximum_request_bytes(&self) {
|
||||
self.0.prefetch_maximum_request_bytes()
|
||||
}
|
||||
|
||||
fn maximum_request_bytes(&self) -> usize {
|
||||
self.0.maximum_request_bytes()
|
||||
}
|
||||
|
||||
fn parse_error(
|
||||
&self,
|
||||
error: &[u8],
|
||||
) -> Result<x11rb::x11_utils::X11Error, x11rb::errors::ParseError> {
|
||||
self.0.parse_error(error)
|
||||
}
|
||||
|
||||
fn parse_event(&self, event: &[u8]) -> Result<Event, x11rb::errors::ParseError> {
|
||||
self.0.parse_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::future::Future;
|
||||
use ashpd::desktop::settings::{ColorScheme, Settings};
|
||||
use calloop::channel::{Channel, Sender};
|
||||
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
|
||||
use parking_lot::Mutex;
|
||||
use smol::stream::StreamExt;
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -115,6 +114,7 @@ impl WindowAppearance {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
fn set_native(&mut self, cs: ColorScheme) {
|
||||
*self = Self::from_native(cs);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ use cocoa::{
|
||||
};
|
||||
|
||||
use objc::runtime::{BOOL, NO, YES};
|
||||
use std::ops::Range;
|
||||
use std::{
|
||||
ffi::{c_char, CStr},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
pub(crate) use dispatcher::*;
|
||||
pub(crate) use display::*;
|
||||
@@ -52,6 +55,21 @@ impl BoolExt for bool {
|
||||
}
|
||||
}
|
||||
|
||||
trait NSStringExt {
|
||||
unsafe fn to_str(&self) -> &str;
|
||||
}
|
||||
|
||||
impl NSStringExt for id {
|
||||
unsafe fn to_str(&self) -> &str {
|
||||
let cstr = self.UTF8String();
|
||||
if cstr.is_null() {
|
||||
""
|
||||
} else {
|
||||
CStr::from_ptr(cstr as *mut c_char).to_str().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct NSRange {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::{
|
||||
point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
|
||||
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||
platform::mac::NSStringExt, point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
|
||||
MouseUpEvent, NavigationDirection, Pixels, PlatformInput, ScrollDelta, ScrollWheelEvent,
|
||||
TouchPhase,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
||||
base::{id, YES},
|
||||
foundation::NSString as _,
|
||||
};
|
||||
use core_graphics::{
|
||||
event::{CGEvent, CGEventFlags, CGKeyCode},
|
||||
@@ -15,7 +15,7 @@ use core_graphics::{
|
||||
use ctor::ctor;
|
||||
use metal::foreign_types::ForeignType as _;
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr};
|
||||
use std::{borrow::Cow, mem, ptr};
|
||||
|
||||
const BACKSPACE_KEY: u16 = 0x7f;
|
||||
const SPACE_KEY: u16 = b' ' as u16;
|
||||
@@ -243,11 +243,10 @@ impl PlatformInput {
|
||||
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
use cocoa::appkit::*;
|
||||
|
||||
let mut chars_ignoring_modifiers =
|
||||
CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let mut chars_ignoring_modifiers = native_event
|
||||
.charactersIgnoringModifiers()
|
||||
.to_str()
|
||||
.to_string();
|
||||
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
|
||||
let modifiers = native_event.modifierFlags();
|
||||
|
||||
@@ -351,9 +350,6 @@ fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
|
||||
|
||||
unsafe {
|
||||
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
|
||||
CStr::from_ptr(event.characters().UTF8String())
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
event.characters().to_str().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,9 +826,6 @@ impl Platform for MacPlatform {
|
||||
CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
|
||||
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||
CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||
CursorStyle::DisappearingItem => {
|
||||
msg_send![class!(NSCursor), disappearingItemCursor]
|
||||
}
|
||||
CursorStyle::IBeamCursorForVerticalLayout => {
|
||||
msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{ns_string, renderer, MacDisplay, NSRange};
|
||||
use super::{ns_string, renderer, MacDisplay, NSRange, NSStringExt};
|
||||
use crate::{
|
||||
platform::PlatformInputHandler, point, px, size, AnyWindowHandle, Bounds, DevicePixels,
|
||||
DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke,
|
||||
@@ -39,7 +39,6 @@ use std::{
|
||||
ffi::{c_void, CStr},
|
||||
mem,
|
||||
ops::Range,
|
||||
os::raw::c_char,
|
||||
path::PathBuf,
|
||||
ptr::{self, NonNull},
|
||||
rc::Rc,
|
||||
@@ -312,6 +311,7 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Clone)]
|
||||
enum ImeInput {
|
||||
InsertText(String, Option<Range<usize>>),
|
||||
SetMarkedText(String, Option<Range<usize>>, Option<Range<usize>>),
|
||||
@@ -340,7 +340,7 @@ struct MacWindowState {
|
||||
traffic_light_position: Option<Point<Pixels>>,
|
||||
previous_modifiers_changed_event: Option<PlatformInput>,
|
||||
// State tracking what the IME did after the last request
|
||||
input_during_keydown: Option<SmallVec<[ImeInput; 1]>>,
|
||||
last_ime_inputs: Option<SmallVec<[(String, Option<Range<usize>>); 1]>>,
|
||||
previous_keydown_inserted_text: Option<String>,
|
||||
external_files_dragged: bool,
|
||||
// Whether the next left-mouse click is also the focusing click.
|
||||
@@ -636,7 +636,7 @@ impl MacWindow {
|
||||
.as_ref()
|
||||
.and_then(|titlebar| titlebar.traffic_light_position),
|
||||
previous_modifiers_changed_event: None,
|
||||
input_during_keydown: None,
|
||||
last_ime_inputs: None,
|
||||
previous_keydown_inserted_text: None,
|
||||
external_files_dragged: false,
|
||||
first_mouse: false,
|
||||
@@ -657,7 +657,7 @@ impl MacWindow {
|
||||
.as_ref()
|
||||
.and_then(|t| t.title.as_ref().map(AsRef::as_ref))
|
||||
{
|
||||
native_window.setTitle_(NSString::alloc(nil).init_str(title));
|
||||
window.set_title(title);
|
||||
}
|
||||
|
||||
native_window.setMovable_(is_movable as BOOL);
|
||||
@@ -799,7 +799,7 @@ impl PlatformWindow for MacWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
unsafe {
|
||||
let screen = self.0.lock().native_window.screen();
|
||||
let device_description: id = msg_send![screen, deviceDescription];
|
||||
@@ -810,7 +810,7 @@ impl PlatformWindow for MacWindow {
|
||||
|
||||
let screen_number: u32 = msg_send![screen_number, unsignedIntValue];
|
||||
|
||||
Rc::new(MacDisplay(screen_number))
|
||||
Some(Rc::new(MacDisplay(screen_number)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1191,14 +1191,30 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
|
||||
}
|
||||
|
||||
// Things to test if you're modifying this method:
|
||||
// U.S. layout:
|
||||
// - The IME consumes characters like 'j' and 'k', which makes paging through `less` in
|
||||
// the terminal behave incorrectly by default. This behavior should be patched by our
|
||||
// IME integration
|
||||
// - `alt-t` should open the tasks menu
|
||||
// - In vim mode, this keybinding should work:
|
||||
// ```
|
||||
// {
|
||||
// "context": "Editor && vim_mode == insert",
|
||||
// "bindings": {"j j": "vim::NormalBefore"}
|
||||
// }
|
||||
// ```
|
||||
// and typing 'j k' in insert mode with this keybinding should insert the two characters
|
||||
// Brazilian layout:
|
||||
// - `" space` should type a quote
|
||||
// - `" space` should create an unmarked quote
|
||||
// - `" backspace` should delete the marked quote
|
||||
// - `" up` should type the quote, unmark it, and move up one line
|
||||
// - `" cmd-down` should not leave a marked quote behind (it maybe should dispatch the key though?)
|
||||
// - `" up` should insert a quote, unmark it, and move up one line
|
||||
// - `" cmd-down` should insert a quote, unmark it, and move to the end of the file
|
||||
// - NOTE: The current implementation does not move the selection to the end of the file
|
||||
// - `cmd-ctrl-space` and clicking on an emoji should type it
|
||||
// Czech (QWERTY) layout:
|
||||
// - in vim mode `option-4` should go to end of line (same as $)
|
||||
// Japanese (Romaji) layout:
|
||||
// - type `a i left down up enter enter` should create an unmarked text "愛"
|
||||
extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let mut lock = window_state.as_ref().lock();
|
||||
@@ -1228,7 +1244,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
} else {
|
||||
lock.last_fresh_keydown = Some(keydown.clone());
|
||||
}
|
||||
lock.input_during_keydown = Some(SmallVec::new());
|
||||
lock.last_ime_inputs = Some(Default::default());
|
||||
drop(lock);
|
||||
|
||||
// Send the event to the input context for IME handling, unless the `fn` modifier is
|
||||
@@ -1244,15 +1260,16 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
let mut handled = false;
|
||||
let mut lock = window_state.lock();
|
||||
let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take();
|
||||
let mut input_during_keydown = lock.input_during_keydown.take().unwrap();
|
||||
let mut last_inserts = lock.last_ime_inputs.take().unwrap();
|
||||
|
||||
let mut callback = lock.event_callback.take();
|
||||
drop(lock);
|
||||
|
||||
let last_ime = input_during_keydown.pop();
|
||||
let last_insert = last_inserts.pop();
|
||||
// on a brazilian keyboard typing `"` and then hitting `up` will cause two IME
|
||||
// events, one to unmark the quote, and one to send the up arrow.
|
||||
for ime in input_during_keydown {
|
||||
send_to_input_handler(this, ime);
|
||||
for (text, range) in last_inserts {
|
||||
send_to_input_handler(this, ImeInput::InsertText(text, range));
|
||||
}
|
||||
|
||||
let is_composing =
|
||||
@@ -1260,20 +1277,18 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
||||
.flatten()
|
||||
.is_some();
|
||||
|
||||
if let Some(ime) = last_ime {
|
||||
if let ImeInput::InsertText(text, _) = &ime {
|
||||
if !is_composing {
|
||||
window_state.lock().previous_keydown_inserted_text = Some(text.clone());
|
||||
if let Some(callback) = callback.as_mut() {
|
||||
event.keystroke.ime_key = Some(text.clone());
|
||||
handled = !callback(PlatformInput::KeyDown(event)).propagate;
|
||||
}
|
||||
if let Some((text, range)) = last_insert {
|
||||
if !is_composing {
|
||||
window_state.lock().previous_keydown_inserted_text = Some(text.clone());
|
||||
if let Some(callback) = callback.as_mut() {
|
||||
event.keystroke.ime_key = Some(text.clone());
|
||||
handled = !callback(PlatformInput::KeyDown(event)).propagate;
|
||||
}
|
||||
}
|
||||
|
||||
if !handled {
|
||||
handled = true;
|
||||
send_to_input_handler(this, ime);
|
||||
send_to_input_handler(this, ImeInput::InsertText(text, range));
|
||||
}
|
||||
} else if !is_composing {
|
||||
let is_held = event.is_held;
|
||||
@@ -1655,21 +1670,24 @@ extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
|
||||
}
|
||||
|
||||
extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL {
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.is_some() as BOOL
|
||||
let has_marked_text_result =
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten();
|
||||
|
||||
has_marked_text_result.is_some() as BOOL
|
||||
}
|
||||
|
||||
extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range())
|
||||
.flatten()
|
||||
.map_or(NSRange::invalid(), |range| range.into())
|
||||
let marked_range_result =
|
||||
with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten();
|
||||
|
||||
marked_range_result.map_or(NSRange::invalid(), |range| range.into())
|
||||
}
|
||||
|
||||
extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
|
||||
with_input_handler(this, |input_handler| input_handler.selected_text_range())
|
||||
.flatten()
|
||||
.map_or(NSRange::invalid(), |range| range.into())
|
||||
let selected_range_result =
|
||||
with_input_handler(this, |input_handler| input_handler.selected_text_range()).flatten();
|
||||
|
||||
selected_range_result.map_or(NSRange::invalid(), |range| range.into())
|
||||
}
|
||||
|
||||
extern "C" fn first_rect_for_character_range(
|
||||
@@ -1711,9 +1729,8 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
|
||||
} else {
|
||||
text
|
||||
};
|
||||
let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
let text = text.to_str();
|
||||
let replacement_range = replacement_range.to_range();
|
||||
send_to_input_handler(
|
||||
this,
|
||||
@@ -1739,9 +1756,7 @@ extern "C" fn set_marked_text(
|
||||
};
|
||||
let selected_range = selected_range.to_range();
|
||||
let replacement_range = replacement_range.to_range();
|
||||
let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.unwrap();
|
||||
let text = text.to_str();
|
||||
|
||||
send_to_input_handler(
|
||||
this,
|
||||
@@ -1765,7 +1780,7 @@ extern "C" fn attributed_substring_for_proposed_range(
|
||||
return None;
|
||||
}
|
||||
|
||||
let selected_text = input_handler.text_for_range(range)?;
|
||||
let selected_text = input_handler.text_for_range(range.clone())?;
|
||||
unsafe {
|
||||
let string: id = msg_send![class!(NSAttributedString), alloc];
|
||||
let string: id = msg_send![string, initWithString: ns_string(&selected_text)];
|
||||
@@ -1930,20 +1945,26 @@ fn send_to_input_handler(window: &Object, ime: ImeInput) {
|
||||
unsafe {
|
||||
let window_state = get_window_state(window);
|
||||
let mut lock = window_state.lock();
|
||||
if let Some(ime_input) = lock.input_during_keydown.as_mut() {
|
||||
ime_input.push(ime);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(mut input_handler) = lock.input_handler.take() {
|
||||
drop(lock);
|
||||
match ime {
|
||||
match ime.clone() {
|
||||
ImeInput::InsertText(text, range) => {
|
||||
if let Some(ime_input) = lock.last_ime_inputs.as_mut() {
|
||||
ime_input.push((text, range));
|
||||
lock.input_handler = Some(input_handler);
|
||||
return;
|
||||
}
|
||||
drop(lock);
|
||||
input_handler.replace_text_in_range(range, &text)
|
||||
}
|
||||
ImeInput::SetMarkedText(text, range, marked_range) => {
|
||||
drop(lock);
|
||||
input_handler.replace_and_mark_text_in_range(range, &text, marked_range)
|
||||
}
|
||||
ImeInput::UnmarkText => input_handler.unmark_text(),
|
||||
ImeInput::UnmarkText => {
|
||||
drop(lock);
|
||||
input_handler.unmark_text()
|
||||
}
|
||||
}
|
||||
window_state.lock().input_handler = Some(input_handler);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
mod dispatcher;
|
||||
mod display;
|
||||
mod platform;
|
||||
mod text_system;
|
||||
mod window;
|
||||
|
||||
pub(crate) use dispatcher::*;
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use crate::{
|
||||
Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, GlyphId, LineLayout, Pixels,
|
||||
PlatformTextSystem, RenderGlyphParams, Size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub(crate) struct TestTextSystem {}
|
||||
|
||||
// todo(linux)
|
||||
#[allow(unused)]
|
||||
impl PlatformTextSystem for TestTextSystem {
|
||||
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn all_font_names(&self) -> Vec<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn all_font_families(&self) -> Vec<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn font_id(&self, descriptor: &Font) -> Result<FontId> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
|
||||
unimplemented!()
|
||||
}
|
||||
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn rasterize_glyph(
|
||||
&self,
|
||||
params: &RenderGlyphParams,
|
||||
raster_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@@ -132,8 +132,8 @@ impl PlatformWindow for TestWindow {
|
||||
WindowAppearance::Light
|
||||
}
|
||||
|
||||
fn display(&self) -> std::rc::Rc<dyn crate::PlatformDisplay> {
|
||||
self.0.lock().display.clone()
|
||||
fn display(&self) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
|
||||
Some(self.0.lock().display.clone())
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
|
||||
@@ -379,8 +379,8 @@ impl PlatformWindow for WindowsWindow {
|
||||
WindowAppearance::Dark
|
||||
}
|
||||
|
||||
fn display(&self) -> Rc<dyn PlatformDisplay> {
|
||||
Rc::new(self.0.state.borrow().display)
|
||||
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
|
||||
Some(Rc::new(self.0.state.borrow().display))
|
||||
}
|
||||
|
||||
fn mouse_position(&self) -> Point<Pixels> {
|
||||
|
||||
@@ -488,7 +488,7 @@ pub struct Window {
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
pub(crate) removed: bool,
|
||||
pub(crate) platform_window: Box<dyn PlatformWindow>,
|
||||
display_id: DisplayId,
|
||||
display_id: Option<DisplayId>,
|
||||
sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||
text_system: Arc<WindowTextSystem>,
|
||||
rem_size: Pixels,
|
||||
@@ -634,7 +634,7 @@ impl Window {
|
||||
window_background,
|
||||
},
|
||||
);
|
||||
let display_id = platform_window.display().id();
|
||||
let display_id = platform_window.display().map(|display| display.id());
|
||||
let sprite_atlas = platform_window.sprite_atlas();
|
||||
let mouse_position = platform_window.mouse_position();
|
||||
let modifiers = platform_window.modifiers();
|
||||
@@ -691,8 +691,15 @@ impl Window {
|
||||
measure("frame duration", || {
|
||||
handle
|
||||
.update(&mut cx, |_, cx| {
|
||||
// let start = Instant::now();
|
||||
cx.draw();
|
||||
// let elapsed = start.elapsed();
|
||||
// eprintln!("draw duration: {:?}", elapsed);
|
||||
|
||||
// let start = Instant::now();
|
||||
cx.present();
|
||||
// let elapsed = start.elapsed();
|
||||
// eprintln!("present duration: {:?}", elapsed);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
@@ -1036,6 +1043,37 @@ impl<'a> WindowContext<'a> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Subscribe to events emitted by a model or view.
|
||||
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
||||
/// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
|
||||
pub fn observe<E, T>(
|
||||
&mut self,
|
||||
entity: &E,
|
||||
mut on_notify: impl FnMut(E, &mut WindowContext<'_>) + 'static,
|
||||
) -> Subscription
|
||||
where
|
||||
E: Entity<T>,
|
||||
{
|
||||
let entity_id = entity.entity_id();
|
||||
let entity = entity.downgrade();
|
||||
let window_handle = self.window.handle;
|
||||
self.app.new_observer(
|
||||
entity_id,
|
||||
Box::new(move |cx| {
|
||||
window_handle
|
||||
.update(cx, |_, cx| {
|
||||
if let Some(handle) = E::upgrade_from(&entity) {
|
||||
on_notify(handle, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Subscribe to events emitted by a model or view.
|
||||
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
|
||||
/// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
|
||||
@@ -1099,7 +1137,12 @@ impl<'a> WindowContext<'a> {
|
||||
fn bounds_changed(&mut self) {
|
||||
self.window.scale_factor = self.window.platform_window.scale_factor();
|
||||
self.window.viewport_size = self.window.platform_window.content_size();
|
||||
self.window.display_id = self.window.platform_window.display().id();
|
||||
self.window.display_id = self
|
||||
.window
|
||||
.platform_window
|
||||
.display()
|
||||
.map(|display| display.id());
|
||||
|
||||
self.refresh();
|
||||
|
||||
self.window
|
||||
@@ -1191,7 +1234,7 @@ impl<'a> WindowContext<'a> {
|
||||
self.platform
|
||||
.displays()
|
||||
.into_iter()
|
||||
.find(|display| display.id() == self.window.display_id)
|
||||
.find(|display| Some(display.id()) == self.window.display_id)
|
||||
}
|
||||
|
||||
/// Show the platform character palette.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user