Move all crates to a top-level crates folder
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
93
crates/zed/Cargo.toml
Normal file
|
@ -0,0 +1,93 @@
|
|||
[package]
|
||||
authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2018"
|
||||
name = "zed"
|
||||
version = "0.1.0"
|
||||
|
||||
[lib]
|
||||
name = "zed"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "Zed"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"buffer/test-support",
|
||||
"gpui/test-support",
|
||||
"rpc_client/test-support",
|
||||
"tempdir",
|
||||
"worktree/test-support",
|
||||
"zrpc/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.38"
|
||||
async-recursion = "0.3"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = { version = "0.14", features = ["async-tls"] }
|
||||
buffer = { path = "../buffer" }
|
||||
clock = { path = "../clock" }
|
||||
crossbeam-channel = "0.5.0"
|
||||
ctor = "0.1.20"
|
||||
dirs = "3.0"
|
||||
easy-parallel = "3.1.0"
|
||||
fsevent = { path = "../fsevent" }
|
||||
futures = "0.3"
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
http-auth-basic = "0.1.3"
|
||||
ignore = "0.4"
|
||||
image = "0.23"
|
||||
indexmap = "1.6.2"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
log-panics = { version = "2.0", features = ["with-backtrace"] }
|
||||
num_cpus = "1.13.0"
|
||||
parking_lot = "0.11.1"
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
rand = "0.8.3"
|
||||
rpc_client = { path = "../rpc_client" }
|
||||
rsa = "0.4"
|
||||
rust-embed = { version = "6.2", features = ["include-exclude"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1.0.64", features = ["preserve_order"] }
|
||||
serde_path_to_error = "0.1.4"
|
||||
simplelog = "0.9"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2.5"
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
surf = "2.2"
|
||||
tempdir = { version = "0.3.7", optional = true }
|
||||
thiserror = "1.0.29"
|
||||
time = { version = "0.3" }
|
||||
tiny_http = "0.8"
|
||||
toml = "0.5"
|
||||
tree-sitter = "0.19.5"
|
||||
tree-sitter-rust = "0.19.0"
|
||||
url = "2.2"
|
||||
util = { path = "../util" }
|
||||
worktree = { path = "../worktree" }
|
||||
zrpc = { path = "../zrpc" }
|
||||
|
||||
[dev-dependencies]
|
||||
cargo-bundle = "0.5.0"
|
||||
env_logger = "0.8"
|
||||
serde_json = { version = "1.0.64", features = ["preserve_order"] }
|
||||
tempdir = { version = "0.3.7" }
|
||||
unindent = "0.1.7"
|
||||
buffer = { path = "../buffer", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
rpc_client = { path = "../rpc_client", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
worktree = { path = "../worktree", features = ["test-support"] }
|
||||
zrpc = { path = "../zrpc", features = ["test-support"] }
|
||||
|
||||
[package.metadata.bundle]
|
||||
icon = ["app-icon@2x.png", "app-icon.png"]
|
||||
identifier = "dev.zed.Zed"
|
||||
name = "Zed"
|
||||
osx_minimum_system_version = "10.14"
|
BIN
crates/zed/app-icon.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
crates/zed/app-icon@2x.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
crates/zed/assets/fonts/inconsolata/Inconsolata-Bold.ttf
Normal file
BIN
crates/zed/assets/fonts/inconsolata/Inconsolata-Regular.ttf
Normal file
3
crates/zed/assets/icons/comment-16.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.01234 1.86426C4.13913 1.86426 1.00077 4.41444 1.00077 7.56176C1.00077 8.86644 1.54614 10.0613 2.45007 11.0186C2.04248 12.1006 1.19361 13.0149 1.17991 13.0251C0.998442 13.2168 0.950506 13.4976 1.05323 13.7373C1.15939 13.9769 1.39392 14.1358 1.65743 14.1358C3.34203 14.1358 4.64588 13.4305 5.46764 12.8689C6.23461 13.1168 7.11663 13.2593 8.01234 13.2593C11.8855 13.2593 15 10.7083 15 7.56176C15 4.41526 11.8855 1.86426 8.01234 1.86426ZM8.01508 11.9445C7.28235 11.9445 6.56002 11.8315 5.86811 11.6125L5.24494 11.4173L4.7108 11.7939C4.32047 12.0711 3.78276 12.3796 3.13577 12.5883C3.33778 12.2563 3.52939 11.883 3.68032 11.4858L3.97122 10.7188L3.4064 10.1198C2.91252 9.5915 2.31675 8.7177 2.31675 7.56176C2.31675 5.14443 4.87104 3.17907 8.01426 3.17907C11.1575 3.17907 13.7118 5.14443 13.7118 7.56176C13.7118 9.97909 11.1569 11.9445 8.01508 11.9445Z" fill="#7E7E83"/>
|
||||
</svg>
|
After Width: | Height: | Size: 979 B |
3
crates/zed/assets/icons/disclosure-closed.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="4" height="8" viewBox="0 0 4 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.923915 0.64914L3.7699 3.67213C3.85691 3.76411 3.9004 3.88214 3.9004 4.00028C3.9004 4.11835 3.85689 4.23635 3.7699 4.32843L0.923915 7.35142C0.742536 7.54234 0.440436 7.5503 0.249113 7.36932C0.0564376 7.18784 0.0496359 6.88444 0.230468 6.69431L2.7841 3.99948L0.230468 1.30465C0.0496359 1.11452 0.0563979 0.813217 0.249113 0.630446C0.440436 0.449663 0.742536 0.457618 0.923915 0.64914Z" fill="#66686A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 512 B |
3
crates/zed/assets/icons/disclosure-open.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="8" height="4" viewBox="0 0 8 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.35131 0.916948L4.32837 3.76288C4.23689 3.8663 4.11756 3.91005 4.00022 3.91005C3.88289 3.91005 3.76396 3.86654 3.67208 3.77955L0.649138 0.916948C0.457619 0.733981 0.449664 0.431687 0.630444 0.240765C0.812019 0.0478537 1.11531 0.0418875 1.30543 0.222866L4.00022 2.77446L6.69501 0.220877C6.88518 0.0400179 7.18723 0.0468593 7.37 0.239522C7.55019 0.431687 7.54223 0.733981 7.35131 0.916948Z" fill="#66686A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 516 B |
1
crates/zed/assets/icons/file-16.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M3.75 1.5a.25.25 0 00-.25.25v11.5c0 .138.112.25.25.25h8.5a.25.25 0 00.25-.25V6H9.75A1.75 1.75 0 018 4.25V1.5H3.75zm5.75.56v2.19c0 .138.112.25.25.25h2.19L9.5 2.06zM2 1.75C2 .784 2.784 0 3.75 0h5.086c.464 0 .909.184 1.237.513l3.414 3.414c.329.328.513.773.513 1.237v8.086A1.75 1.75 0 0112.25 15h-8.5A1.75 1.75 0 012 13.25V1.75z"/></svg>
|
After Width: | Height: | Size: 445 B |
3
crates/zed/assets/icons/folder-tree-16.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.2222 9.55561H11.8889L10.8156 8.89377C10.6931 8.81672 10.5302 8.77783 10.4062 8.77783H8.77778C8.3483 8.77783 8 9.12613 8 9.55561V13.4445C8 13.874 8.3483 14.2223 8.77778 14.2223H14.2222C14.6517 14.2223 15 13.874 15 13.4445V10.3334C15 9.90318 14.6524 9.55561 14.2222 9.55561ZM13.8333 13.0556H9.16667V9.9445H10.2969L11.2764 10.5487C11.4611 10.6615 11.6726 10.7223 11.8889 10.7223H13.8333V13.0556ZM6.63889 5.66672C6.96215 5.66672 7.22222 5.40665 7.22222 5.08339C7.22222 4.76012 6.96215 4.50005 6.63889 4.50005H2.16667V2.36117C2.16667 2.03887 1.90538 1.77783 1.58333 1.77783C1.26128 1.77783 1 2.03887 1 2.36117V11.3056C1 12.0567 1.60934 12.6667 2.36111 12.6667H6.63889C6.96215 12.6667 7.22222 12.4067 7.22222 12.0834C7.22222 11.7611 6.96094 11.5001 6.63889 11.5001H2.36111C2.25417 11.5001 2.16667 11.4125 2.16667 11.3056V5.66672H6.63889ZM14.2222 2.55561H11.8889L10.8156 1.89377C10.6931 1.81789 10.5302 1.77783 10.4062 1.77783H8.77778C8.3483 1.77783 8 2.12613 8 2.55561V6.4445C8 6.87398 8.3483 7.22228 8.77778 7.22228H14.2222C14.6517 7.22228 15 6.87398 15 6.4445V3.33339C15 2.90391 14.6524 2.55561 14.2222 2.55561ZM13.8333 6.05561H9.16667V2.9445H10.2969L11.2764 3.54873C11.4611 3.66224 11.6726 3.72228 11.8889 3.72228H13.8333V6.05561Z" fill="#7E7E83"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
3
crates/zed/assets/icons/offline-14.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.14992 9.84972C1.99189 9.84972 1.04997 8.90759 1.04997 7.74977C1.04997 6.87042 1.61368 6.07944 2.45278 5.78173L3.15692 5.53193L3.15307 4.98616L2.15704 4.18643C2.12729 4.39861 2.10016 4.59111 2.1017 4.79235C0.880227 5.22546 0 6.38043 0 7.74977C0 9.48879 1.41024 10.8997 3.14992 10.8997H10.6988L9.3592 9.84972H3.14992ZM13.0569 10.0816C13.6343 9.5391 13.9996 8.77787 13.9996 7.92477C13.9996 6.58321 13.1056 5.46171 11.8855 5.09203C11.8888 5.04391 11.8997 4.99797 11.8997 4.94985C11.8997 3.59582 10.8038 2.49991 9.44976 2.49991C9.20017 2.49991 8.96436 2.54819 8.73752 2.61753C8.06948 1.70171 6.99544 1.09995 5.77485 1.09995C4.71394 1.09995 3.76678 1.55668 3.1018 2.27679L0.849166 0.51172C0.752699 0.436537 0.638515 0.399963 0.525643 0.399963C0.369897 0.399963 0.215398 0.468999 0.112194 0.600946C-0.0669139 0.82914 -0.0272556 1.15944 0.201048 1.33816L13.1509 11.4881C13.3806 11.6676 13.71 11.6265 13.8879 11.3989C14.067 11.1705 14.0273 10.8405 13.799 10.6617L13.0569 10.0816ZM12.2169 9.42317L3.92865 2.92427C4.40114 2.44916 5.05081 2.14992 5.77485 2.14992C6.61483 2.14992 7.38547 2.54585 7.88923 3.23642L8.32979 3.84016L9.04442 3.62167C10.1132 3.29487 10.8869 4.18078 10.8373 5.03039L10.7893 5.85549L11.58 6.09589C12.3984 6.34544 12.9497 7.08042 12.9497 7.92477C12.9497 8.53288 12.6609 9.0688 12.2169 9.42317Z" fill="#B3B3B3"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
3
crates/zed/assets/icons/signed-out-12.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 3.04688C5.00332 3.04688 4.19531 3.85488 4.19531 4.85156C4.19531 5.84824 5.00332 6.65625 6 6.65625C6.99668 6.65625 7.80469 5.84824 7.80469 4.85156C7.80469 3.85488 6.99668 3.04688 6 3.04688ZM6 5.67188C5.5476 5.67188 5.17969 5.30376 5.17969 4.85156C5.17969 4.39834 5.54678 4.03125 6 4.03125C6.45322 4.03125 6.82031 4.39916 6.82031 4.85156C6.82031 5.30479 6.45322 5.67188 6 5.67188ZM6 0.75C3.1002 0.75 0.75 3.1002 0.75 6C0.75 8.8998 3.1002 11.25 6 11.25C8.8998 11.25 11.25 8.8998 11.25 6C11.25 3.1002 8.8998 0.75 6 0.75ZM6 10.2656C5.04167 10.2656 4.15922 9.94406 3.44678 9.4086C3.80156 8.72754 4.49062 8.29688 5.26582 8.29688H6.73603C7.5102 8.29688 8.19844 8.72774 8.55466 9.4086C7.8416 9.94365 6.95771 10.2656 6 10.2656ZM9.28535 8.71729C8.73164 7.85186 7.78828 7.3125 6.73418 7.3125H5.26582C4.21254 7.3125 3.26938 7.85083 2.71465 8.71688C2.1027 7.979 1.73438 7.03154 1.73438 6C1.73438 3.64775 3.64796 1.73438 6 1.73438C8.35204 1.73438 10.2656 3.64796 10.2656 6C10.2656 7.03154 9.89648 7.979 9.28535 8.71729Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
3
crates/zed/assets/icons/user-16.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.3125 9.3125H6.6875C4.02969 9.3125 1.875 11.4672 1.875 14.125C1.875 14.6082 2.26684 15 2.75 15H13.25C13.7332 15 14.125 14.6082 14.125 14.125C14.125 11.4672 11.9703 9.3125 9.3125 9.3125ZM3.21457 13.6875C3.43059 11.9621 4.90469 10.625 6.6875 10.625H9.3125C11.0942 10.625 12.5691 11.9635 12.7852 13.6875H3.21457ZM8 8C9.93293 8 11.5 6.43293 11.5 4.5C11.5 2.56707 9.93293 1 8 1C6.06707 1 4.5 2.56707 4.5 4.5C4.5 6.4332 6.0668 8 8 8ZM8 2.3125C9.20613 2.3125 10.1875 3.29387 10.1875 4.5C10.1875 5.70613 9.20613 6.6875 8 6.6875C6.79387 6.6875 5.8125 5.70586 5.8125 4.5C5.8125 3.29387 6.79414 2.3125 8 2.3125Z" fill="#9BA8BE"/>
|
||||
</svg>
|
After Width: | Height: | Size: 733 B |
3
crates/zed/assets/icons/x.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.6066 13.957C16.0751 14.4242 16.0751 15.1823 15.6066 15.6496C15.1381 16.1168 14.378 16.1168 13.9094 15.6496L8.00082 9.71303L2.0502 15.6476C1.5817 16.1148 0.821573 16.1148 0.353024 15.6476C-0.115524 15.1803 -0.115474 14.4223 0.353024 13.955L6.30564 8.02244L0.351374 2.04303C-0.117125 1.5758 -0.117125 0.817724 0.351374 0.350443C0.819872 -0.116839 1.58 -0.116789 2.04855 0.350443L8.00082 6.33185L13.9514 0.39732C14.4199 -0.0699117 15.1801 -0.0699117 15.6486 0.39732C16.1172 0.864552 16.1171 1.62263 15.6486 2.08991L9.69599 8.02244L15.6066 13.957Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 676 B |
228
crates/zed/assets/themes/_base.toml
Normal file
|
@ -0,0 +1,228 @@
|
|||
[text]
|
||||
base = { family = "Inconsolata", size = 15 }
|
||||
|
||||
[workspace]
|
||||
background = "$surface.0"
|
||||
pane_divider = { width = 1, color = "$border.0" }
|
||||
|
||||
[workspace.titlebar]
|
||||
border = { width = 1, bottom = true, color = "$border.0" }
|
||||
title = "$text.0"
|
||||
avatar_width = 20
|
||||
icon_color = "$text.2.color"
|
||||
avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } }
|
||||
outdated_warning = { extends = "$text.2", size = 13 }
|
||||
|
||||
[workspace.titlebar.offline_icon]
|
||||
padding = { right = 4 }
|
||||
width = 16
|
||||
|
||||
[workspace.tab]
|
||||
height = 34
|
||||
text = "$text.2"
|
||||
padding = { left = 12, right = 12 }
|
||||
icon_width = 8
|
||||
spacing = 10
|
||||
icon_close = "$text.2.color"
|
||||
icon_close_active = "$text.0.color"
|
||||
icon_dirty = "$status.info"
|
||||
icon_conflict = "$status.warn"
|
||||
border = { left = true, bottom = true, width = 1, color = "$border.0", overlay = true }
|
||||
|
||||
[workspace.active_tab]
|
||||
extends = "$workspace.tab"
|
||||
border.bottom = false
|
||||
background = "$surface.1"
|
||||
text = "$text.0"
|
||||
|
||||
[workspace.sidebar]
|
||||
width = 32
|
||||
border = { right = true, width = 1, color = "$border.0" }
|
||||
|
||||
[workspace.sidebar.resize_handle]
|
||||
padding = { left = 1 }
|
||||
background = "$border.0"
|
||||
|
||||
[workspace.sidebar.item]
|
||||
icon_color = "$text.2.color"
|
||||
icon_size = 18
|
||||
height = "$workspace.tab.height"
|
||||
|
||||
[workspace.sidebar.active_item]
|
||||
extends = "$workspace.sidebar.item"
|
||||
icon_color = "$text.0.color"
|
||||
|
||||
[workspace.left_sidebar]
|
||||
extends = "$workspace.sidebar"
|
||||
border = { width = 1, color = "$border.0", right = true }
|
||||
|
||||
[workspace.right_sidebar]
|
||||
extends = "$workspace.sidebar"
|
||||
border = { width = 1, color = "$border.0", left = true }
|
||||
|
||||
[panel]
|
||||
padding = { top = 12, left = 12, bottom = 12, right = 12 }
|
||||
|
||||
[chat_panel]
|
||||
extends = "$panel"
|
||||
channel_name = { extends = "$text.0", weight = "bold" }
|
||||
channel_name_hash = { text = "$text.2", padding.right = 8 }
|
||||
|
||||
[chat_panel.message]
|
||||
body = "$text.1"
|
||||
sender = { extends = "$text.0", weight = "bold", margin.right = 8 }
|
||||
timestamp = "$text.2"
|
||||
padding.bottom = 6
|
||||
|
||||
[chat_panel.pending_message]
|
||||
extends = "$chat_panel.message"
|
||||
body = { color = "$text.3.color" }
|
||||
sender = { color = "$text.3.color" }
|
||||
timestamp = { color = "$text.3.color" }
|
||||
|
||||
[chat_panel.channel_select.item]
|
||||
padding = 4
|
||||
name = "$text.1"
|
||||
hash = { extends = "$text.2", margin.right = 8 }
|
||||
|
||||
[chat_panel.channel_select.hovered_item]
|
||||
extends = "$chat_panel.channel_select.item"
|
||||
background = "$state.hover"
|
||||
corner_radius = 6
|
||||
|
||||
[chat_panel.channel_select.active_item]
|
||||
extends = "$chat_panel.channel_select.item"
|
||||
name = "$text.0"
|
||||
|
||||
[chat_panel.channel_select.hovered_active_item]
|
||||
extends = "$chat_panel.channel_select.hovered_item"
|
||||
name = "$text.0"
|
||||
|
||||
[chat_panel.channel_select.header]
|
||||
extends = "$chat_panel.channel_select.active_item"
|
||||
padding.bottom = 4
|
||||
padding.left = 0
|
||||
|
||||
[chat_panel.channel_select.menu]
|
||||
padding = 4
|
||||
corner_radius = 6
|
||||
border = { color = "$border.0", width = 1 }
|
||||
background = "$surface.0"
|
||||
shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
|
||||
|
||||
[chat_panel.input_editor]
|
||||
background = "$surface.1"
|
||||
corner_radius = 6
|
||||
padding = { left = 8, right = 8, top = 7, bottom = 7 }
|
||||
text = "$text.0"
|
||||
placeholder_text = "$text.2"
|
||||
selection = "$selection.host"
|
||||
border = { width = 1, color = "$border.0" }
|
||||
|
||||
[chat_panel.sign_in_prompt]
|
||||
extends = "$text.0"
|
||||
underline = true
|
||||
|
||||
[chat_panel.hovered_sign_in_prompt]
|
||||
extends = "$chat_panel.sign_in_prompt"
|
||||
color = "$text.1.color"
|
||||
|
||||
[people_panel]
|
||||
extends = "$panel"
|
||||
host_row_height = 28
|
||||
host_avatar = { corner_radius = 10, width = 20 }
|
||||
host_username = { extends = "$text.0", padding.left = 8 }
|
||||
tree_branch_width = 1
|
||||
tree_branch_color = "$surface.2"
|
||||
|
||||
[people_panel.worktree]
|
||||
height = 24
|
||||
padding = { left = 8 }
|
||||
guest_avatar = { corner_radius = 8, width = 16 }
|
||||
guest_avatar_spacing = 4
|
||||
|
||||
[people_panel.worktree.name]
|
||||
extends = "$text.1"
|
||||
margin = { right = 6 }
|
||||
|
||||
[people_panel.unshared_worktree]
|
||||
extends = "$people_panel.worktree"
|
||||
|
||||
[people_panel.hovered_unshared_worktree]
|
||||
extends = "$people_panel.unshared_worktree"
|
||||
background = "$state.hover"
|
||||
corner_radius = 6
|
||||
|
||||
[people_panel.shared_worktree]
|
||||
extends = "$people_panel.worktree"
|
||||
name.color = "$text.0.color"
|
||||
|
||||
[people_panel.hovered_shared_worktree]
|
||||
extends = "$people_panel.shared_worktree"
|
||||
background = "$state.hover"
|
||||
corner_radius = 6
|
||||
|
||||
[project_panel]
|
||||
extends = "$panel"
|
||||
padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
|
||||
|
||||
[project_panel.entry]
|
||||
text = "$text.1"
|
||||
height = 22
|
||||
icon_color = "$text.3.color"
|
||||
icon_size = 8
|
||||
icon_spacing = 8
|
||||
|
||||
[project_panel.hovered_entry]
|
||||
extends = "$project_panel.entry"
|
||||
background = "$state.hover"
|
||||
|
||||
[project_panel.selected_entry]
|
||||
extends = "$project_panel.entry"
|
||||
text = { extends = "$text.0" }
|
||||
|
||||
[project_panel.hovered_selected_entry]
|
||||
extends = "$project_panel.hovered_entry"
|
||||
text = { extends = "$text.0" }
|
||||
|
||||
[selector]
|
||||
background = "$surface.0"
|
||||
padding = 8
|
||||
margin.top = 52
|
||||
corner_radius = 6
|
||||
shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
|
||||
border = { width = 1, color = "$border.0" }
|
||||
|
||||
[selector.input_editor]
|
||||
background = "$surface.1"
|
||||
corner_radius = 6
|
||||
padding = { left = 16, right = 16, top = 7, bottom = 7 }
|
||||
text = "$text.0"
|
||||
placeholder_text = "$text.2"
|
||||
selection = "$selection.host"
|
||||
border = { width = 1, color = "$border.0" }
|
||||
|
||||
[selector.empty]
|
||||
text = "$text.2"
|
||||
padding = { left = 16, right = 16, top = 8, bottom = 4 }
|
||||
|
||||
[selector.item]
|
||||
text = "$text.1"
|
||||
highlight_text = { extends = "$text.base", color = "$syntax.keyword.color", weight = "$syntax.keyword.weight" }
|
||||
padding = { left = 16, right = 16, top = 4, bottom = 4 }
|
||||
corner_radius = 6
|
||||
|
||||
[selector.active_item]
|
||||
extends = "$selector.item"
|
||||
background = "$state.hover"
|
||||
text = "$text.0"
|
||||
|
||||
[editor]
|
||||
text = "$text.1"
|
||||
background = "$surface.1"
|
||||
gutter_background = "$surface.1"
|
||||
active_line_background = "$state.active_line"
|
||||
line_number = "$text.2.color"
|
||||
line_number_active = "$text.0.color"
|
||||
selection = "$selection.host"
|
||||
guest_selections = "$selection.guests"
|
51
crates/zed/assets/themes/black.toml
Normal file
|
@ -0,0 +1,51 @@
|
|||
extends = "_base"
|
||||
|
||||
[surface]
|
||||
0 = "#222324"
|
||||
1 = "#141516"
|
||||
2 = "#131415"
|
||||
|
||||
[border]
|
||||
0 = "#0F1011"
|
||||
|
||||
[text]
|
||||
0 = { extends = "$text.base", color = "#ffffff" }
|
||||
1 = { extends = "$text.base", color = "#b3b3b3" }
|
||||
2 = { extends = "$text.base", color = "#7b7d80" }
|
||||
3 = { extends = "$text.base", color = "#66686A" }
|
||||
|
||||
[shadow]
|
||||
0 = "#00000052"
|
||||
|
||||
[selection]
|
||||
host = { selection = "#3B57BC33", cursor = "$text.0.color" }
|
||||
guests = [
|
||||
{ selection = "#FDF35133", cursor = "#FDF351" },
|
||||
{ selection = "#4EACAD33", cursor = "#4EACAD" },
|
||||
{ selection = "#D0453B33", cursor = "#D0453B" },
|
||||
{ selection = "#3B874B33", cursor = "#3B874B" },
|
||||
{ selection = "#BD7CB433", cursor = "#BD7CB4" },
|
||||
{ selection = "#EE823133", cursor = "#EE8231" },
|
||||
{ selection = "#5A2B9233", cursor = "#5A2B92" }
|
||||
]
|
||||
|
||||
[status]
|
||||
good = "#4fac63"
|
||||
info = "#3c5dd4"
|
||||
warn = "#faca50"
|
||||
bad = "#b7372e"
|
||||
|
||||
[state]
|
||||
active_line = "#00000033"
|
||||
hover = "#00000033"
|
||||
|
||||
[syntax]
|
||||
keyword = { color = "#0086c0", weight = "bold" }
|
||||
function = "#dcdcaa"
|
||||
string = "#cb8f77"
|
||||
type = "#4ec9b0"
|
||||
number = "#b5cea8"
|
||||
comment = "#6a9955"
|
||||
property = "#4e94ce"
|
||||
variant = "#4fc1ff"
|
||||
constant = "#9cdcfe"
|
51
crates/zed/assets/themes/dark.toml
Normal file
|
@ -0,0 +1,51 @@
|
|||
extends = "_base"
|
||||
|
||||
[surface]
|
||||
0 = "#283340"
|
||||
1 = "#1C2733"
|
||||
2 = "#1C2733"
|
||||
|
||||
[border]
|
||||
0 = "#1B222B"
|
||||
|
||||
[text]
|
||||
0 = { extends = "$text.base", color = "#FFFFFF" }
|
||||
1 = { extends = "$text.base", color = "#CDD1E2" }
|
||||
2 = { extends = "$text.base", color = "#9BA8BE" }
|
||||
3 = { extends = "$text.base", color = "#6E7483" }
|
||||
|
||||
[shadow]
|
||||
0 = "#00000052"
|
||||
|
||||
[selection]
|
||||
host = { selection = "#3B57BC33", cursor = "$text.0.color" }
|
||||
guests = [
|
||||
{ selection = "#FDF35133", cursor = "#FDF351" },
|
||||
{ selection = "#4EACAD33", cursor = "#4EACAD" },
|
||||
{ selection = "#D0453B33", cursor = "#D0453B" },
|
||||
{ selection = "#3B874B33", cursor = "#3B874B" },
|
||||
{ selection = "#BD7CB433", cursor = "#BD7CB4" },
|
||||
{ selection = "#EE823133", cursor = "#EE8231" },
|
||||
{ selection = "#5A2B9233", cursor = "#5A2B92" }
|
||||
]
|
||||
|
||||
[status]
|
||||
good = "#4fac63"
|
||||
info = "#3c5dd4"
|
||||
warn = "#faca50"
|
||||
bad = "#b7372e"
|
||||
|
||||
[state]
|
||||
active_line = "#00000022"
|
||||
hover = "#00000033"
|
||||
|
||||
[syntax]
|
||||
keyword = { color = "#0086c0", weight = "bold" }
|
||||
function = "#dcdcaa"
|
||||
string = "#cb8f77"
|
||||
type = "#4ec9b0"
|
||||
number = "#b5cea8"
|
||||
comment = "#6a9955"
|
||||
property = "#4e94ce"
|
||||
variant = "#4fc1ff"
|
||||
constant = "#9cdcfe"
|
51
crates/zed/assets/themes/light.toml
Normal file
|
@ -0,0 +1,51 @@
|
|||
extends = "_base"
|
||||
|
||||
[surface]
|
||||
0 = "#EAEAEB"
|
||||
1 = "#FAFAFA"
|
||||
2 = "#FFFFFF"
|
||||
|
||||
[border]
|
||||
0 = "#DDDDDC"
|
||||
|
||||
[text]
|
||||
0 = { extends = "$text.base", color = "#000000" }
|
||||
1 = { extends = "$text.base", color = "#29292B" }
|
||||
2 = { extends = "$text.base", color = "#7E7E83" }
|
||||
3 = { extends = "$text.base", color = "#939393" }
|
||||
|
||||
[shadow]
|
||||
0 = "#0000000D"
|
||||
|
||||
[selection]
|
||||
host = { selection = "#3B57BC33", cursor = "$text.0.color" }
|
||||
guests = [
|
||||
{ selection = "#FDF35133", cursor = "#FDF351" },
|
||||
{ selection = "#4EACAD33", cursor = "#4EACAD" },
|
||||
{ selection = "#D0453B33", cursor = "#D0453B" },
|
||||
{ selection = "#3B874B33", cursor = "#3B874B" },
|
||||
{ selection = "#BD7CB433", cursor = "#BD7CB4" },
|
||||
{ selection = "#EE823133", cursor = "#EE8231" },
|
||||
{ selection = "#5A2B9233", cursor = "#5A2B92" }
|
||||
]
|
||||
|
||||
[status]
|
||||
good = "#4fac63"
|
||||
info = "#3c5dd4"
|
||||
warn = "#faca50"
|
||||
bad = "#b7372e"
|
||||
|
||||
[state]
|
||||
active_line = "#00000008"
|
||||
hover = "#0000000D"
|
||||
|
||||
[syntax]
|
||||
keyword = { color = "#0000fa", weight = "bold" }
|
||||
function = "#795e26"
|
||||
string = "#a82121"
|
||||
type = "#267f29"
|
||||
number = "#b5cea8"
|
||||
comment = "#6a9955"
|
||||
property = "#4e94ce"
|
||||
variant = "#4fc1ff"
|
||||
constant = "#9cdcfe"
|
3
crates/zed/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.14");
|
||||
}
|
6
crates/zed/languages/rust/brackets.scm
Normal file
|
@ -0,0 +1,6 @@
|
|||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)
|
||||
(closure_parameters "|" @open "|" @close)
|
8
crates/zed/languages/rust/config.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
name = "Rust"
|
||||
path_suffixes = ["rs"]
|
||||
bracket_pairs = [
|
||||
{ start = "{", end = "}" },
|
||||
{ start = "[", end = "]" },
|
||||
{ start = "(", end = ")" },
|
||||
{ start = "<", end = ">" },
|
||||
]
|
83
crates/zed/languages/rust/highlights.scm
Normal file
|
@ -0,0 +1,83 @@
|
|||
(type_identifier) @type
|
||||
(primitive_type) @type.builtin
|
||||
|
||||
(field_identifier) @property
|
||||
|
||||
(call_expression
|
||||
function: [
|
||||
(identifier) @function
|
||||
(scoped_identifier
|
||||
name: (identifier) @function)
|
||||
(field_expression
|
||||
field: (field_identifier) @function.method)
|
||||
])
|
||||
|
||||
(function_item name: (identifier) @function.definition)
|
||||
(function_signature_item name: (identifier) @function.definition)
|
||||
|
||||
; Identifier conventions
|
||||
|
||||
; Assume uppercase names are enum constructors
|
||||
((identifier) @variant
|
||||
(#match? @variant "^[A-Z]"))
|
||||
|
||||
; Assume that uppercase names in paths are types
|
||||
((scoped_identifier
|
||||
path: (identifier) @type)
|
||||
(#match? @type "^[A-Z]"))
|
||||
((scoped_identifier
|
||||
path: (scoped_identifier
|
||||
name: (identifier) @type))
|
||||
(#match? @type "^[A-Z]"))
|
||||
|
||||
; Assume all-caps names are constants
|
||||
((identifier) @constant
|
||||
(#match? @constant "^[A-Z][A-Z\\d_]+$"))
|
||||
|
||||
[
|
||||
"as"
|
||||
"async"
|
||||
"break"
|
||||
"const"
|
||||
"continue"
|
||||
"default"
|
||||
"dyn"
|
||||
"else"
|
||||
"enum"
|
||||
"extern"
|
||||
"for"
|
||||
"fn"
|
||||
"if"
|
||||
"in"
|
||||
"impl"
|
||||
"let"
|
||||
"loop"
|
||||
"macro_rules!"
|
||||
"match"
|
||||
"mod"
|
||||
"move"
|
||||
"pub"
|
||||
"return"
|
||||
"static"
|
||||
"struct"
|
||||
"trait"
|
||||
"type"
|
||||
"use"
|
||||
"where"
|
||||
"while"
|
||||
"union"
|
||||
"unsafe"
|
||||
(mutable_specifier)
|
||||
(super)
|
||||
] @keyword
|
||||
|
||||
[
|
||||
(string_literal)
|
||||
(raw_string_literal)
|
||||
(char_literal)
|
||||
] @string
|
||||
|
||||
[
|
||||
(line_comment)
|
||||
(block_comment)
|
||||
] @comment
|
20
crates/zed/src/assets.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use gpui::AssetSource;
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "assets"]
|
||||
#[exclude = "*.DS_Store"]
|
||||
pub struct Assets;
|
||||
|
||||
impl AssetSource for Assets {
|
||||
fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
|
||||
Self::get(path)
|
||||
.map(|f| f.data)
|
||||
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
|
||||
Self::iter().filter(|p| p.starts_with(path)).collect()
|
||||
}
|
||||
}
|
820
crates/zed/src/channel.rs
Normal file
|
@ -0,0 +1,820 @@
|
|||
use crate::user::{User, UserStore};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use gpui::{
|
||||
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
|
||||
};
|
||||
use postage::prelude::Stream;
|
||||
use rand::prelude::*;
|
||||
use rpc_client as rpc;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
mem,
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::{self, Bias, SumTree};
|
||||
use time::OffsetDateTime;
|
||||
use util::{post_inc, TryFutureExt};
|
||||
use zrpc::{
|
||||
proto::{self, ChannelMessageSent},
|
||||
TypedEnvelope,
|
||||
};
|
||||
|
||||
pub struct ChannelList {
|
||||
available_channels: Option<Vec<ChannelDetails>>,
|
||||
channels: HashMap<u64, WeakModelHandle<Channel>>,
|
||||
rpc: Arc<rpc::Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
_task: Task<Option<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ChannelDetails {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct Channel {
|
||||
details: ChannelDetails,
|
||||
messages: SumTree<ChannelMessage>,
|
||||
loaded_all_messages: bool,
|
||||
next_pending_message_id: usize,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<rpc::Client>,
|
||||
rng: StdRng,
|
||||
_subscription: rpc::Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChannelMessage {
|
||||
pub id: ChannelMessageId,
|
||||
pub body: String,
|
||||
pub timestamp: OffsetDateTime,
|
||||
pub sender: Arc<User>,
|
||||
pub nonce: u128,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ChannelMessageId {
|
||||
Saved(u64),
|
||||
Pending(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ChannelMessageSummary {
|
||||
max_id: ChannelMessageId,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct Count(usize);
|
||||
|
||||
pub enum ChannelListEvent {}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ChannelEvent {
|
||||
MessagesUpdated {
|
||||
old_range: Range<usize>,
|
||||
new_count: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Entity for ChannelList {
|
||||
type Event = ChannelListEvent;
|
||||
}
|
||||
|
||||
impl ChannelList {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<rpc::Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let _task = cx.spawn_weak(|this, mut cx| {
|
||||
let rpc = rpc.clone();
|
||||
async move {
|
||||
let mut status = rpc.status();
|
||||
while let Some((status, this)) = status.recv().await.zip(this.upgrade(&cx)) {
|
||||
match status {
|
||||
rpc::Status::Connected { .. } => {
|
||||
let response = rpc
|
||||
.request(proto::GetChannels {})
|
||||
.await
|
||||
.context("failed to fetch available channels")?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.available_channels =
|
||||
Some(response.channels.into_iter().map(Into::into).collect());
|
||||
|
||||
let mut to_remove = Vec::new();
|
||||
for (channel_id, channel) in &this.channels {
|
||||
if let Some(channel) = channel.upgrade(cx) {
|
||||
channel.update(cx, |channel, cx| channel.rejoin(cx))
|
||||
} else {
|
||||
to_remove.push(*channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
for channel_id in to_remove {
|
||||
this.channels.remove(&channel_id);
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
rpc::Status::SignedOut { .. } => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.available_channels = None;
|
||||
this.channels.clear();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
Self {
|
||||
available_channels: None,
|
||||
channels: Default::default(),
|
||||
user_store,
|
||||
rpc,
|
||||
_task,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available_channels(&self) -> Option<&[ChannelDetails]> {
|
||||
self.available_channels.as_ref().map(Vec::as_slice)
|
||||
}
|
||||
|
||||
pub fn get_channel(
|
||||
&mut self,
|
||||
id: u64,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<ModelHandle<Channel>> {
|
||||
if let Some(channel) = self.channels.get(&id).and_then(|c| c.upgrade(cx)) {
|
||||
return Some(channel);
|
||||
}
|
||||
|
||||
let channels = self.available_channels.as_ref()?;
|
||||
let details = channels.iter().find(|details| details.id == id)?.clone();
|
||||
let channel =
|
||||
cx.add_model(|cx| Channel::new(details, self.user_store.clone(), self.rpc.clone(), cx));
|
||||
self.channels.insert(id, channel.downgrade());
|
||||
Some(channel)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Channel {
|
||||
type Event = ChannelEvent;
|
||||
|
||||
fn release(&mut self, cx: &mut MutableAppContext) {
|
||||
let rpc = self.rpc.clone();
|
||||
let channel_id = self.details.id;
|
||||
cx.foreground()
|
||||
.spawn(async move {
|
||||
if let Err(error) = rpc.send(proto::LeaveChannel { channel_id }).await {
|
||||
log::error!("error leaving channel: {}", error);
|
||||
};
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn new(
|
||||
details: ChannelDetails,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<rpc::Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let _subscription = rpc.subscribe_to_entity(details.id, cx, Self::handle_message_sent);
|
||||
|
||||
{
|
||||
let user_store = user_store.clone();
|
||||
let rpc = rpc.clone();
|
||||
let channel_id = details.id;
|
||||
cx.spawn(|channel, mut cx| {
|
||||
async move {
|
||||
let response = rpc.request(proto::JoinChannel { channel_id }).await?;
|
||||
let messages =
|
||||
messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
let loaded_all_messages = response.done;
|
||||
|
||||
channel.update(&mut cx, |channel, cx| {
|
||||
channel.insert_messages(messages, cx);
|
||||
channel.loaded_all_messages = loaded_all_messages;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
Self {
|
||||
details,
|
||||
user_store,
|
||||
rpc,
|
||||
messages: Default::default(),
|
||||
loaded_all_messages: false,
|
||||
next_pending_message_id: 0,
|
||||
rng: StdRng::from_entropy(),
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.details.name
|
||||
}
|
||||
|
||||
pub fn send_message(
|
||||
&mut self,
|
||||
body: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<Task<Result<()>>> {
|
||||
if body.is_empty() {
|
||||
Err(anyhow!("message body can't be empty"))?;
|
||||
}
|
||||
|
||||
let current_user = self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.ok_or_else(|| anyhow!("current_user is not present"))?;
|
||||
|
||||
let channel_id = self.details.id;
|
||||
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
|
||||
let nonce = self.rng.gen();
|
||||
self.insert_messages(
|
||||
SumTree::from_item(
|
||||
ChannelMessage {
|
||||
id: pending_id,
|
||||
body: body.clone(),
|
||||
sender: current_user,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
nonce,
|
||||
},
|
||||
&(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
Ok(cx.spawn(|this, mut cx| async move {
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id,
|
||||
body,
|
||||
nonce: Some(nonce.into()),
|
||||
});
|
||||
let response = request.await?;
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
Ok(())
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> bool {
|
||||
if !self.loaded_all_messages {
|
||||
let rpc = self.rpc.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let channel_id = self.details.id;
|
||||
if let Some(before_message_id) =
|
||||
self.messages.first().and_then(|message| match message.id {
|
||||
ChannelMessageId::Saved(id) => Some(id),
|
||||
ChannelMessageId::Pending(_) => None,
|
||||
})
|
||||
{
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = rpc
|
||||
.request(proto::GetChannelMessages {
|
||||
channel_id,
|
||||
before_message_id,
|
||||
})
|
||||
.await?;
|
||||
let loaded_all_messages = response.done;
|
||||
let messages =
|
||||
messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
this.insert_messages(messages, cx);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let channel_id = self.details.id;
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = rpc.request(proto::JoinChannel { channel_id }).await?;
|
||||
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
let loaded_all_messages = response.done;
|
||||
|
||||
let pending_messages = this.update(&mut cx, |this, cx| {
|
||||
if let Some((first_new_message, last_old_message)) =
|
||||
messages.first().zip(this.messages.last())
|
||||
{
|
||||
if first_new_message.id > last_old_message.id {
|
||||
let old_messages = mem::take(&mut this.messages);
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..old_messages.summary().count,
|
||||
new_count: 0,
|
||||
});
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
}
|
||||
}
|
||||
|
||||
this.insert_messages(messages, cx);
|
||||
if loaded_all_messages {
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
}
|
||||
|
||||
this.pending_messages().cloned().collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
for pending_message in pending_messages {
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id,
|
||||
body: pending_message.body,
|
||||
nonce: Some(pending_message.nonce.into()),
|
||||
});
|
||||
let response = request.await?;
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn message_count(&self) -> usize {
|
||||
self.messages.summary().count
|
||||
}
|
||||
|
||||
pub fn messages(&self) -> &SumTree<ChannelMessage> {
|
||||
&self.messages
|
||||
}
|
||||
|
||||
pub fn message(&self, ix: usize) -> &ChannelMessage {
|
||||
let mut cursor = self.messages.cursor::<Count>();
|
||||
cursor.seek(&Count(ix), Bias::Right, &());
|
||||
cursor.item().unwrap()
|
||||
}
|
||||
|
||||
pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
|
||||
let mut cursor = self.messages.cursor::<Count>();
|
||||
cursor.seek(&Count(range.start), Bias::Right, &());
|
||||
cursor.take(range.len())
|
||||
}
|
||||
|
||||
pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
|
||||
let mut cursor = self.messages.cursor::<ChannelMessageId>();
|
||||
cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
|
||||
cursor
|
||||
}
|
||||
|
||||
fn handle_message_sent(
|
||||
&mut self,
|
||||
message: TypedEnvelope<ChannelMessageSent>,
|
||||
_: Arc<rpc::Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
let user_store = self.user_store.clone();
|
||||
let message = message
|
||||
.payload
|
||||
.message
|
||||
.ok_or_else(|| anyhow!("empty message"))?;
|
||||
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
|
||||
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
|
||||
let nonces = messages
|
||||
.cursor::<()>()
|
||||
.map(|m| m.nonce)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>();
|
||||
let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &());
|
||||
let start_ix = old_cursor.start().1 .0;
|
||||
let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &());
|
||||
let removed_count = removed_messages.summary().count;
|
||||
let new_count = messages.summary().count;
|
||||
let end_ix = start_ix + removed_count;
|
||||
|
||||
new_messages.push_tree(messages, &());
|
||||
|
||||
let mut ranges = Vec::<Range<usize>>::new();
|
||||
if new_messages.last().unwrap().is_pending() {
|
||||
new_messages.push_tree(old_cursor.suffix(&()), &());
|
||||
} else {
|
||||
new_messages.push_tree(
|
||||
old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()),
|
||||
&(),
|
||||
);
|
||||
|
||||
while let Some(message) = old_cursor.item() {
|
||||
let message_ix = old_cursor.start().1 .0;
|
||||
if nonces.contains(&message.nonce) {
|
||||
if ranges.last().map_or(false, |r| r.end == message_ix) {
|
||||
ranges.last_mut().unwrap().end += 1;
|
||||
} else {
|
||||
ranges.push(message_ix..message_ix + 1);
|
||||
}
|
||||
} else {
|
||||
new_messages.push(message.clone(), &());
|
||||
}
|
||||
old_cursor.next(&());
|
||||
}
|
||||
}
|
||||
|
||||
drop(old_cursor);
|
||||
self.messages = new_messages;
|
||||
|
||||
for range in ranges.into_iter().rev() {
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: range,
|
||||
new_count: 0,
|
||||
});
|
||||
}
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: start_ix..end_ix,
|
||||
new_count,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn messages_from_proto(
|
||||
proto_messages: Vec<proto::ChannelMessage>,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<SumTree<ChannelMessage>> {
|
||||
let unique_user_ids = proto_messages
|
||||
.iter()
|
||||
.map(|m| m.sender_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.load_users(unique_user_ids, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut messages = Vec::with_capacity(proto_messages.len());
|
||||
for message in proto_messages {
|
||||
messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
|
||||
}
|
||||
let mut result = SumTree::new();
|
||||
result.extend(messages, &());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
impl From<proto::Channel> for ChannelDetails {
|
||||
fn from(message: proto::Channel) -> Self {
|
||||
Self {
|
||||
id: message.id,
|
||||
name: message.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelMessage {
|
||||
pub async fn from_proto(
|
||||
message: proto::ChannelMessage,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let sender = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.fetch_user(message.sender_id, cx)
|
||||
})
|
||||
.await?;
|
||||
Ok(ChannelMessage {
|
||||
id: ChannelMessageId::Saved(message.id),
|
||||
body: message.body,
|
||||
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
|
||||
sender,
|
||||
nonce: message
|
||||
.nonce
|
||||
.ok_or_else(|| anyhow!("nonce is required"))?
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_pending(&self) -> bool {
|
||||
matches!(self.id, ChannelMessageId::Pending(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for ChannelMessage {
|
||||
type Summary = ChannelMessageSummary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
ChannelMessageSummary {
|
||||
max_id: self.id,
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChannelMessageId {
|
||||
fn default() -> Self {
|
||||
Self::Saved(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for ChannelMessageSummary {
|
||||
type Context = ();
|
||||
|
||||
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||
self.max_id = summary.max_id;
|
||||
self.count += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId {
|
||||
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||
debug_assert!(summary.max_id > *self);
|
||||
*self = summary.max_id;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
|
||||
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||
self.0 += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::FakeHttpClient;
|
||||
use gpui::TestAppContext;
|
||||
use rpc_client::test::FakeServer;
|
||||
use surf::http::Response;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_messages(mut cx: TestAppContext) {
|
||||
let user_id = 5;
|
||||
let mut client = rpc::Client::new();
|
||||
let http_client = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) });
|
||||
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||
|
||||
let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx));
|
||||
channel_list.read_with(&cx, |list, _| assert_eq!(list.available_channels(), None));
|
||||
|
||||
// Get the available channels.
|
||||
let get_channels = server.receive::<proto::GetChannels>().await.unwrap();
|
||||
server
|
||||
.respond(
|
||||
get_channels.receipt(),
|
||||
proto::GetChannelsResponse {
|
||||
channels: vec![proto::Channel {
|
||||
id: 5,
|
||||
name: "the-channel".to_string(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
channel_list.next_notification(&cx).await;
|
||||
channel_list.read_with(&cx, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: 5,
|
||||
name: "the-channel".into(),
|
||||
}]
|
||||
)
|
||||
});
|
||||
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
assert_eq!(get_users.payload.user_ids, vec![5]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::GetUsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 5,
|
||||
github_login: "nathansobo".into(),
|
||||
avatar_url: "http://avatar.com/nathansobo".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Join a channel and populate its existing messages.
|
||||
let channel = channel_list
|
||||
.update(&mut cx, |list, cx| {
|
||||
let channel_id = list.available_channels().unwrap()[0].id;
|
||||
list.get_channel(channel_id, cx)
|
||||
})
|
||||
.unwrap();
|
||||
channel.read_with(&cx, |channel, _| assert!(channel.messages().is_empty()));
|
||||
let join_channel = server.receive::<proto::JoinChannel>().await.unwrap();
|
||||
server
|
||||
.respond(
|
||||
join_channel.receipt(),
|
||||
proto::JoinChannelResponse {
|
||||
messages: vec![
|
||||
proto::ChannelMessage {
|
||||
id: 10,
|
||||
body: "a".into(),
|
||||
timestamp: 1000,
|
||||
sender_id: 5,
|
||||
nonce: Some(1.into()),
|
||||
},
|
||||
proto::ChannelMessage {
|
||||
id: 11,
|
||||
body: "b".into(),
|
||||
timestamp: 1001,
|
||||
sender_id: 6,
|
||||
nonce: Some(2.into()),
|
||||
},
|
||||
],
|
||||
done: false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Client requests all users for the received messages
|
||||
let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
get_users.payload.user_ids.sort();
|
||||
assert_eq!(get_users.payload.user_ids, vec![6]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::GetUsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 6,
|
||||
github_login: "maxbrunsfeld".into(),
|
||||
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(&cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..0,
|
||||
new_count: 2,
|
||||
}
|
||||
);
|
||||
channel.read_with(&cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("nathansobo".into(), "a".into()),
|
||||
("maxbrunsfeld".into(), "b".into())
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Receive a new message.
|
||||
server
|
||||
.send(proto::ChannelMessageSent {
|
||||
channel_id: channel.read_with(&cx, |channel, _| channel.details.id),
|
||||
message: Some(proto::ChannelMessage {
|
||||
id: 12,
|
||||
body: "c".into(),
|
||||
timestamp: 1002,
|
||||
sender_id: 7,
|
||||
nonce: Some(3.into()),
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
|
||||
// Client requests user for message since they haven't seen them yet
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
assert_eq!(get_users.payload.user_ids, vec![7]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::GetUsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 7,
|
||||
github_login: "as-cii".into(),
|
||||
avatar_url: "http://avatar.com/as-cii".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(&cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 2..2,
|
||||
new_count: 1,
|
||||
}
|
||||
);
|
||||
channel.read_with(&cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(2..3)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[("as-cii".into(), "c".into())]
|
||||
)
|
||||
});
|
||||
|
||||
// Scroll up to view older messages.
|
||||
channel.update(&mut cx, |channel, cx| {
|
||||
assert!(channel.load_more_messages(cx));
|
||||
});
|
||||
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
|
||||
assert_eq!(get_messages.payload.channel_id, 5);
|
||||
assert_eq!(get_messages.payload.before_message_id, 10);
|
||||
server
|
||||
.respond(
|
||||
get_messages.receipt(),
|
||||
proto::GetChannelMessagesResponse {
|
||||
done: true,
|
||||
messages: vec![
|
||||
proto::ChannelMessage {
|
||||
id: 8,
|
||||
body: "y".into(),
|
||||
timestamp: 998,
|
||||
sender_id: 5,
|
||||
nonce: Some(4.into()),
|
||||
},
|
||||
proto::ChannelMessage {
|
||||
id: 9,
|
||||
body: "z".into(),
|
||||
timestamp: 999,
|
||||
sender_id: 6,
|
||||
nonce: Some(5.into()),
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(&cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..0,
|
||||
new_count: 2,
|
||||
}
|
||||
);
|
||||
channel.read_with(&cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("nathansobo".into(), "y".into()),
|
||||
("maxbrunsfeld".into(), "z".into())
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
437
crates/zed/src/chat_panel.rs
Normal file
|
@ -0,0 +1,437 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
|
||||
editor::Editor,
|
||||
theme, Settings,
|
||||
};
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
keymap::Binding,
|
||||
platform::CursorStyle,
|
||||
views::{ItemType, Select, SelectStyle},
|
||||
AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
use postage::{prelude::Stream, watch};
|
||||
use rpc_client as rpc;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
const MESSAGE_LOADING_THRESHOLD: usize = 50;
|
||||
|
||||
pub struct ChatPanel {
|
||||
rpc: Arc<rpc::Client>,
|
||||
channel_list: ModelHandle<ChannelList>,
|
||||
active_channel: Option<(ModelHandle<Channel>, Subscription)>,
|
||||
message_list: ListState,
|
||||
input_editor: ViewHandle<Editor>,
|
||||
channel_select: ViewHandle<Select>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
local_timezone: UtcOffset,
|
||||
_observe_status: Task<()>,
|
||||
}
|
||||
|
||||
pub enum Event {}
|
||||
|
||||
action!(Send);
|
||||
action!(LoadMoreMessages);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ChatPanel::send);
|
||||
cx.add_action(ChatPanel::load_more_messages);
|
||||
|
||||
cx.add_bindings(vec![Binding::new("enter", Send, Some("ChatPanel"))]);
|
||||
}
|
||||
|
||||
impl ChatPanel {
|
||||
pub fn new(
|
||||
rpc: Arc<rpc::Client>,
|
||||
channel_list: ModelHandle<ChannelList>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let input_editor = cx.add_view(|cx| {
|
||||
Editor::auto_height(
|
||||
4,
|
||||
settings.clone(),
|
||||
{
|
||||
let settings = settings.clone();
|
||||
move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let channel_select = cx.add_view(|cx| {
|
||||
let channel_list = channel_list.clone();
|
||||
Select::new(0, cx, {
|
||||
let settings = settings.clone();
|
||||
move |ix, item_type, is_hovered, cx| {
|
||||
Self::render_channel_name(
|
||||
&channel_list,
|
||||
ix,
|
||||
item_type,
|
||||
is_hovered,
|
||||
&settings.borrow().theme.chat_panel.channel_select,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.with_style({
|
||||
let settings = settings.clone();
|
||||
move |_| {
|
||||
let theme = &settings.borrow().theme.chat_panel.channel_select;
|
||||
SelectStyle {
|
||||
header: theme.header.container.clone(),
|
||||
menu: theme.menu.clone(),
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let mut message_list = ListState::new(0, Orientation::Bottom, 1000., {
|
||||
let this = cx.handle().downgrade();
|
||||
move |ix, cx| {
|
||||
let this = this.upgrade(cx).unwrap().read(cx);
|
||||
let message = this.active_channel.as_ref().unwrap().0.read(cx).message(ix);
|
||||
this.render_message(message)
|
||||
}
|
||||
});
|
||||
message_list.set_scroll_handler(|visible_range, cx| {
|
||||
if visible_range.start < MESSAGE_LOADING_THRESHOLD {
|
||||
cx.dispatch_action(LoadMoreMessages);
|
||||
}
|
||||
});
|
||||
let _observe_status = cx.spawn(|this, mut cx| {
|
||||
let mut status = rpc.status();
|
||||
async move {
|
||||
while let Some(_) = status.recv().await {
|
||||
this.update(&mut cx, |_, cx| cx.notify());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
rpc,
|
||||
channel_list,
|
||||
active_channel: Default::default(),
|
||||
message_list,
|
||||
input_editor,
|
||||
channel_select,
|
||||
settings,
|
||||
local_timezone: cx.platform().local_timezone(),
|
||||
_observe_status,
|
||||
};
|
||||
|
||||
this.init_active_channel(cx);
|
||||
cx.observe(&this.channel_list, |this, _, cx| {
|
||||
this.init_active_channel(cx);
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&this.channel_select, |this, channel_select, cx| {
|
||||
let selected_ix = channel_select.read(cx).selected_index();
|
||||
let selected_channel = this.channel_list.update(cx, |channel_list, cx| {
|
||||
let available_channels = channel_list.available_channels()?;
|
||||
let channel_id = available_channels.get(selected_ix)?.id;
|
||||
channel_list.get_channel(channel_id, cx)
|
||||
});
|
||||
if let Some(selected_channel) = selected_channel {
|
||||
this.set_active_channel(selected_channel, cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let (active_channel, channel_count) = self.channel_list.update(cx, |list, cx| {
|
||||
let channel_count;
|
||||
let mut active_channel = None;
|
||||
|
||||
if let Some(available_channels) = list.available_channels() {
|
||||
channel_count = available_channels.len();
|
||||
if self.active_channel.is_none() {
|
||||
if let Some(channel_id) = available_channels.first().map(|channel| channel.id) {
|
||||
active_channel = list.get_channel(channel_id, cx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel_count = 0;
|
||||
}
|
||||
|
||||
(active_channel, channel_count)
|
||||
});
|
||||
|
||||
if let Some(active_channel) = active_channel {
|
||||
self.set_active_channel(active_channel, cx);
|
||||
} else {
|
||||
self.message_list.reset(0);
|
||||
self.active_channel = None;
|
||||
}
|
||||
|
||||
self.channel_select.update(cx, |select, cx| {
|
||||
select.set_item_count(channel_count, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn set_active_channel(&mut self, channel: ModelHandle<Channel>, cx: &mut ViewContext<Self>) {
|
||||
if self.active_channel.as_ref().map(|e| &e.0) != Some(&channel) {
|
||||
{
|
||||
let channel = channel.read(cx);
|
||||
self.message_list.reset(channel.message_count());
|
||||
let placeholder = format!("Message #{}", channel.name());
|
||||
self.input_editor.update(cx, move |editor, cx| {
|
||||
editor.set_placeholder_text(placeholder, cx);
|
||||
});
|
||||
}
|
||||
let subscription = cx.subscribe(&channel, Self::channel_did_change);
|
||||
self.active_channel = Some((channel, subscription));
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_did_change(
|
||||
&mut self,
|
||||
_: ModelHandle<Channel>,
|
||||
event: &ChannelEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range,
|
||||
new_count,
|
||||
} => {
|
||||
self.message_list.splice(old_range.clone(), *new_count);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_channel(&self) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme;
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Container::new(ChildView::new(self.channel_select.id()).boxed())
|
||||
.with_style(theme.chat_panel.channel_select.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(self.render_active_channel_messages())
|
||||
.with_child(self.render_input_box())
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_active_channel_messages(&self) -> ElementBox {
|
||||
let messages = if self.active_channel.is_some() {
|
||||
List::new(self.message_list.clone()).boxed()
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
};
|
||||
|
||||
Expanded::new(1., messages).boxed()
|
||||
}
|
||||
|
||||
fn render_message(&self, message: &ChannelMessage) -> ElementBox {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let settings = self.settings.borrow();
|
||||
let theme = if message.is_pending() {
|
||||
&settings.theme.chat_panel.pending_message
|
||||
} else {
|
||||
&settings.theme.chat_panel.message
|
||||
};
|
||||
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Container::new(
|
||||
Label::new(
|
||||
message.sender.github_login.clone(),
|
||||
theme.sender.text.clone(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.sender.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
Label::new(
|
||||
format_timestamp(message.timestamp, now, self.local_timezone),
|
||||
theme.timestamp.text.clone(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.timestamp.container)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Text::new(message.body.clone(), theme.body.clone()).boxed())
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_input_box(&self) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme;
|
||||
Container::new(ChildView::new(self.input_editor.id()).boxed())
|
||||
.with_style(theme.chat_panel.input_editor.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_channel_name(
|
||||
channel_list: &ModelHandle<ChannelList>,
|
||||
ix: usize,
|
||||
item_type: ItemType,
|
||||
is_hovered: bool,
|
||||
theme: &theme::ChannelSelect,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
let channel = &channel_list.read(cx).available_channels().unwrap()[ix];
|
||||
let theme = match (item_type, is_hovered) {
|
||||
(ItemType::Header, _) => &theme.header,
|
||||
(ItemType::Selected, false) => &theme.active_item,
|
||||
(ItemType::Selected, true) => &theme.hovered_active_item,
|
||||
(ItemType::Unselected, false) => &theme.item,
|
||||
(ItemType::Unselected, true) => &theme.hovered_item,
|
||||
};
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
|
||||
.with_style(theme.hash.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_sign_in_prompt(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme;
|
||||
let rpc = self.rpc.clone();
|
||||
let this = cx.handle();
|
||||
|
||||
enum SignInPromptLabel {}
|
||||
|
||||
Align::new(
|
||||
MouseEventHandler::new::<SignInPromptLabel, _, _, _>(0, cx, |mouse_state, _| {
|
||||
Label::new(
|
||||
"Sign in to use chat".to_string(),
|
||||
if mouse_state.hovered {
|
||||
theme.chat_panel.hovered_sign_in_prompt.clone()
|
||||
} else {
|
||||
theme.chat_panel.sign_in_prompt.clone()
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |cx| {
|
||||
let rpc = rpc.clone();
|
||||
let this = this.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
if rpc.authenticate_and_connect(&cx).log_err().await.is_some() {
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = this.upgrade(cx) {
|
||||
if this.is_focused(cx) {
|
||||
this.update(cx, |this, cx| cx.focus(&this.input_editor));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn send(&mut self, _: &Send, cx: &mut ViewContext<Self>) {
|
||||
if let Some((channel, _)) = self.active_channel.as_ref() {
|
||||
let body = self.input_editor.update(cx, |editor, cx| {
|
||||
let body = editor.text(cx);
|
||||
editor.clear(cx);
|
||||
body
|
||||
});
|
||||
|
||||
if let Some(task) = channel
|
||||
.update(cx, |channel, cx| channel.send_message(body, cx))
|
||||
.log_err()
|
||||
{
|
||||
task.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
|
||||
if let Some((channel, _)) = self.active_channel.as_ref() {
|
||||
channel.update(cx, |channel, cx| {
|
||||
channel.load_more_messages(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ChatPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for ChatPanel {
|
||||
fn ui_name() -> &'static str {
|
||||
"ChatPanel"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme;
|
||||
let element = if self.rpc.user_id().is_some() {
|
||||
self.render_channel()
|
||||
} else {
|
||||
self.render_sign_in_prompt(cx)
|
||||
};
|
||||
ConstrainedBox::new(
|
||||
Container::new(element)
|
||||
.with_style(theme.chat_panel.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_min_width(150.)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if matches!(*self.rpc.status().borrow(), rpc::Status::Connected { .. }) {
|
||||
cx.focus(&self.input_editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_timestamp(
|
||||
mut timestamp: OffsetDateTime,
|
||||
mut now: OffsetDateTime,
|
||||
local_timezone: UtcOffset,
|
||||
) -> String {
|
||||
timestamp = timestamp.to_offset(local_timezone);
|
||||
now = now.to_offset(local_timezone);
|
||||
|
||||
let today = now.date();
|
||||
let date = timestamp.date();
|
||||
let mut hour = timestamp.hour();
|
||||
let mut part = "am";
|
||||
if hour > 12 {
|
||||
hour -= 12;
|
||||
part = "pm";
|
||||
}
|
||||
if date == today {
|
||||
format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
|
||||
} else if date.next_day() == Some(today) {
|
||||
format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
|
||||
} else {
|
||||
format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
|
||||
}
|
||||
}
|
4437
crates/zed/src/editor.rs
Normal file
992
crates/zed/src/editor/display_map.rs
Normal file
|
@ -0,0 +1,992 @@
|
|||
mod fold_map;
|
||||
mod tab_map;
|
||||
mod wrap_map;
|
||||
|
||||
use buffer::{self, Anchor, Buffer, Point, ToOffset, ToPoint};
|
||||
use fold_map::{FoldMap, ToFoldPoint as _};
|
||||
use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
|
||||
use std::ops::Range;
|
||||
use sum_tree::Bias;
|
||||
use tab_map::TabMap;
|
||||
use wrap_map::WrapMap;
|
||||
pub use wrap_map::{BufferRows, HighlightedChunks};
|
||||
|
||||
pub trait ToDisplayPoint {
|
||||
fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint;
|
||||
}
|
||||
|
||||
pub struct DisplayMap {
|
||||
buffer: ModelHandle<Buffer>,
|
||||
fold_map: FoldMap,
|
||||
tab_map: TabMap,
|
||||
wrap_map: ModelHandle<WrapMap>,
|
||||
}
|
||||
|
||||
impl Entity for DisplayMap {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl DisplayMap {
|
||||
pub fn new(
|
||||
buffer: ModelHandle<Buffer>,
|
||||
tab_size: usize,
|
||||
font_id: FontId,
|
||||
font_size: f32,
|
||||
wrap_width: Option<f32>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx);
|
||||
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
|
||||
let wrap_map =
|
||||
cx.add_model(|cx| WrapMap::new(snapshot, font_id, font_size, wrap_width, cx));
|
||||
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
|
||||
DisplayMap {
|
||||
buffer,
|
||||
fold_map,
|
||||
tab_map,
|
||||
wrap_map,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplayMapSnapshot {
|
||||
let (folds_snapshot, edits) = self.fold_map.read(cx);
|
||||
let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits);
|
||||
let wraps_snapshot = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
|
||||
DisplayMapSnapshot {
|
||||
buffer_snapshot: self.buffer.read(cx).snapshot(),
|
||||
folds_snapshot,
|
||||
tabs_snapshot,
|
||||
wraps_snapshot,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fold<T: ToOffset>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let (mut fold_map, snapshot, edits) = self.fold_map.write(cx);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
|
||||
self.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
let (snapshot, edits) = fold_map.fold(ranges, cx);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
|
||||
self.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
}
|
||||
|
||||
pub fn unfold<T: ToOffset>(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<T>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let (mut fold_map, snapshot, edits) = self.fold_map.write(cx);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
|
||||
self.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
let (snapshot, edits) = fold_map.unfold(ranges, cx);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
|
||||
self.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
}
|
||||
|
||||
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
|
||||
self.wrap_map
|
||||
.update(cx, |map, cx| map.set_font(font_id, font_size, cx));
|
||||
}
|
||||
|
||||
pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
|
||||
self.wrap_map
|
||||
.update(cx, |map, cx| map.set_wrap_width(width, cx))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool {
|
||||
self.wrap_map.read(cx).is_rewrapping()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DisplayMapSnapshot {
|
||||
buffer_snapshot: buffer::Snapshot,
|
||||
folds_snapshot: fold_map::Snapshot,
|
||||
tabs_snapshot: tab_map::Snapshot,
|
||||
wraps_snapshot: wrap_map::Snapshot,
|
||||
}
|
||||
|
||||
impl DisplayMapSnapshot {
|
||||
#[cfg(test)]
|
||||
pub fn fold_count(&self) -> usize {
|
||||
self.folds_snapshot.fold_count()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.buffer_snapshot.len() == 0
|
||||
}
|
||||
|
||||
pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
|
||||
self.wraps_snapshot.buffer_rows(start_row)
|
||||
}
|
||||
|
||||
pub fn buffer_row_count(&self) -> u32 {
|
||||
self.buffer_snapshot.max_point().row + 1
|
||||
}
|
||||
|
||||
pub fn prev_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) {
|
||||
loop {
|
||||
*display_point.column_mut() = 0;
|
||||
let mut point = display_point.to_buffer_point(self, Bias::Left);
|
||||
point.column = 0;
|
||||
let next_display_point = point.to_display_point(self, Bias::Left);
|
||||
if next_display_point == display_point {
|
||||
return (display_point, point);
|
||||
}
|
||||
display_point = next_display_point;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) {
|
||||
loop {
|
||||
*display_point.column_mut() = self.line_len(display_point.row());
|
||||
let mut point = display_point.to_buffer_point(self, Bias::Right);
|
||||
point.column = self.buffer_snapshot.line_len(point.row);
|
||||
let next_display_point = point.to_display_point(self, Bias::Right);
|
||||
if next_display_point == display_point {
|
||||
return (display_point, point);
|
||||
}
|
||||
display_point = next_display_point;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> DisplayPoint {
|
||||
DisplayPoint(self.wraps_snapshot.max_point())
|
||||
}
|
||||
|
||||
pub fn chunks_at(&self, display_row: u32) -> wrap_map::Chunks {
|
||||
self.wraps_snapshot.chunks_at(display_row)
|
||||
}
|
||||
|
||||
pub fn highlighted_chunks_for_rows(
|
||||
&mut self,
|
||||
display_rows: Range<u32>,
|
||||
) -> wrap_map::HighlightedChunks {
|
||||
self.wraps_snapshot
|
||||
.highlighted_chunks_for_rows(display_rows)
|
||||
}
|
||||
|
||||
pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
|
||||
let mut column = 0;
|
||||
let mut chars = self.chunks_at(point.row()).flat_map(str::chars);
|
||||
while column < point.column() {
|
||||
if let Some(c) = chars.next() {
|
||||
column += c.len_utf8() as u32;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
chars
|
||||
}
|
||||
|
||||
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
|
||||
let mut count = 0;
|
||||
let mut column = 0;
|
||||
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||
if column >= target {
|
||||
break;
|
||||
}
|
||||
count += 1;
|
||||
column += c.len_utf8() as u32;
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
|
||||
let mut count = 0;
|
||||
let mut column = 0;
|
||||
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||
if c == '\n' || count >= char_count {
|
||||
break;
|
||||
}
|
||||
count += 1;
|
||||
column += c.len_utf8() as u32;
|
||||
}
|
||||
column
|
||||
}
|
||||
|
||||
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
|
||||
DisplayPoint(self.wraps_snapshot.clip_point(point.0, bias))
|
||||
}
|
||||
|
||||
pub fn folds_in_range<'a, T>(
|
||||
&'a self,
|
||||
range: Range<T>,
|
||||
) -> impl Iterator<Item = &'a Range<Anchor>>
|
||||
where
|
||||
T: ToOffset,
|
||||
{
|
||||
self.folds_snapshot.folds_in_range(range)
|
||||
}
|
||||
|
||||
pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
|
||||
self.folds_snapshot.intersects_fold(offset)
|
||||
}
|
||||
|
||||
pub fn is_line_folded(&self, display_row: u32) -> bool {
|
||||
let wrap_point = DisplayPoint::new(display_row, 0).0;
|
||||
let row = self.wraps_snapshot.to_tab_point(wrap_point).row();
|
||||
self.folds_snapshot.is_line_folded(row)
|
||||
}
|
||||
|
||||
pub fn soft_wrap_indent(&self, display_row: u32) -> Option<u32> {
|
||||
self.wraps_snapshot.soft_wrap_indent(display_row)
|
||||
}
|
||||
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks_at(0).collect()
|
||||
}
|
||||
|
||||
pub fn line(&self, display_row: u32) -> String {
|
||||
let mut result = String::new();
|
||||
for chunk in self.chunks_at(display_row) {
|
||||
if let Some(ix) = chunk.find('\n') {
|
||||
result.push_str(&chunk[0..ix]);
|
||||
break;
|
||||
} else {
|
||||
result.push_str(chunk);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
|
||||
let mut indent = 0;
|
||||
let mut is_blank = true;
|
||||
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
|
||||
if c == ' ' {
|
||||
indent += 1;
|
||||
} else {
|
||||
is_blank = c == '\n';
|
||||
break;
|
||||
}
|
||||
}
|
||||
(indent, is_blank)
|
||||
}
|
||||
|
||||
pub fn line_len(&self, row: u32) -> u32 {
|
||||
self.wraps_snapshot.line_len(row)
|
||||
}
|
||||
|
||||
pub fn longest_row(&self) -> u32 {
|
||||
self.wraps_snapshot.longest_row()
|
||||
}
|
||||
|
||||
pub fn anchor_before(&self, point: DisplayPoint, bias: Bias) -> Anchor {
|
||||
self.buffer_snapshot
|
||||
.anchor_before(point.to_buffer_point(self, bias))
|
||||
}
|
||||
|
||||
pub fn anchor_after(&self, point: DisplayPoint, bias: Bias) -> Anchor {
|
||||
self.buffer_snapshot
|
||||
.anchor_after(point.to_buffer_point(self, bias))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct DisplayPoint(wrap_map::WrapPoint);
|
||||
|
||||
impl DisplayPoint {
|
||||
pub fn new(row: u32, column: u32) -> Self {
|
||||
Self(wrap_map::WrapPoint::new(row, column))
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self::new(0, 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.0.is_zero()
|
||||
}
|
||||
|
||||
pub fn row(self) -> u32 {
|
||||
self.0.row()
|
||||
}
|
||||
|
||||
pub fn column(self) -> u32 {
|
||||
self.0.column()
|
||||
}
|
||||
|
||||
pub fn row_mut(&mut self) -> &mut u32 {
|
||||
self.0.row_mut()
|
||||
}
|
||||
|
||||
pub fn column_mut(&mut self) -> &mut u32 {
|
||||
self.0.column_mut()
|
||||
}
|
||||
|
||||
pub fn to_buffer_point(self, map: &DisplayMapSnapshot, bias: Bias) -> Point {
|
||||
let unwrapped_point = map.wraps_snapshot.to_tab_point(self.0);
|
||||
let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
|
||||
unexpanded_point.to_buffer_point(&map.folds_snapshot)
|
||||
}
|
||||
|
||||
pub fn to_buffer_offset(self, map: &DisplayMapSnapshot, bias: Bias) -> usize {
|
||||
let unwrapped_point = map.wraps_snapshot.to_tab_point(self.0);
|
||||
let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
|
||||
unexpanded_point.to_buffer_offset(&map.folds_snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToDisplayPoint for Point {
|
||||
fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint {
|
||||
let fold_point = self.to_fold_point(&map.folds_snapshot, bias);
|
||||
let tab_point = map.tabs_snapshot.to_tab_point(fold_point);
|
||||
let wrap_point = map.wraps_snapshot.to_wrap_point(tab_point);
|
||||
DisplayPoint(wrap_point)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToDisplayPoint for Anchor {
|
||||
fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint {
|
||||
self.to_point(&map.buffer_snapshot)
|
||||
.to_display_point(map, bias)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{editor::movement, test::*};
|
||||
use buffer::{History, Language, LanguageConfig, RandomCharIter, SelectionGoal, SyntaxTheme};
|
||||
use gpui::{color::Color, MutableAppContext};
|
||||
use rand::{prelude::StdRng, Rng};
|
||||
use std::{env, sync::Arc};
|
||||
use Bias::*;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random(mut cx: gpui::TestAppContext, mut rng: StdRng) {
|
||||
cx.foreground().set_block_on_ticks(0..=50);
|
||||
cx.foreground().forbid_parking();
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let font_cache = cx.font_cache().clone();
|
||||
let tab_size = rng.gen_range(1..=4);
|
||||
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let max_wrap_width = 300.0;
|
||||
let mut wrap_width = if rng.gen_bool(0.1) {
|
||||
None
|
||||
} else {
|
||||
Some(rng.gen_range(0.0..=max_wrap_width))
|
||||
};
|
||||
|
||||
log::info!("tab size: {}", tab_size);
|
||||
log::info!("wrap width: {:?}", wrap_width);
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let len = rng.gen_range(0..10);
|
||||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
||||
Buffer::new(0, text, cx)
|
||||
});
|
||||
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
|
||||
});
|
||||
let (_observer, notifications) = Observer::new(&map, &mut cx);
|
||||
let mut fold_count = 0;
|
||||
|
||||
for _i in 0..operations {
|
||||
match rng.gen_range(0..100) {
|
||||
0..=19 => {
|
||||
wrap_width = if rng.gen_bool(0.2) {
|
||||
None
|
||||
} else {
|
||||
Some(rng.gen_range(0.0..=max_wrap_width))
|
||||
};
|
||||
log::info!("setting wrap width to {:?}", wrap_width);
|
||||
map.update(&mut cx, |map, cx| map.set_wrap_width(wrap_width, cx));
|
||||
}
|
||||
20..=80 => {
|
||||
let mut ranges = Vec::new();
|
||||
for _ in 0..rng.gen_range(1..=3) {
|
||||
buffer.read_with(&cx, |buffer, _| {
|
||||
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
|
||||
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
|
||||
ranges.push(start..end);
|
||||
});
|
||||
}
|
||||
|
||||
if rng.gen() && fold_count > 0 {
|
||||
log::info!("unfolding ranges: {:?}", ranges);
|
||||
map.update(&mut cx, |map, cx| {
|
||||
map.unfold(ranges, cx);
|
||||
});
|
||||
} else {
|
||||
log::info!("folding ranges: {:?}", ranges);
|
||||
map.update(&mut cx, |map, cx| {
|
||||
map.fold(ranges, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
buffer.update(&mut cx, |buffer, cx| buffer.randomly_mutate(&mut rng, cx));
|
||||
}
|
||||
}
|
||||
|
||||
if map.read_with(&cx, |map, cx| map.is_rewrapping(cx)) {
|
||||
notifications.recv().await.unwrap();
|
||||
}
|
||||
|
||||
let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
|
||||
fold_count = snapshot.fold_count();
|
||||
log::info!("buffer text: {:?}", buffer.read_with(&cx, |b, _| b.text()));
|
||||
log::info!("display text: {:?}", snapshot.text());
|
||||
|
||||
// Line boundaries
|
||||
for _ in 0..5 {
|
||||
let row = rng.gen_range(0..=snapshot.max_point().row());
|
||||
let column = rng.gen_range(0..=snapshot.line_len(row));
|
||||
let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
|
||||
|
||||
let (prev_display_bound, prev_buffer_bound) = snapshot.prev_row_boundary(point);
|
||||
let (next_display_bound, next_buffer_bound) = snapshot.next_row_boundary(point);
|
||||
|
||||
assert!(prev_display_bound <= point);
|
||||
assert!(next_display_bound >= point);
|
||||
assert_eq!(prev_buffer_bound.column, 0);
|
||||
assert_eq!(prev_display_bound.column(), 0);
|
||||
if next_display_bound < snapshot.max_point() {
|
||||
assert_eq!(
|
||||
buffer
|
||||
.read_with(&cx, |buffer, _| buffer.chars_at(next_buffer_bound).next()),
|
||||
Some('\n')
|
||||
)
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
prev_display_bound,
|
||||
prev_buffer_bound.to_display_point(&snapshot, Left),
|
||||
"row boundary before {:?}. reported buffer row boundary: {:?}",
|
||||
point,
|
||||
prev_buffer_bound
|
||||
);
|
||||
assert_eq!(
|
||||
next_display_bound,
|
||||
next_buffer_bound.to_display_point(&snapshot, Right),
|
||||
"display row boundary after {:?}. reported buffer row boundary: {:?}",
|
||||
point,
|
||||
next_buffer_bound
|
||||
);
|
||||
assert_eq!(
|
||||
prev_buffer_bound,
|
||||
prev_display_bound.to_buffer_point(&snapshot, Left),
|
||||
"row boundary before {:?}. reported display row boundary: {:?}",
|
||||
point,
|
||||
prev_display_bound
|
||||
);
|
||||
assert_eq!(
|
||||
next_buffer_bound,
|
||||
next_display_bound.to_buffer_point(&snapshot, Right),
|
||||
"row boundary after {:?}. reported display row boundary: {:?}",
|
||||
point,
|
||||
next_display_bound
|
||||
);
|
||||
}
|
||||
|
||||
// Movement
|
||||
for _ in 0..5 {
|
||||
let row = rng.gen_range(0..=snapshot.max_point().row());
|
||||
let column = rng.gen_range(0..=snapshot.line_len(row));
|
||||
let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
|
||||
|
||||
log::info!("Moving from point {:?}", point);
|
||||
|
||||
let moved_right = movement::right(&snapshot, point).unwrap();
|
||||
log::info!("Right {:?}", moved_right);
|
||||
if point < snapshot.max_point() {
|
||||
assert!(moved_right > point);
|
||||
if point.column() == snapshot.line_len(point.row())
|
||||
|| snapshot.soft_wrap_indent(point.row()).is_some()
|
||||
&& point.column() == snapshot.line_len(point.row()) - 1
|
||||
{
|
||||
assert!(moved_right.row() > point.row());
|
||||
}
|
||||
} else {
|
||||
assert_eq!(moved_right, point);
|
||||
}
|
||||
|
||||
let moved_left = movement::left(&snapshot, point).unwrap();
|
||||
log::info!("Left {:?}", moved_left);
|
||||
if !point.is_zero() {
|
||||
assert!(moved_left < point);
|
||||
if point.column() == 0 {
|
||||
assert!(moved_left.row() < point.row());
|
||||
}
|
||||
} else {
|
||||
assert!(moved_left.is_zero());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_soft_wraps(cx: &mut MutableAppContext) {
|
||||
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let font_cache = cx.font_cache();
|
||||
|
||||
let tab_size = 4;
|
||||
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 12.0;
|
||||
let wrap_width = Some(64.);
|
||||
|
||||
let text = "one two three four five\nsix seven eight";
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text.to_string(), cx));
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
|
||||
});
|
||||
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
assert_eq!(
|
||||
snapshot.chunks_at(0).collect::<String>(),
|
||||
"one two \nthree four \nfive\nsix seven \neight"
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
|
||||
DisplayPoint::new(0, 7)
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
|
||||
DisplayPoint::new(1, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
movement::right(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
|
||||
DisplayPoint::new(1, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
movement::left(&snapshot, DisplayPoint::new(1, 0)).unwrap(),
|
||||
DisplayPoint::new(0, 7)
|
||||
);
|
||||
assert_eq!(
|
||||
movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None).unwrap(),
|
||||
(DisplayPoint::new(0, 7), SelectionGoal::Column(10))
|
||||
);
|
||||
assert_eq!(
|
||||
movement::down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(0, 7),
|
||||
SelectionGoal::Column(10)
|
||||
)
|
||||
.unwrap(),
|
||||
(DisplayPoint::new(1, 10), SelectionGoal::Column(10))
|
||||
);
|
||||
assert_eq!(
|
||||
movement::down(
|
||||
&snapshot,
|
||||
DisplayPoint::new(1, 10),
|
||||
SelectionGoal::Column(10)
|
||||
)
|
||||
.unwrap(),
|
||||
(DisplayPoint::new(2, 4), SelectionGoal::Column(10))
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let ix = buffer.text().find("seven").unwrap();
|
||||
buffer.edit(vec![ix..ix], "and ", cx);
|
||||
});
|
||||
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
assert_eq!(
|
||||
snapshot.chunks_at(1).collect::<String>(),
|
||||
"three four \nfive\nsix and \nseven eight"
|
||||
);
|
||||
|
||||
// Re-wrap on font size changes
|
||||
map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
|
||||
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
assert_eq!(
|
||||
snapshot.chunks_at(1).collect::<String>(),
|
||||
"three \nfour five\nsix and \nseven \neight"
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_chunks_at(cx: &mut gpui::MutableAppContext) {
|
||||
let text = sample_text(6, 6);
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
|
||||
let tab_size = 4;
|
||||
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
vec![
|
||||
Point::new(1, 0)..Point::new(1, 0),
|
||||
Point::new(1, 1)..Point::new(1, 1),
|
||||
Point::new(2, 1)..Point::new(2, 1),
|
||||
],
|
||||
"\t",
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
map.update(cx, |map, cx| map.snapshot(cx))
|
||||
.chunks_at(1)
|
||||
.collect::<String>()
|
||||
.lines()
|
||||
.next(),
|
||||
Some(" b bbbbb")
|
||||
);
|
||||
assert_eq!(
|
||||
map.update(cx, |map, cx| map.snapshot(cx))
|
||||
.chunks_at(2)
|
||||
.collect::<String>()
|
||||
.lines()
|
||||
.next(),
|
||||
Some("c ccccc")
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_highlighted_chunks_at(mut cx: gpui::TestAppContext) {
|
||||
use unindent::Unindent as _;
|
||||
|
||||
let grammar = tree_sitter_rust::language();
|
||||
let text = r#"
|
||||
fn outer() {}
|
||||
|
||||
mod module {
|
||||
fn inner() {}
|
||||
}"#
|
||||
.unindent();
|
||||
let highlight_query = tree_sitter::Query::new(
|
||||
grammar,
|
||||
r#"
|
||||
(mod_item name: (identifier) body: _ @mod.body)
|
||||
(function_item name: (identifier) @fn.name)"#,
|
||||
)
|
||||
.unwrap();
|
||||
let theme = SyntaxTheme::new(vec![
|
||||
("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
|
||||
("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
|
||||
]);
|
||||
let lang = Arc::new(Language {
|
||||
config: LanguageConfig {
|
||||
name: "Test".to_string(),
|
||||
path_suffixes: vec![".test".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
grammar: grammar.clone(),
|
||||
highlight_query,
|
||||
brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
|
||||
highlight_map: Default::default(),
|
||||
});
|
||||
lang.set_theme(&theme);
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
Buffer::from_history(0, History::new(text.into()), None, Some(lang), cx)
|
||||
});
|
||||
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
|
||||
|
||||
let tab_size = 2;
|
||||
let font_cache = cx.font_cache();
|
||||
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
|
||||
let map =
|
||||
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
|
||||
assert_eq!(
|
||||
cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
|
||||
vec![
|
||||
("fn ".to_string(), None),
|
||||
("outer".to_string(), Some("fn.name")),
|
||||
("() {}\n\nmod module ".to_string(), None),
|
||||
("{\n fn ".to_string(), Some("mod.body")),
|
||||
("inner".to_string(), Some("fn.name")),
|
||||
("() {}\n}".to_string(), Some("mod.body")),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
cx.update(|cx| highlighted_chunks(3..5, &map, &theme, cx)),
|
||||
vec![
|
||||
(" fn ".to_string(), Some("mod.body")),
|
||||
("inner".to_string(), Some("fn.name")),
|
||||
("() {}\n}".to_string(), Some("mod.body")),
|
||||
]
|
||||
);
|
||||
|
||||
map.update(&mut cx, |map, cx| {
|
||||
map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
|
||||
});
|
||||
assert_eq!(
|
||||
cx.update(|cx| highlighted_chunks(0..2, &map, &theme, cx)),
|
||||
vec![
|
||||
("fn ".to_string(), None),
|
||||
("out".to_string(), Some("fn.name")),
|
||||
("…".to_string(), None),
|
||||
(" fn ".to_string(), Some("mod.body")),
|
||||
("inner".to_string(), Some("fn.name")),
|
||||
("() {}\n}".to_string(), Some("mod.body")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_highlighted_chunks_with_soft_wrapping(mut cx: gpui::TestAppContext) {
|
||||
use unindent::Unindent as _;
|
||||
|
||||
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
|
||||
|
||||
let grammar = tree_sitter_rust::language();
|
||||
let text = r#"
|
||||
fn outer() {}
|
||||
|
||||
mod module {
|
||||
fn inner() {}
|
||||
}"#
|
||||
.unindent();
|
||||
let highlight_query = tree_sitter::Query::new(
|
||||
grammar,
|
||||
r#"
|
||||
(mod_item name: (identifier) body: _ @mod.body)
|
||||
(function_item name: (identifier) @fn.name)"#,
|
||||
)
|
||||
.unwrap();
|
||||
let theme = SyntaxTheme::new(vec![
|
||||
("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
|
||||
("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
|
||||
]);
|
||||
let lang = Arc::new(Language {
|
||||
config: LanguageConfig {
|
||||
name: "Test".to_string(),
|
||||
path_suffixes: vec![".test".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
grammar: grammar.clone(),
|
||||
highlight_query,
|
||||
brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
|
||||
highlight_map: Default::default(),
|
||||
});
|
||||
lang.set_theme(&theme);
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
Buffer::from_history(0, History::new(text.into()), None, Some(lang), cx)
|
||||
});
|
||||
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
|
||||
|
||||
let font_cache = cx.font_cache();
|
||||
|
||||
let tab_size = 4;
|
||||
let family_id = font_cache.load_family(&["Courier"]).unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 16.0;
|
||||
|
||||
let map = cx
|
||||
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), cx));
|
||||
assert_eq!(
|
||||
cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
|
||||
[
|
||||
("fn \n".to_string(), None),
|
||||
("oute\nr".to_string(), Some("fn.name")),
|
||||
("() \n{}\n\n".to_string(), None),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
cx.update(|cx| highlighted_chunks(3..5, &map, &theme, cx)),
|
||||
[("{}\n\n".to_string(), None)]
|
||||
);
|
||||
|
||||
map.update(&mut cx, |map, cx| {
|
||||
map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
|
||||
});
|
||||
assert_eq!(
|
||||
cx.update(|cx| highlighted_chunks(1..4, &map, &theme, cx)),
|
||||
[
|
||||
("out".to_string(), Some("fn.name")),
|
||||
("…\n".to_string(), None),
|
||||
(" \nfn ".to_string(), Some("mod.body")),
|
||||
("i\n".to_string(), Some("fn.name"))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_clip_point(cx: &mut gpui::MutableAppContext) {
|
||||
use Bias::{Left, Right};
|
||||
|
||||
let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n";
|
||||
let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n";
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
|
||||
|
||||
let tab_size = 4;
|
||||
let font_cache = cx.font_cache();
|
||||
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
|
||||
});
|
||||
let map = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
assert_eq!(map.text(), display_text);
|
||||
for (input_column, bias, output_column) in vec![
|
||||
("'a', '".len(), Left, "'a', '".len()),
|
||||
("'a', '".len() + 1, Left, "'a', '".len()),
|
||||
("'a', '".len() + 1, Right, "'a', 'α".len()),
|
||||
("'a', 'α', ".len(), Left, "'a', 'α',".len()),
|
||||
("'a', 'α', ".len(), Right, "'a', 'α', ".len()),
|
||||
("'a', 'α', '".len() + 1, Left, "'a', 'α', '".len()),
|
||||
("'a', 'α', '".len() + 1, Right, "'a', 'α', '✋".len()),
|
||||
("'a', 'α', '✋',".len(), Right, "'a', 'α', '✋',".len()),
|
||||
("'a', 'α', '✋', ".len(), Left, "'a', 'α', '✋',".len()),
|
||||
(
|
||||
"'a', 'α', '✋', ".len(),
|
||||
Right,
|
||||
"'a', 'α', '✋', ".len(),
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
map.clip_point(DisplayPoint::new(1, input_column as u32), bias),
|
||||
DisplayPoint::new(1, output_column as u32),
|
||||
"clip_point(({}, {}))",
|
||||
1,
|
||||
input_column,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
|
||||
let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
|
||||
let tab_size = 4;
|
||||
let font_cache = cx.font_cache();
|
||||
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
|
||||
});
|
||||
let map = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
assert_eq!(map.text(), "✅ α\nβ \n🏀β γ");
|
||||
assert_eq!(
|
||||
map.chunks_at(0).collect::<String>(),
|
||||
"✅ α\nβ \n🏀β γ"
|
||||
);
|
||||
assert_eq!(map.chunks_at(1).collect::<String>(), "β \n🏀β γ");
|
||||
assert_eq!(map.chunks_at(2).collect::<String>(), "🏀β γ");
|
||||
|
||||
let point = Point::new(0, "✅\t\t".len() as u32);
|
||||
let display_point = DisplayPoint::new(0, "✅ ".len() as u32);
|
||||
assert_eq!(point.to_display_point(&map, Left), display_point);
|
||||
assert_eq!(display_point.to_buffer_point(&map, Left), point,);
|
||||
|
||||
let point = Point::new(1, "β\t".len() as u32);
|
||||
let display_point = DisplayPoint::new(1, "β ".len() as u32);
|
||||
assert_eq!(point.to_display_point(&map, Left), display_point);
|
||||
assert_eq!(display_point.to_buffer_point(&map, Left), point,);
|
||||
|
||||
let point = Point::new(2, "🏀β\t\t".len() as u32);
|
||||
let display_point = DisplayPoint::new(2, "🏀β ".len() as u32);
|
||||
assert_eq!(point.to_display_point(&map, Left), display_point);
|
||||
assert_eq!(display_point.to_buffer_point(&map, Left), point,);
|
||||
|
||||
// Display points inside of expanded tabs
|
||||
assert_eq!(
|
||||
DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Right),
|
||||
Point::new(0, "✅\t\t".len() as u32),
|
||||
);
|
||||
assert_eq!(
|
||||
DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Left),
|
||||
Point::new(0, "✅\t".len() as u32),
|
||||
);
|
||||
assert_eq!(
|
||||
DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Right),
|
||||
Point::new(0, "✅\t".len() as u32),
|
||||
);
|
||||
assert_eq!(
|
||||
DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Left),
|
||||
Point::new(0, "✅".len() as u32),
|
||||
);
|
||||
|
||||
// Clipping display points inside of multi-byte characters
|
||||
assert_eq!(
|
||||
map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Left),
|
||||
DisplayPoint::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Bias::Right),
|
||||
DisplayPoint::new(0, "✅".len() as u32)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_max_point(cx: &mut gpui::MutableAppContext) {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "aaa\n\t\tbbb", cx));
|
||||
let tab_size = 4;
|
||||
let font_cache = cx.font_cache();
|
||||
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
let map = cx.add_model(|cx| {
|
||||
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
|
||||
DisplayPoint::new(1, 11)
|
||||
)
|
||||
}
|
||||
|
||||
fn highlighted_chunks<'a>(
|
||||
rows: Range<u32>,
|
||||
map: &ModelHandle<DisplayMap>,
|
||||
theme: &'a SyntaxTheme,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Vec<(String, Option<&'a str>)> {
|
||||
let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let mut chunks: Vec<(String, Option<&str>)> = Vec::new();
|
||||
for (chunk, style_id) in snapshot.highlighted_chunks_for_rows(rows) {
|
||||
let style_name = theme.highlight_name(style_id);
|
||||
if let Some((last_chunk, last_style_name)) = chunks.last_mut() {
|
||||
if style_name == *last_style_name {
|
||||
last_chunk.push_str(chunk);
|
||||
} else {
|
||||
chunks.push((chunk.to_string(), style_name));
|
||||
}
|
||||
} else {
|
||||
chunks.push((chunk.to_string(), style_name));
|
||||
}
|
||||
}
|
||||
chunks
|
||||
}
|
||||
}
|
1606
crates/zed/src/editor/display_map/fold_map.rs
Normal file
475
crates/zed/src/editor/display_map/tab_map.rs
Normal file
|
@ -0,0 +1,475 @@
|
|||
use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot};
|
||||
use buffer::{rope, HighlightId};
|
||||
use parking_lot::Mutex;
|
||||
use std::{mem, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
|
||||
pub struct TabMap(Mutex<Snapshot>);
|
||||
|
||||
impl TabMap {
|
||||
pub fn new(input: FoldSnapshot, tab_size: usize) -> (Self, Snapshot) {
|
||||
let snapshot = Snapshot {
|
||||
fold_snapshot: input,
|
||||
tab_size,
|
||||
};
|
||||
(Self(Mutex::new(snapshot.clone())), snapshot)
|
||||
}
|
||||
|
||||
pub fn sync(
|
||||
&self,
|
||||
fold_snapshot: FoldSnapshot,
|
||||
mut fold_edits: Vec<FoldEdit>,
|
||||
) -> (Snapshot, Vec<Edit>) {
|
||||
let mut old_snapshot = self.0.lock();
|
||||
let new_snapshot = Snapshot {
|
||||
fold_snapshot,
|
||||
tab_size: old_snapshot.tab_size,
|
||||
};
|
||||
|
||||
let mut tab_edits = Vec::with_capacity(fold_edits.len());
|
||||
for fold_edit in &mut fold_edits {
|
||||
let mut delta = 0;
|
||||
for chunk in old_snapshot
|
||||
.fold_snapshot
|
||||
.chunks_at(fold_edit.old_bytes.end)
|
||||
{
|
||||
let patterns: &[_] = &['\t', '\n'];
|
||||
if let Some(ix) = chunk.find(patterns) {
|
||||
if &chunk[ix..ix + 1] == "\t" {
|
||||
fold_edit.old_bytes.end.0 += delta + ix + 1;
|
||||
fold_edit.new_bytes.end.0 += delta + ix + 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
delta += chunk.len();
|
||||
}
|
||||
}
|
||||
|
||||
let mut ix = 1;
|
||||
while ix < fold_edits.len() {
|
||||
let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
|
||||
let prev_edit = prev_edits.last_mut().unwrap();
|
||||
let edit = &next_edits[0];
|
||||
if prev_edit.old_bytes.end >= edit.old_bytes.start {
|
||||
prev_edit.old_bytes.end = edit.old_bytes.end;
|
||||
prev_edit.new_bytes.end = edit.new_bytes.end;
|
||||
fold_edits.remove(ix);
|
||||
} else {
|
||||
ix += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for fold_edit in fold_edits {
|
||||
let old_start = fold_edit
|
||||
.old_bytes
|
||||
.start
|
||||
.to_point(&old_snapshot.fold_snapshot);
|
||||
let old_end = fold_edit
|
||||
.old_bytes
|
||||
.end
|
||||
.to_point(&old_snapshot.fold_snapshot);
|
||||
let new_start = fold_edit
|
||||
.new_bytes
|
||||
.start
|
||||
.to_point(&new_snapshot.fold_snapshot);
|
||||
let new_end = fold_edit
|
||||
.new_bytes
|
||||
.end
|
||||
.to_point(&new_snapshot.fold_snapshot);
|
||||
tab_edits.push(Edit {
|
||||
old_lines: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
|
||||
new_lines: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
|
||||
});
|
||||
}
|
||||
|
||||
*old_snapshot = new_snapshot;
|
||||
(old_snapshot.clone(), tab_edits)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Snapshot {
|
||||
pub fold_snapshot: FoldSnapshot,
|
||||
pub tab_size: usize,
|
||||
}
|
||||
|
||||
impl Snapshot {
|
||||
pub fn text_summary(&self) -> TextSummary {
|
||||
self.text_summary_for_range(TabPoint::zero()..self.max_point())
|
||||
}
|
||||
|
||||
pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
|
||||
let input_start = self.to_fold_point(range.start, Bias::Left).0;
|
||||
let input_end = self.to_fold_point(range.end, Bias::Right).0;
|
||||
let input_summary = self
|
||||
.fold_snapshot
|
||||
.text_summary_for_range(input_start..input_end);
|
||||
|
||||
let mut first_line_chars = 0;
|
||||
let mut first_line_bytes = 0;
|
||||
for c in self.chunks_at(range.start).flat_map(|chunk| chunk.chars()) {
|
||||
if c == '\n'
|
||||
|| (range.start.row() == range.end.row() && first_line_bytes == range.end.column())
|
||||
{
|
||||
break;
|
||||
}
|
||||
first_line_chars += 1;
|
||||
first_line_bytes += c.len_utf8() as u32;
|
||||
}
|
||||
|
||||
let mut last_line_chars = 0;
|
||||
let mut last_line_bytes = 0;
|
||||
for c in self
|
||||
.chunks_at(TabPoint::new(range.end.row(), 0).max(range.start))
|
||||
.flat_map(|chunk| chunk.chars())
|
||||
{
|
||||
if last_line_bytes == range.end.column() {
|
||||
break;
|
||||
}
|
||||
last_line_chars += 1;
|
||||
last_line_bytes += c.len_utf8() as u32;
|
||||
}
|
||||
|
||||
TextSummary {
|
||||
lines: range.end.0 - range.start.0,
|
||||
first_line_chars,
|
||||
last_line_chars,
|
||||
longest_row: input_summary.longest_row,
|
||||
longest_row_chars: input_summary.longest_row_chars,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn version(&self) -> usize {
|
||||
self.fold_snapshot.version
|
||||
}
|
||||
|
||||
pub fn chunks_at(&self, point: TabPoint) -> Chunks {
|
||||
let (point, expanded_char_column, to_next_stop) = self.to_fold_point(point, Bias::Left);
|
||||
let fold_chunks = self
|
||||
.fold_snapshot
|
||||
.chunks_at(point.to_offset(&self.fold_snapshot));
|
||||
Chunks {
|
||||
fold_chunks,
|
||||
column: expanded_char_column,
|
||||
tab_size: self.tab_size,
|
||||
chunk: &SPACES[0..to_next_stop],
|
||||
skip_leading_tab: to_next_stop > 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlighted_chunks(&mut self, range: Range<TabPoint>) -> HighlightedChunks {
|
||||
let (input_start, expanded_char_column, to_next_stop) =
|
||||
self.to_fold_point(range.start, Bias::Left);
|
||||
let input_start = input_start.to_offset(&self.fold_snapshot);
|
||||
let input_end = self
|
||||
.to_fold_point(range.end, Bias::Right)
|
||||
.0
|
||||
.to_offset(&self.fold_snapshot);
|
||||
HighlightedChunks {
|
||||
fold_chunks: self
|
||||
.fold_snapshot
|
||||
.highlighted_chunks(input_start..input_end),
|
||||
column: expanded_char_column,
|
||||
tab_size: self.tab_size,
|
||||
chunk: &SPACES[0..to_next_stop],
|
||||
skip_leading_tab: to_next_stop > 0,
|
||||
style_id: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_rows(&self, row: u32) -> fold_map::BufferRows {
|
||||
self.fold_snapshot.buffer_rows(row)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks_at(Default::default()).collect()
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> TabPoint {
|
||||
self.to_tab_point(self.fold_snapshot.max_point())
|
||||
}
|
||||
|
||||
pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
|
||||
self.to_tab_point(
|
||||
self.fold_snapshot
|
||||
.clip_point(self.to_fold_point(point, bias).0, bias),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
|
||||
let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
|
||||
let expanded = Self::expand_tabs(chars, input.column() as usize, self.tab_size);
|
||||
TabPoint::new(input.row(), expanded as u32)
|
||||
}
|
||||
|
||||
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) {
|
||||
let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
|
||||
let expanded = output.column() as usize;
|
||||
let (collapsed, expanded_char_column, to_next_stop) =
|
||||
Self::collapse_tabs(chars, expanded, bias, self.tab_size);
|
||||
(
|
||||
FoldPoint::new(output.row(), collapsed as u32),
|
||||
expanded_char_column,
|
||||
to_next_stop,
|
||||
)
|
||||
}
|
||||
|
||||
fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
|
||||
let mut expanded_chars = 0;
|
||||
let mut expanded_bytes = 0;
|
||||
let mut collapsed_bytes = 0;
|
||||
for c in chars {
|
||||
if collapsed_bytes == column {
|
||||
break;
|
||||
}
|
||||
if c == '\t' {
|
||||
let tab_len = tab_size - expanded_chars % tab_size;
|
||||
expanded_bytes += tab_len;
|
||||
expanded_chars += tab_len;
|
||||
} else {
|
||||
expanded_bytes += c.len_utf8();
|
||||
expanded_chars += 1;
|
||||
}
|
||||
collapsed_bytes += c.len_utf8();
|
||||
}
|
||||
expanded_bytes
|
||||
}
|
||||
|
||||
fn collapse_tabs(
|
||||
mut chars: impl Iterator<Item = char>,
|
||||
column: usize,
|
||||
bias: Bias,
|
||||
tab_size: usize,
|
||||
) -> (usize, usize, usize) {
|
||||
let mut expanded_bytes = 0;
|
||||
let mut expanded_chars = 0;
|
||||
let mut collapsed_bytes = 0;
|
||||
while let Some(c) = chars.next() {
|
||||
if expanded_bytes >= column {
|
||||
break;
|
||||
}
|
||||
|
||||
if c == '\t' {
|
||||
let tab_len = tab_size - (expanded_chars % tab_size);
|
||||
expanded_chars += tab_len;
|
||||
expanded_bytes += tab_len;
|
||||
if expanded_bytes > column {
|
||||
expanded_chars -= expanded_bytes - column;
|
||||
return match bias {
|
||||
Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
|
||||
Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
expanded_chars += 1;
|
||||
expanded_bytes += c.len_utf8();
|
||||
}
|
||||
|
||||
if expanded_bytes > column && matches!(bias, Bias::Left) {
|
||||
expanded_chars -= 1;
|
||||
break;
|
||||
}
|
||||
|
||||
collapsed_bytes += c.len_utf8();
|
||||
}
|
||||
(collapsed_bytes, expanded_chars, 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct TabPoint(pub super::Point);
|
||||
|
||||
impl TabPoint {
|
||||
pub fn new(row: u32, column: u32) -> Self {
|
||||
Self(super::Point::new(row, column))
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self::new(0, 0)
|
||||
}
|
||||
|
||||
pub fn row(self) -> u32 {
|
||||
self.0.row
|
||||
}
|
||||
|
||||
pub fn column(self) -> u32 {
|
||||
self.0.column
|
||||
}
|
||||
}
|
||||
|
||||
impl From<super::Point> for TabPoint {
|
||||
fn from(point: super::Point) -> Self {
|
||||
Self(point)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Edit {
|
||||
pub old_lines: Range<TabPoint>,
|
||||
pub new_lines: Range<TabPoint>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct TextSummary {
|
||||
pub lines: super::Point,
|
||||
pub first_line_chars: u32,
|
||||
pub last_line_chars: u32,
|
||||
pub longest_row: u32,
|
||||
pub longest_row_chars: u32,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for TextSummary {
|
||||
fn from(text: &'a str) -> Self {
|
||||
let sum = rope::TextSummary::from(text);
|
||||
|
||||
TextSummary {
|
||||
lines: sum.lines,
|
||||
first_line_chars: sum.first_line_chars,
|
||||
last_line_chars: sum.last_line_chars,
|
||||
longest_row: sum.longest_row,
|
||||
longest_row_chars: sum.longest_row_chars,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
|
||||
fn add_assign(&mut self, other: &'a Self) {
|
||||
let joined_chars = self.last_line_chars + other.first_line_chars;
|
||||
if joined_chars > self.longest_row_chars {
|
||||
self.longest_row = self.lines.row;
|
||||
self.longest_row_chars = joined_chars;
|
||||
}
|
||||
if other.longest_row_chars > self.longest_row_chars {
|
||||
self.longest_row = self.lines.row + other.longest_row;
|
||||
self.longest_row_chars = other.longest_row_chars;
|
||||
}
|
||||
|
||||
if self.lines.row == 0 {
|
||||
self.first_line_chars += other.first_line_chars;
|
||||
}
|
||||
|
||||
if other.lines.row == 0 {
|
||||
self.last_line_chars += other.first_line_chars;
|
||||
} else {
|
||||
self.last_line_chars = other.last_line_chars;
|
||||
}
|
||||
|
||||
self.lines += &other.lines;
|
||||
}
|
||||
}
|
||||
|
||||
// Handles a tab width <= 16
|
||||
const SPACES: &'static str = " ";
|
||||
|
||||
pub struct Chunks<'a> {
|
||||
fold_chunks: fold_map::Chunks<'a>,
|
||||
chunk: &'a str,
|
||||
column: usize,
|
||||
tab_size: usize,
|
||||
skip_leading_tab: bool,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Chunks<'a> {
|
||||
type Item = &'a str;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.chunk.is_empty() {
|
||||
if let Some(chunk) = self.fold_chunks.next() {
|
||||
self.chunk = chunk;
|
||||
if self.skip_leading_tab {
|
||||
self.chunk = &self.chunk[1..];
|
||||
self.skip_leading_tab = false;
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
for (ix, c) in self.chunk.char_indices() {
|
||||
match c {
|
||||
'\t' => {
|
||||
if ix > 0 {
|
||||
let (prefix, suffix) = self.chunk.split_at(ix);
|
||||
self.chunk = suffix;
|
||||
return Some(prefix);
|
||||
} else {
|
||||
self.chunk = &self.chunk[1..];
|
||||
let len = self.tab_size - self.column % self.tab_size;
|
||||
self.column += len;
|
||||
return Some(&SPACES[0..len]);
|
||||
}
|
||||
}
|
||||
'\n' => self.column = 0,
|
||||
_ => self.column += 1,
|
||||
}
|
||||
}
|
||||
|
||||
let result = Some(self.chunk);
|
||||
self.chunk = "";
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HighlightedChunks<'a> {
|
||||
fold_chunks: fold_map::HighlightedChunks<'a>,
|
||||
chunk: &'a str,
|
||||
style_id: HighlightId,
|
||||
column: usize,
|
||||
tab_size: usize,
|
||||
skip_leading_tab: bool,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for HighlightedChunks<'a> {
|
||||
type Item = (&'a str, HighlightId);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.chunk.is_empty() {
|
||||
if let Some((chunk, style_id)) = self.fold_chunks.next() {
|
||||
self.chunk = chunk;
|
||||
self.style_id = style_id;
|
||||
if self.skip_leading_tab {
|
||||
self.chunk = &self.chunk[1..];
|
||||
self.skip_leading_tab = false;
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
for (ix, c) in self.chunk.char_indices() {
|
||||
match c {
|
||||
'\t' => {
|
||||
if ix > 0 {
|
||||
let (prefix, suffix) = self.chunk.split_at(ix);
|
||||
self.chunk = suffix;
|
||||
return Some((prefix, self.style_id));
|
||||
} else {
|
||||
self.chunk = &self.chunk[1..];
|
||||
let len = self.tab_size - self.column % self.tab_size;
|
||||
self.column += len;
|
||||
return Some((&SPACES[0..len], self.style_id));
|
||||
}
|
||||
}
|
||||
'\n' => self.column = 0,
|
||||
_ => self.column += 1,
|
||||
}
|
||||
}
|
||||
|
||||
Some((mem::take(&mut self.chunk), mem::take(&mut self.style_id)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_expand_tabs() {
|
||||
assert_eq!(Snapshot::expand_tabs("\t".chars(), 0, 4), 0);
|
||||
assert_eq!(Snapshot::expand_tabs("\t".chars(), 1, 4), 4);
|
||||
assert_eq!(Snapshot::expand_tabs("\ta".chars(), 2, 4), 5);
|
||||
}
|
||||
}
|
1108
crates/zed/src/editor/display_map/wrap_map.rs
Normal file
1071
crates/zed/src/editor/element.rs
Normal file
257
crates/zed/src/editor/movement.rs
Normal file
|
@ -0,0 +1,257 @@
|
|||
use super::{Bias, DisplayMapSnapshot, DisplayPoint, SelectionGoal};
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn left(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
|
||||
if point.column() > 0 {
|
||||
*point.column_mut() -= 1;
|
||||
} else if point.row() > 0 {
|
||||
*point.row_mut() -= 1;
|
||||
*point.column_mut() = map.line_len(point.row());
|
||||
}
|
||||
Ok(map.clip_point(point, Bias::Left))
|
||||
}
|
||||
|
||||
pub fn right(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
|
||||
let max_column = map.line_len(point.row());
|
||||
if point.column() < max_column {
|
||||
*point.column_mut() += 1;
|
||||
} else if point.row() < map.max_point().row() {
|
||||
*point.row_mut() += 1;
|
||||
*point.column_mut() = 0;
|
||||
}
|
||||
Ok(map.clip_point(point, Bias::Right))
|
||||
}
|
||||
|
||||
pub fn up(
|
||||
map: &DisplayMapSnapshot,
|
||||
mut point: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
) -> Result<(DisplayPoint, SelectionGoal)> {
|
||||
let goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
} else {
|
||||
map.column_to_chars(point.row(), point.column())
|
||||
};
|
||||
|
||||
if point.row() > 0 {
|
||||
*point.row_mut() -= 1;
|
||||
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
|
||||
} else {
|
||||
point = DisplayPoint::new(0, 0);
|
||||
}
|
||||
|
||||
let clip_bias = if point.column() == map.line_len(point.row()) {
|
||||
Bias::Left
|
||||
} else {
|
||||
Bias::Right
|
||||
};
|
||||
|
||||
Ok((
|
||||
map.clip_point(point, clip_bias),
|
||||
SelectionGoal::Column(goal_column),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn down(
|
||||
map: &DisplayMapSnapshot,
|
||||
mut point: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
) -> Result<(DisplayPoint, SelectionGoal)> {
|
||||
let max_point = map.max_point();
|
||||
let goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
} else {
|
||||
map.column_to_chars(point.row(), point.column())
|
||||
};
|
||||
|
||||
if point.row() < max_point.row() {
|
||||
*point.row_mut() += 1;
|
||||
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
|
||||
} else {
|
||||
point = max_point;
|
||||
}
|
||||
|
||||
let clip_bias = if point.column() == map.line_len(point.row()) {
|
||||
Bias::Left
|
||||
} else {
|
||||
Bias::Right
|
||||
};
|
||||
|
||||
Ok((
|
||||
map.clip_point(point, clip_bias),
|
||||
SelectionGoal::Column(goal_column),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn line_beginning(
|
||||
map: &DisplayMapSnapshot,
|
||||
point: DisplayPoint,
|
||||
toggle_indent: bool,
|
||||
) -> Result<DisplayPoint> {
|
||||
let (indent, is_blank) = map.line_indent(point.row());
|
||||
if toggle_indent && !is_blank && point.column() != indent {
|
||||
Ok(DisplayPoint::new(point.row(), indent))
|
||||
} else {
|
||||
Ok(DisplayPoint::new(point.row(), 0))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line_end(map: &DisplayMapSnapshot, point: DisplayPoint) -> Result<DisplayPoint> {
|
||||
let line_end = DisplayPoint::new(point.row(), map.line_len(point.row()));
|
||||
Ok(map.clip_point(line_end, Bias::Left))
|
||||
}
|
||||
|
||||
pub fn prev_word_boundary(
|
||||
map: &DisplayMapSnapshot,
|
||||
mut point: DisplayPoint,
|
||||
) -> Result<DisplayPoint> {
|
||||
let mut line_start = 0;
|
||||
if point.row() > 0 {
|
||||
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
|
||||
line_start = indent;
|
||||
}
|
||||
}
|
||||
|
||||
if point.column() == line_start {
|
||||
if point.row() == 0 {
|
||||
return Ok(DisplayPoint::new(0, 0));
|
||||
} else {
|
||||
let row = point.row() - 1;
|
||||
point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
|
||||
}
|
||||
}
|
||||
|
||||
let mut boundary = DisplayPoint::new(point.row(), 0);
|
||||
let mut column = 0;
|
||||
let mut prev_char_kind = CharKind::Newline;
|
||||
for c in map.chars_at(DisplayPoint::new(point.row(), 0)) {
|
||||
if column >= point.column() {
|
||||
break;
|
||||
}
|
||||
|
||||
let char_kind = char_kind(c);
|
||||
if char_kind != prev_char_kind
|
||||
&& char_kind != CharKind::Whitespace
|
||||
&& char_kind != CharKind::Newline
|
||||
{
|
||||
*boundary.column_mut() = column;
|
||||
}
|
||||
|
||||
prev_char_kind = char_kind;
|
||||
column += c.len_utf8() as u32;
|
||||
}
|
||||
Ok(boundary)
|
||||
}
|
||||
|
||||
pub fn next_word_boundary(
|
||||
map: &DisplayMapSnapshot,
|
||||
mut point: DisplayPoint,
|
||||
) -> Result<DisplayPoint> {
|
||||
let mut prev_char_kind = None;
|
||||
for c in map.chars_at(point) {
|
||||
let char_kind = char_kind(c);
|
||||
if let Some(prev_char_kind) = prev_char_kind {
|
||||
if c == '\n' {
|
||||
break;
|
||||
}
|
||||
if prev_char_kind != char_kind
|
||||
&& prev_char_kind != CharKind::Whitespace
|
||||
&& prev_char_kind != CharKind::Newline
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if c == '\n' {
|
||||
*point.row_mut() += 1;
|
||||
*point.column_mut() = 0;
|
||||
} else {
|
||||
*point.column_mut() += c.len_utf8() as u32;
|
||||
}
|
||||
prev_char_kind = Some(char_kind);
|
||||
}
|
||||
Ok(point)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
enum CharKind {
|
||||
Newline,
|
||||
Whitespace,
|
||||
Punctuation,
|
||||
Word,
|
||||
}
|
||||
|
||||
fn char_kind(c: char) -> CharKind {
|
||||
if c == '\n' {
|
||||
CharKind::Newline
|
||||
} else if c.is_whitespace() {
|
||||
CharKind::Whitespace
|
||||
} else if c.is_alphanumeric() || c == '_' {
|
||||
CharKind::Word
|
||||
} else {
|
||||
CharKind::Punctuation
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::editor::{display_map::DisplayMap, Buffer};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
|
||||
let tab_size = 4;
|
||||
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
|
||||
let font_id = cx
|
||||
.font_cache()
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
let font_size = 14.0;
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "a bcΔ defγ hi—jk", cx));
|
||||
let display_map =
|
||||
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)).unwrap(),
|
||||
DisplayPoint::new(0, 7)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
|
||||
DisplayPoint::new(0, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
|
||||
DisplayPoint::new(0, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
|
||||
DisplayPoint::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
|
||||
DisplayPoint::new(0, 0)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 0)).unwrap(),
|
||||
DisplayPoint::new(0, 1)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
|
||||
DisplayPoint::new(0, 6)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
|
||||
DisplayPoint::new(0, 6)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
|
||||
DisplayPoint::new(0, 12)
|
||||
);
|
||||
assert_eq!(
|
||||
next_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
|
||||
DisplayPoint::new(0, 12)
|
||||
);
|
||||
}
|
||||
}
|
676
crates/zed/src/file_finder.rs
Normal file
|
@ -0,0 +1,676 @@
|
|||
use crate::{
|
||||
editor::{self, Editor},
|
||||
fuzzy::PathMatch,
|
||||
project::{Project, ProjectPath},
|
||||
settings::Settings,
|
||||
workspace::Workspace,
|
||||
};
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
keymap::{
|
||||
self,
|
||||
menu::{SelectNext, SelectPrev},
|
||||
Binding,
|
||||
},
|
||||
AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use std::{
|
||||
cmp,
|
||||
path::Path,
|
||||
sync::{
|
||||
atomic::{self, AtomicBool},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use util::post_inc;
|
||||
|
||||
pub struct FileFinder {
|
||||
handle: WeakViewHandle<Self>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
project: ModelHandle<Project>,
|
||||
query_editor: ViewHandle<Editor>,
|
||||
search_count: usize,
|
||||
latest_search_id: usize,
|
||||
latest_search_did_cancel: bool,
|
||||
latest_search_query: String,
|
||||
matches: Vec<PathMatch>,
|
||||
selected: Option<(usize, Arc<Path>)>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
list_state: UniformListState,
|
||||
}
|
||||
|
||||
action!(Toggle);
|
||||
action!(Confirm);
|
||||
action!(Select, ProjectPath);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(FileFinder::toggle);
|
||||
cx.add_action(FileFinder::confirm);
|
||||
cx.add_action(FileFinder::select);
|
||||
cx.add_action(FileFinder::select_prev);
|
||||
cx.add_action(FileFinder::select_next);
|
||||
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("cmd-p", Toggle, None),
|
||||
Binding::new("escape", Toggle, Some("FileFinder")),
|
||||
Binding::new("enter", Confirm, Some("FileFinder")),
|
||||
]);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Selected(ProjectPath),
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
impl Entity for FileFinder {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for FileFinder {
|
||||
fn ui_name() -> &'static str {
|
||||
"FileFinder"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
Container::new(ChildView::new(self.query_editor.id()).boxed())
|
||||
.with_style(settings.theme.selector.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Flexible::new(1.0, self.render_matches()).boxed())
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(settings.theme.selector.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_max_width(500.0)
|
||||
.with_max_height(420.0)
|
||||
.boxed(),
|
||||
)
|
||||
.top()
|
||||
.named("file finder")
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.focus(&self.query_editor);
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx
|
||||
}
|
||||
}
|
||||
|
||||
impl FileFinder {
|
||||
fn render_matches(&self) -> ElementBox {
|
||||
if self.matches.is_empty() {
|
||||
let settings = self.settings.borrow();
|
||||
return Container::new(
|
||||
Label::new(
|
||||
"No matches".into(),
|
||||
settings.theme.selector.empty.label.clone(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(settings.theme.selector.empty.container)
|
||||
.named("empty matches");
|
||||
}
|
||||
|
||||
let handle = self.handle.clone();
|
||||
let list = UniformList::new(
|
||||
self.list_state.clone(),
|
||||
self.matches.len(),
|
||||
move |mut range, items, cx| {
|
||||
let cx = cx.as_ref();
|
||||
let finder = handle.upgrade(cx).unwrap();
|
||||
let finder = finder.read(cx);
|
||||
let start = range.start;
|
||||
range.end = cmp::min(range.end, finder.matches.len());
|
||||
items.extend(
|
||||
finder.matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(i, path_match)| finder.render_match(path_match, start + i)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Container::new(list.boxed())
|
||||
.with_margin_top(6.0)
|
||||
.named("matches")
|
||||
}
|
||||
|
||||
fn render_match(&self, path_match: &PathMatch, index: usize) -> ElementBox {
|
||||
let selected_index = self.selected_index();
|
||||
let settings = self.settings.borrow();
|
||||
let style = if index == selected_index {
|
||||
&settings.theme.selector.active_item
|
||||
} else {
|
||||
&settings.theme.selector.item
|
||||
};
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||
self.labels_for_match(path_match);
|
||||
let container = Container::new(
|
||||
Flex::row()
|
||||
// .with_child(
|
||||
// Container::new(
|
||||
// LineBox::new(
|
||||
// Svg::new("icons/file-16.svg")
|
||||
// .with_color(style.label.text.color)
|
||||
// .boxed(),
|
||||
// style.label.text.clone(),
|
||||
// )
|
||||
// .boxed(),
|
||||
// )
|
||||
// .with_padding_right(6.0)
|
||||
// .boxed(),
|
||||
// )
|
||||
.with_child(
|
||||
Flexible::new(
|
||||
1.0,
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(file_name.to_string(), style.label.clone())
|
||||
.with_highlights(file_name_positions)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(full_path, style.label.clone())
|
||||
.with_highlights(full_path_positions)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(style.container);
|
||||
|
||||
let action = Select(ProjectPath {
|
||||
worktree_id: path_match.worktree_id,
|
||||
path: path_match.path.clone(),
|
||||
});
|
||||
EventHandler::new(container.boxed())
|
||||
.on_mouse_down(move |cx| {
|
||||
cx.dispatch_action(action.clone());
|
||||
true
|
||||
})
|
||||
.named("match")
|
||||
}
|
||||
|
||||
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
|
||||
let path_string = path_match.path.to_string_lossy();
|
||||
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
|
||||
let path_positions = path_match.positions.clone();
|
||||
|
||||
let file_name = path_match.path.file_name().map_or_else(
|
||||
|| path_match.path_prefix.to_string(),
|
||||
|file_name| file_name.to_string_lossy().to_string(),
|
||||
);
|
||||
let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
|
||||
- file_name.chars().count();
|
||||
let file_name_positions = path_positions
|
||||
.iter()
|
||||
.filter_map(|pos| {
|
||||
if pos >= &file_name_start {
|
||||
Some(pos - file_name_start)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
(file_name, file_name_positions, full_path, path_positions)
|
||||
}
|
||||
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |cx, workspace| {
|
||||
let project = workspace.project().clone();
|
||||
let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), project, cx));
|
||||
cx.subscribe(&finder, Self::on_event).detach();
|
||||
finder
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
workspace: &mut Workspace,
|
||||
_: ViewHandle<FileFinder>,
|
||||
event: &Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
Event::Selected(project_path) => {
|
||||
workspace
|
||||
.open_entry(project_path.clone(), cx)
|
||||
.map(|d| d.detach());
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
Event::Dismissed => {
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
settings: watch::Receiver<Settings>,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&project, Self::project_updated).detach();
|
||||
|
||||
let query_editor = cx.add_view(|cx| {
|
||||
Editor::single_line(
|
||||
settings.clone(),
|
||||
{
|
||||
let settings = settings.clone();
|
||||
move |_| settings.borrow().theme.selector.input_editor.as_editor()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&query_editor, Self::on_query_editor_event)
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
handle: cx.handle().downgrade(),
|
||||
settings,
|
||||
project,
|
||||
query_editor,
|
||||
search_count: 0,
|
||||
latest_search_id: 0,
|
||||
latest_search_did_cancel: false,
|
||||
latest_search_query: String::new(),
|
||||
matches: Vec::new(),
|
||||
selected: None,
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
list_state: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn project_updated(&mut self, _: ModelHandle<Project>, cx: &mut ViewContext<Self>) {
|
||||
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
|
||||
if let Some(task) = self.spawn_search(query, cx) {
|
||||
task.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn on_query_editor_event(
|
||||
&mut self,
|
||||
_: ViewHandle<Editor>,
|
||||
event: &editor::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Edited => {
|
||||
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
|
||||
if query.is_empty() {
|
||||
self.latest_search_id = post_inc(&mut self.search_count);
|
||||
self.matches.clear();
|
||||
cx.notify();
|
||||
} else {
|
||||
if let Some(task) = self.spawn_search(query, cx) {
|
||||
task.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
if let Some(selected) = self.selected.as_ref() {
|
||||
for (ix, path_match) in self.matches.iter().enumerate() {
|
||||
if (path_match.worktree_id, path_match.path.as_ref())
|
||||
== (selected.0, selected.1.as_ref())
|
||||
{
|
||||
return ix;
|
||||
}
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
let mut selected_index = self.selected_index();
|
||||
if selected_index > 0 {
|
||||
selected_index -= 1;
|
||||
let mat = &self.matches[selected_index];
|
||||
self.selected = Some((mat.worktree_id, mat.path.clone()));
|
||||
}
|
||||
self.list_state.scroll_to(selected_index);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
let mut selected_index = self.selected_index();
|
||||
if selected_index + 1 < self.matches.len() {
|
||||
selected_index += 1;
|
||||
let mat = &self.matches[selected_index];
|
||||
self.selected = Some((mat.worktree_id, mat.path.clone()));
|
||||
}
|
||||
self.list_state.scroll_to(selected_index);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some(m) = self.matches.get(self.selected_index()) {
|
||||
cx.emit(Event::Selected(ProjectPath {
|
||||
worktree_id: m.worktree_id,
|
||||
path: m.path.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn select(&mut self, Select(project_path): &Select, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Selected(project_path.clone()));
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Option<Task<()>> {
|
||||
let search_id = util::post_inc(&mut self.search_count);
|
||||
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
|
||||
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let cancel_flag = self.cancel_flag.clone();
|
||||
let project = self.project.clone();
|
||||
Some(cx.spawn(|this, mut cx| async move {
|
||||
let matches = project
|
||||
.read_with(&cx, |project, cx| {
|
||||
project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
|
||||
})
|
||||
.await;
|
||||
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update_matches((search_id, did_cancel, query, matches), cx)
|
||||
});
|
||||
}))
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
(search_id, did_cancel, query, matches): (usize, bool, String, Vec<PathMatch>),
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if search_id >= self.latest_search_id {
|
||||
self.latest_search_id = search_id;
|
||||
if self.latest_search_did_cancel && query == self.latest_search_query {
|
||||
util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
|
||||
} else {
|
||||
self.matches = matches;
|
||||
}
|
||||
self.latest_search_query = query;
|
||||
self.latest_search_did_cancel = did_cancel;
|
||||
self.list_state.scroll_to(self.selected_index());
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
editor::{self, Insert},
|
||||
test::test_app_state,
|
||||
workspace::Workspace,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use worktree::fs::FakeFs;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_paths(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"a": {
|
||||
"banana": "",
|
||||
"bandana": "",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
cx.update(|cx| {
|
||||
super::init(cx);
|
||||
editor::init(cx);
|
||||
});
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
cx.dispatch_action(window_id, vec![workspace.id()], Toggle);
|
||||
|
||||
let finder = cx.read(|cx| {
|
||||
workspace
|
||||
.read(cx)
|
||||
.modal()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.downcast::<FileFinder>()
|
||||
.unwrap()
|
||||
});
|
||||
let query_buffer = cx.read(|cx| finder.read(cx).query_editor.clone());
|
||||
|
||||
let chain = vec![finder.id(), query_buffer.id()];
|
||||
cx.dispatch_action(window_id, chain.clone(), Insert("b".into()));
|
||||
cx.dispatch_action(window_id, chain.clone(), Insert("n".into()));
|
||||
cx.dispatch_action(window_id, chain.clone(), Insert("a".into()));
|
||||
finder
|
||||
.condition(&cx, |finder, _| finder.matches.len() == 2)
|
||||
.await;
|
||||
|
||||
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
|
||||
cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], SelectNext);
|
||||
cx.dispatch_action(window_id, vec![workspace.id(), finder.id()], Confirm);
|
||||
active_pane
|
||||
.condition(&cx, |pane, _| pane.active_item().is_some())
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let active_item = active_pane.read(cx).active_item().unwrap();
|
||||
assert_eq!(active_item.title(cx), "bandana");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_cancellation(mut cx: gpui::TestAppContext) {
|
||||
let fs = Arc::new(FakeFs::new());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
"hello": "",
|
||||
"goodbye": "",
|
||||
"halogen-light": "",
|
||||
"happiness": "",
|
||||
"height": "",
|
||||
"hi": "",
|
||||
"hiccup": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut app_state = cx.update(test_app_state);
|
||||
Arc::get_mut(&mut app_state).unwrap().fs = fs;
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree("/dir".as_ref(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
let (_, finder) = cx.add_window(|cx| {
|
||||
FileFinder::new(
|
||||
app_state.settings.clone(),
|
||||
workspace.read(cx).project().clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let query = "hi".to_string();
|
||||
finder
|
||||
.update(&mut cx, |f, cx| f.spawn_search(query.clone(), cx))
|
||||
.unwrap()
|
||||
.await;
|
||||
finder.read_with(&cx, |f, _| assert_eq!(f.matches.len(), 5));
|
||||
|
||||
finder.update(&mut cx, |finder, cx| {
|
||||
let matches = finder.matches.clone();
|
||||
|
||||
// Simulate a search being cancelled after the time limit,
|
||||
// returning only a subset of the matches that would have been found.
|
||||
finder.spawn_search(query.clone(), cx).unwrap().detach();
|
||||
finder.update_matches(
|
||||
(
|
||||
finder.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
vec![matches[1].clone(), matches[3].clone()],
|
||||
),
|
||||
cx,
|
||||
);
|
||||
|
||||
// Simulate another cancellation.
|
||||
finder.spawn_search(query.clone(), cx).unwrap().detach();
|
||||
finder.update_matches(
|
||||
(
|
||||
finder.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
|
||||
),
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(finder.matches, matches[0..4])
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_single_file_worktrees(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
|
||||
.await;
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_worktree(Path::new("/root/the-parent-dir/the-file"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
let (_, finder) = cx.add_window(|cx| {
|
||||
FileFinder::new(
|
||||
app_state.settings.clone(),
|
||||
workspace.read(cx).project().clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Even though there is only one worktree, that worktree's filename
|
||||
// is included in the matching, because the worktree is a single file.
|
||||
finder
|
||||
.update(&mut cx, |f, cx| f.spawn_search("thf".into(), cx))
|
||||
.unwrap()
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let finder = finder.read(cx);
|
||||
assert_eq!(finder.matches.len(), 1);
|
||||
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||
finder.labels_for_match(&finder.matches[0]);
|
||||
assert_eq!(file_name, "the-file");
|
||||
assert_eq!(file_name_positions, &[0, 1, 4]);
|
||||
assert_eq!(full_path, "the-file");
|
||||
assert_eq!(full_path_positions, &[0, 1, 4]);
|
||||
});
|
||||
|
||||
// Since the worktree root is a file, searching for its name followed by a slash does
|
||||
// not match anything.
|
||||
finder
|
||||
.update(&mut cx, |f, cx| f.spawn_search("thf/".into(), cx))
|
||||
.unwrap()
|
||||
.await;
|
||||
finder.read_with(&cx, |f, _| assert_eq!(f.matches.len(), 0));
|
||||
}
|
||||
|
||||
#[gpui::test(retries = 5)]
|
||||
async fn test_multiple_matches_with_same_relative_path(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"dir1": { "a.txt": "" },
|
||||
"dir2": { "a.txt": "" }
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.open_paths(
|
||||
&[PathBuf::from("/root/dir1"), PathBuf::from("/root/dir2")],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
|
||||
.await;
|
||||
|
||||
let (_, finder) = cx.add_window(|cx| {
|
||||
FileFinder::new(
|
||||
app_state.settings.clone(),
|
||||
workspace.read(cx).project().clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Run a search that matches two files with the same relative path.
|
||||
finder
|
||||
.update(&mut cx, |f, cx| f.spawn_search("a.t".into(), cx))
|
||||
.unwrap()
|
||||
.await;
|
||||
|
||||
// Can switch between different matches with the same relative path.
|
||||
finder.update(&mut cx, |f, cx| {
|
||||
assert_eq!(f.matches.len(), 2);
|
||||
assert_eq!(f.selected_index(), 0);
|
||||
f.select_next(&SelectNext, cx);
|
||||
assert_eq!(f.selected_index(), 1);
|
||||
f.select_prev(&SelectPrev, cx);
|
||||
assert_eq!(f.selected_index(), 0);
|
||||
});
|
||||
}
|
||||
}
|
173
crates/zed/src/fuzzy.rs
Normal file
|
@ -0,0 +1,173 @@
|
|||
use gpui::executor;
|
||||
use std::{
|
||||
cmp,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use util;
|
||||
use worktree::{EntryKind, Snapshot};
|
||||
|
||||
pub use fuzzy::*;
|
||||
|
||||
pub async fn match_strings(
|
||||
candidates: &[StringMatchCandidate],
|
||||
query: &str,
|
||||
smart_case: bool,
|
||||
max_results: usize,
|
||||
cancel_flag: &AtomicBool,
|
||||
background: Arc<executor::Background>,
|
||||
) -> Vec<StringMatch> {
|
||||
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
||||
let query = query.chars().collect::<Vec<_>>();
|
||||
|
||||
let lowercase_query = &lowercase_query;
|
||||
let query = &query;
|
||||
let query_char_bag = CharBag::from(&lowercase_query[..]);
|
||||
|
||||
let num_cpus = background.num_cpus().min(candidates.len());
|
||||
let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
|
||||
let mut segment_results = (0..num_cpus)
|
||||
.map(|_| Vec::with_capacity(max_results))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
background
|
||||
.scoped(|scope| {
|
||||
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
|
||||
let cancel_flag = &cancel_flag;
|
||||
scope.spawn(async move {
|
||||
let segment_start = segment_idx * segment_size;
|
||||
let segment_end = segment_start + segment_size;
|
||||
let mut matcher = Matcher::new(
|
||||
query,
|
||||
lowercase_query,
|
||||
query_char_bag,
|
||||
smart_case,
|
||||
max_results,
|
||||
);
|
||||
matcher.match_strings(
|
||||
&candidates[segment_start..segment_end],
|
||||
results,
|
||||
cancel_flag,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for segment_result in segment_results {
|
||||
if results.is_empty() {
|
||||
results = segment_result;
|
||||
} else {
|
||||
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(&a));
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn match_paths(
|
||||
snapshots: &[Snapshot],
|
||||
query: &str,
|
||||
include_ignored: bool,
|
||||
smart_case: bool,
|
||||
max_results: usize,
|
||||
cancel_flag: &AtomicBool,
|
||||
background: Arc<executor::Background>,
|
||||
) -> Vec<PathMatch> {
|
||||
let path_count: usize = if include_ignored {
|
||||
snapshots.iter().map(Snapshot::file_count).sum()
|
||||
} else {
|
||||
snapshots.iter().map(Snapshot::visible_file_count).sum()
|
||||
};
|
||||
if path_count == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
||||
let query = query.chars().collect::<Vec<_>>();
|
||||
|
||||
let lowercase_query = &lowercase_query;
|
||||
let query = &query;
|
||||
let query_char_bag = CharBag::from(&lowercase_query[..]);
|
||||
|
||||
let num_cpus = background.num_cpus().min(path_count);
|
||||
let segment_size = (path_count + num_cpus - 1) / num_cpus;
|
||||
let mut segment_results = (0..num_cpus)
|
||||
.map(|_| Vec::with_capacity(max_results))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
background
|
||||
.scoped(|scope| {
|
||||
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
|
||||
scope.spawn(async move {
|
||||
let segment_start = segment_idx * segment_size;
|
||||
let segment_end = segment_start + segment_size;
|
||||
let mut matcher = Matcher::new(
|
||||
query,
|
||||
lowercase_query,
|
||||
query_char_bag,
|
||||
smart_case,
|
||||
max_results,
|
||||
);
|
||||
|
||||
let mut tree_start = 0;
|
||||
for snapshot in snapshots {
|
||||
let tree_end = if include_ignored {
|
||||
tree_start + snapshot.file_count()
|
||||
} else {
|
||||
tree_start + snapshot.visible_file_count()
|
||||
};
|
||||
|
||||
if tree_start < segment_end && segment_start < tree_end {
|
||||
let path_prefix: Arc<str> =
|
||||
if snapshot.root_entry().map_or(false, |e| e.is_file()) {
|
||||
snapshot.root_name().into()
|
||||
} else if snapshots.len() > 1 {
|
||||
format!("{}/", snapshot.root_name()).into()
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
|
||||
let start = cmp::max(tree_start, segment_start) - tree_start;
|
||||
let end = cmp::min(tree_end, segment_end) - tree_start;
|
||||
let paths = snapshot
|
||||
.files(include_ignored, start)
|
||||
.take(end - start)
|
||||
.map(|entry| {
|
||||
if let EntryKind::File(char_bag) = entry.kind {
|
||||
PathMatchCandidate {
|
||||
path: &entry.path,
|
||||
char_bag,
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
|
||||
matcher.match_paths(
|
||||
snapshot.id(),
|
||||
path_prefix,
|
||||
paths,
|
||||
results,
|
||||
&cancel_flag,
|
||||
);
|
||||
}
|
||||
if tree_end >= segment_end {
|
||||
break;
|
||||
}
|
||||
tree_start = tree_end;
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for segment_result in segment_results {
|
||||
if results.is_empty() {
|
||||
results = segment_result;
|
||||
} else {
|
||||
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(&a));
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
26
crates/zed/src/http.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
pub use anyhow::{anyhow, Result};
|
||||
use futures::future::BoxFuture;
|
||||
use std::sync::Arc;
|
||||
pub use surf::{
|
||||
http::{Method, Response as ServerResponse},
|
||||
Request, Response, Url,
|
||||
};
|
||||
|
||||
pub trait HttpClient: Send + Sync {
|
||||
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>>;
|
||||
}
|
||||
|
||||
pub fn client() -> Arc<dyn HttpClient> {
|
||||
Arc::new(surf::client())
|
||||
}
|
||||
|
||||
impl HttpClient for surf::Client {
|
||||
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>> {
|
||||
Box::pin(async move {
|
||||
Ok(self
|
||||
.send(req)
|
||||
.await
|
||||
.map_err(|e| anyhow!("http request failed: {}", e))?)
|
||||
})
|
||||
}
|
||||
}
|
36
crates/zed/src/language.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use buffer::{HighlightMap, Language, LanguageRegistry};
|
||||
use parking_lot::Mutex;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{str, sync::Arc};
|
||||
use tree_sitter::Query;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "languages"]
|
||||
struct LanguageDir;
|
||||
|
||||
pub fn build_language_registry() -> LanguageRegistry {
|
||||
let mut languages = LanguageRegistry::default();
|
||||
languages.add(Arc::new(rust()));
|
||||
languages
|
||||
}
|
||||
|
||||
pub fn rust() -> Language {
|
||||
let grammar = tree_sitter_rust::language();
|
||||
let rust_config =
|
||||
toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap();
|
||||
Language {
|
||||
config: rust_config,
|
||||
grammar,
|
||||
highlight_query: load_query(grammar, "rust/highlights.scm"),
|
||||
brackets_query: load_query(grammar, "rust/brackets.scm"),
|
||||
highlight_map: Mutex::new(HighlightMap::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_query(grammar: tree_sitter::Language, path: &str) -> Query {
|
||||
Query::new(
|
||||
grammar,
|
||||
str::from_utf8(&LanguageDir::get(path).unwrap().data).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
82
crates/zed/src/lib.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
pub mod assets;
|
||||
pub mod channel;
|
||||
pub mod chat_panel;
|
||||
pub mod editor;
|
||||
pub mod file_finder;
|
||||
mod fuzzy;
|
||||
pub mod http;
|
||||
pub mod language;
|
||||
pub mod menus;
|
||||
pub mod people_panel;
|
||||
pub mod project;
|
||||
pub mod project_panel;
|
||||
pub mod settings;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
pub mod theme;
|
||||
pub mod theme_selector;
|
||||
pub mod user;
|
||||
pub mod workspace;
|
||||
|
||||
pub use buffer;
|
||||
use buffer::LanguageRegistry;
|
||||
use channel::ChannelList;
|
||||
use gpui::{action, keymap::Binding, ModelHandle};
|
||||
use parking_lot::Mutex;
|
||||
use postage::watch;
|
||||
pub use rpc_client as rpc;
|
||||
pub use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use util::TryFutureExt;
|
||||
pub use worktree::{self, fs};
|
||||
|
||||
action!(About);
|
||||
action!(Quit);
|
||||
action!(Authenticate);
|
||||
action!(AdjustBufferFontSize, f32);
|
||||
|
||||
const MIN_FONT_SIZE: f32 = 6.0;
|
||||
|
||||
pub struct AppState {
|
||||
pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
|
||||
pub settings: watch::Receiver<Settings>,
|
||||
pub languages: Arc<LanguageRegistry>,
|
||||
pub themes: Arc<settings::ThemeRegistry>,
|
||||
pub rpc: Arc<rpc::Client>,
|
||||
pub user_store: ModelHandle<user::UserStore>,
|
||||
pub fs: Arc<dyn fs::Fs>,
|
||||
pub channel_list: ModelHandle<ChannelList>,
|
||||
}
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||
cx.add_global_action(quit);
|
||||
|
||||
cx.add_global_action({
|
||||
let rpc = app_state.rpc.clone();
|
||||
move |_: &Authenticate, cx| {
|
||||
let rpc = rpc.clone();
|
||||
cx.spawn(|cx| async move { rpc.authenticate_and_connect(&cx).log_err().await })
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
|
||||
cx.add_global_action({
|
||||
let settings_tx = app_state.settings_tx.clone();
|
||||
|
||||
move |action: &AdjustBufferFontSize, cx| {
|
||||
let mut settings_tx = settings_tx.lock();
|
||||
let new_size = (settings_tx.borrow().buffer_font_size + action.0).max(MIN_FONT_SIZE);
|
||||
settings_tx.borrow_mut().buffer_font_size = new_size;
|
||||
cx.refresh_windows();
|
||||
}
|
||||
});
|
||||
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("cmd-=", AdjustBufferFontSize(1.), None),
|
||||
Binding::new("cmd--", AdjustBufferFontSize(-1.), None),
|
||||
])
|
||||
}
|
||||
|
||||
fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
|
||||
cx.platform().quit();
|
||||
}
|
113
crates/zed/src/main.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
// Allow binary to be called Zed for a nice application menu when running executable direcly
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use fs::OpenOptions;
|
||||
use gpui::AssetSource;
|
||||
use log::LevelFilter;
|
||||
use parking_lot::Mutex;
|
||||
use simplelog::SimpleLogger;
|
||||
use std::{fs, path::PathBuf, sync::Arc};
|
||||
use zed::{
|
||||
self,
|
||||
assets::Assets,
|
||||
channel::ChannelList,
|
||||
chat_panel, editor, file_finder,
|
||||
fs::RealFs,
|
||||
http, language, menus, project_panel, rpc, settings, theme_selector,
|
||||
user::UserStore,
|
||||
workspace::{self, OpenNew, OpenParams, OpenPaths},
|
||||
AppState,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
init_logger();
|
||||
|
||||
let app = gpui::App::new(Assets).unwrap();
|
||||
let embedded_fonts = Assets
|
||||
.list("fonts")
|
||||
.into_iter()
|
||||
.map(|f| Arc::new(Assets.load(&f).unwrap().to_vec()))
|
||||
.collect::<Vec<_>>();
|
||||
app.platform().fonts().add_fonts(&embedded_fonts).unwrap();
|
||||
|
||||
let themes = settings::ThemeRegistry::new(Assets, app.font_cache());
|
||||
let (settings_tx, settings) = settings::channel(&app.font_cache(), &themes).unwrap();
|
||||
let languages = Arc::new(language::build_language_registry());
|
||||
languages.set_theme(&settings.borrow().theme.syntax);
|
||||
|
||||
app.run(move |cx| {
|
||||
let rpc = rpc::Client::new();
|
||||
let http = http::client();
|
||||
let user_store = cx.add_model(|cx| UserStore::new(rpc.clone(), http.clone(), cx));
|
||||
let app_state = Arc::new(AppState {
|
||||
languages: languages.clone(),
|
||||
settings_tx: Arc::new(Mutex::new(settings_tx)),
|
||||
settings,
|
||||
themes,
|
||||
channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)),
|
||||
rpc,
|
||||
user_store,
|
||||
fs: Arc::new(RealFs),
|
||||
});
|
||||
|
||||
zed::init(&app_state, cx);
|
||||
workspace::init(cx);
|
||||
editor::init(cx);
|
||||
file_finder::init(cx);
|
||||
chat_panel::init(cx);
|
||||
project_panel::init(cx);
|
||||
theme_selector::init(&app_state, cx);
|
||||
|
||||
cx.set_menus(menus::menus(&app_state.clone()));
|
||||
|
||||
if stdout_is_a_pty() {
|
||||
cx.platform().activate(true);
|
||||
}
|
||||
|
||||
let paths = collect_path_args();
|
||||
if paths.is_empty() {
|
||||
cx.dispatch_global_action(OpenNew(app_state));
|
||||
} else {
|
||||
cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
let level = LevelFilter::Info;
|
||||
|
||||
if stdout_is_a_pty() {
|
||||
SimpleLogger::init(level, Default::default()).expect("could not initialize logger");
|
||||
} else {
|
||||
let log_dir_path = dirs::home_dir()
|
||||
.expect("could not locate home directory for logging")
|
||||
.join("Library/Logs/");
|
||||
let log_file_path = log_dir_path.join("Zed.log");
|
||||
fs::create_dir_all(&log_dir_path).expect("could not create log directory");
|
||||
let log_file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(log_file_path)
|
||||
.expect("could not open logfile");
|
||||
simplelog::WriteLogger::init(level, simplelog::Config::default(), log_file)
|
||||
.expect("could not initialize logger");
|
||||
log_panics::init();
|
||||
}
|
||||
}
|
||||
|
||||
fn stdout_is_a_pty() -> bool {
|
||||
unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 }
|
||||
}
|
||||
|
||||
fn collect_path_args() -> Vec<PathBuf> {
|
||||
std::env::args()
|
||||
.skip(1)
|
||||
.filter_map(|arg| match fs::canonicalize(arg) {
|
||||
Ok(path) => Some(path),
|
||||
Err(error) => {
|
||||
log::error!("error parsing path argument: {}", error);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
74
crates/zed/src/menus.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use crate::{workspace, AppState};
|
||||
use gpui::{Menu, MenuItem};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
|
||||
use crate::editor;
|
||||
|
||||
vec![
|
||||
Menu {
|
||||
name: "Zed",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "About Zed…",
|
||||
keystroke: None,
|
||||
action: Box::new(super::About),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Quit",
|
||||
keystroke: Some("cmd-q"),
|
||||
action: Box::new(super::Quit),
|
||||
},
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "File",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "New",
|
||||
keystroke: Some("cmd-n"),
|
||||
action: Box::new(workspace::OpenNew(state.clone())),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Open…",
|
||||
keystroke: Some("cmd-o"),
|
||||
action: Box::new(workspace::Open(state.clone())),
|
||||
},
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Edit",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "Undo",
|
||||
keystroke: Some("cmd-z"),
|
||||
action: Box::new(editor::Undo),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Redo",
|
||||
keystroke: Some("cmd-Z"),
|
||||
action: Box::new(editor::Redo),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Cut",
|
||||
keystroke: Some("cmd-x"),
|
||||
action: Box::new(editor::Cut),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Copy",
|
||||
keystroke: Some("cmd-c"),
|
||||
action: Box::new(editor::Copy),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Paste",
|
||||
keystroke: Some("cmd-v"),
|
||||
action: Box::new(editor::Paste),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
267
crates/zed/src/people_panel.rs
Normal file
|
@ -0,0 +1,267 @@
|
|||
use crate::{
|
||||
theme::Theme,
|
||||
user::{Collaborator, UserStore},
|
||||
Settings,
|
||||
};
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
platform::CursorStyle,
|
||||
Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View,
|
||||
ViewContext,
|
||||
};
|
||||
use postage::watch;
|
||||
|
||||
action!(JoinWorktree, u64);
|
||||
action!(LeaveWorktree, u64);
|
||||
action!(ShareWorktree, u64);
|
||||
action!(UnshareWorktree, u64);
|
||||
|
||||
pub struct PeoplePanel {
|
||||
collaborators: ListState,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
_maintain_collaborators: Subscription,
|
||||
}
|
||||
|
||||
impl PeoplePanel {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
collaborators: ListState::new(
|
||||
user_store.read(cx).collaborators().len(),
|
||||
Orientation::Top,
|
||||
1000.,
|
||||
{
|
||||
let user_store = user_store.clone();
|
||||
let settings = settings.clone();
|
||||
move |ix, cx| {
|
||||
let user_store = user_store.read(cx);
|
||||
let collaborators = user_store.collaborators().clone();
|
||||
let current_user_id = user_store.current_user().map(|user| user.id);
|
||||
Self::render_collaborator(
|
||||
&collaborators[ix],
|
||||
current_user_id,
|
||||
&settings.borrow().theme,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
_maintain_collaborators: cx.observe(&user_store, Self::update_collaborators),
|
||||
user_store,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_collaborators(&mut self, _: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) {
|
||||
self.collaborators
|
||||
.reset(self.user_store.read(cx).collaborators().len());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_collaborator(
|
||||
collaborator: &Collaborator,
|
||||
current_user_id: Option<u64>,
|
||||
theme: &Theme,
|
||||
cx: &mut LayoutContext,
|
||||
) -> ElementBox {
|
||||
let theme = &theme.people_panel;
|
||||
let worktree_count = collaborator.worktrees.len();
|
||||
let font_cache = cx.font_cache();
|
||||
let line_height = theme.unshared_worktree.name.text.line_height(font_cache);
|
||||
let cap_height = theme.unshared_worktree.name.text.cap_height(font_cache);
|
||||
let baseline_offset = theme
|
||||
.unshared_worktree
|
||||
.name
|
||||
.text
|
||||
.baseline_offset(font_cache)
|
||||
+ (theme.unshared_worktree.height - line_height) / 2.;
|
||||
let tree_branch_width = theme.tree_branch_width;
|
||||
let tree_branch_color = theme.tree_branch_color;
|
||||
let host_avatar_height = theme
|
||||
.host_avatar
|
||||
.width
|
||||
.or(theme.host_avatar.height)
|
||||
.unwrap_or(0.);
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_children(collaborator.user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
.with_style(theme.host_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(
|
||||
collaborator.user.github_login.clone(),
|
||||
theme.host_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.host_username.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed(),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.host_row_height)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(
|
||||
collaborator
|
||||
.worktrees
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, worktree)| {
|
||||
let worktree_id = worktree.id;
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Canvas::new(move |bounds, _, cx| {
|
||||
let start_x = bounds.min_x() + (bounds.width() / 2.)
|
||||
- (tree_branch_width / 2.);
|
||||
let end_x = bounds.max_x();
|
||||
let start_y = bounds.min_y();
|
||||
let end_y =
|
||||
bounds.min_y() + baseline_offset - (cap_height / 2.);
|
||||
|
||||
cx.scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, start_y),
|
||||
vec2f(
|
||||
start_x + tree_branch_width,
|
||||
if ix + 1 == worktree_count {
|
||||
end_y
|
||||
} else {
|
||||
bounds.max_y()
|
||||
},
|
||||
),
|
||||
),
|
||||
background: Some(tree_branch_color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
cx.scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, end_y),
|
||||
vec2f(end_x, end_y + tree_branch_width),
|
||||
),
|
||||
background: Some(tree_branch_color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
})
|
||||
.constrained()
|
||||
.with_width(host_avatar_height)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child({
|
||||
let is_host = Some(collaborator.user.id) == current_user_id;
|
||||
let is_guest = !is_host
|
||||
&& worktree
|
||||
.guests
|
||||
.iter()
|
||||
.any(|guest| Some(guest.id) == current_user_id);
|
||||
let is_shared = worktree.is_shared;
|
||||
|
||||
MouseEventHandler::new::<PeoplePanel, _, _, _>(
|
||||
worktree_id as usize,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
let style = match (worktree.is_shared, mouse_state.hovered)
|
||||
{
|
||||
(false, false) => &theme.unshared_worktree,
|
||||
(false, true) => &theme.hovered_unshared_worktree,
|
||||
(true, false) => &theme.shared_worktree,
|
||||
(true, true) => &theme.hovered_shared_worktree,
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
worktree.root_name.clone(),
|
||||
style.name.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
.with_style(style.name.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(worktree.guests.iter().filter_map(
|
||||
|participant| {
|
||||
participant.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
.with_style(style.guest_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
.with_margin_right(
|
||||
style.guest_avatar_spacing,
|
||||
)
|
||||
.boxed()
|
||||
})
|
||||
},
|
||||
))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(style.height)
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
.with_cursor_style(if is_host || is_shared {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::Arrow
|
||||
})
|
||||
.on_click(move |cx| {
|
||||
if is_shared {
|
||||
if is_host {
|
||||
cx.dispatch_action(UnshareWorktree(worktree_id));
|
||||
} else if is_guest {
|
||||
cx.dispatch_action(LeaveWorktree(worktree_id));
|
||||
} else {
|
||||
cx.dispatch_action(JoinWorktree(worktree_id))
|
||||
}
|
||||
} else if is_host {
|
||||
cx.dispatch_action(ShareWorktree(worktree_id));
|
||||
}
|
||||
})
|
||||
.expanded(1.0)
|
||||
.boxed()
|
||||
})
|
||||
.constrained()
|
||||
.with_height(theme.unshared_worktree.height)
|
||||
.boxed()
|
||||
}),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Event {}
|
||||
|
||||
impl Entity for PeoplePanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for PeoplePanel {
|
||||
fn ui_name() -> &'static str {
|
||||
"PeoplePanel"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme.people_panel;
|
||||
Container::new(List::new(self.collaborators.clone()).boxed())
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
}
|
||||
}
|
342
crates/zed/src/project.rs
Normal file
|
@ -0,0 +1,342 @@
|
|||
use crate::{
|
||||
fuzzy::{self, PathMatch},
|
||||
AppState,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use buffer::LanguageRegistry;
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
use rpc_client as rpc;
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use util::TryFutureExt as _;
|
||||
use worktree::{fs::Fs, Worktree};
|
||||
|
||||
pub struct Project {
|
||||
worktrees: Vec<ModelHandle<Worktree>>,
|
||||
active_entry: Option<ProjectEntry>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
rpc: Arc<rpc::Client>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
ActiveEntryChanged(Option<ProjectEntry>),
|
||||
WorktreeRemoved(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct ProjectPath {
|
||||
pub worktree_id: usize,
|
||||
pub path: Arc<Path>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ProjectEntry {
|
||||
pub worktree_id: usize,
|
||||
pub entry_id: usize,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn new(app_state: &AppState) -> Self {
|
||||
Self {
|
||||
worktrees: Default::default(),
|
||||
active_entry: None,
|
||||
languages: app_state.languages.clone(),
|
||||
rpc: app_state.rpc.clone(),
|
||||
fs: app_state.fs.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn worktrees(&self) -> &[ModelHandle<Worktree>] {
|
||||
&self.worktrees
|
||||
}
|
||||
|
||||
pub fn worktree_for_id(&self, id: usize) -> Option<ModelHandle<Worktree>> {
|
||||
self.worktrees
|
||||
.iter()
|
||||
.find(|worktree| worktree.id() == id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn add_local_worktree(
|
||||
&mut self,
|
||||
abs_path: &Path,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<Worktree>>> {
|
||||
let fs = self.fs.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let languages = self.languages.clone();
|
||||
let path = Arc::from(abs_path);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.add_worktree(worktree.clone(), cx);
|
||||
});
|
||||
Ok(worktree)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_remote_worktree(
|
||||
&mut self,
|
||||
remote_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<Worktree>>> {
|
||||
let rpc = self.rpc.clone();
|
||||
let languages = self.languages.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
rpc.authenticate_and_connect(&cx).await?;
|
||||
let worktree =
|
||||
Worktree::open_remote(rpc.clone(), remote_id, languages, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.subscribe(&worktree, move |this, _, event, cx| match event {
|
||||
worktree::Event::Closed => {
|
||||
this.close_remote_worktree(remote_id, cx);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
this.add_worktree(worktree.clone(), cx);
|
||||
});
|
||||
Ok(worktree)
|
||||
})
|
||||
}
|
||||
|
||||
fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
|
||||
cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
|
||||
self.worktrees.push(worktree);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||
let new_active_entry = entry.and_then(|project_path| {
|
||||
let worktree = self.worktree_for_id(project_path.worktree_id)?;
|
||||
let entry = worktree.read(cx).entry_for_path(project_path.path)?;
|
||||
Some(ProjectEntry {
|
||||
worktree_id: project_path.worktree_id,
|
||||
entry_id: entry.id,
|
||||
})
|
||||
});
|
||||
if new_active_entry != self.active_entry {
|
||||
self.active_entry = new_active_entry;
|
||||
cx.emit(Event::ActiveEntryChanged(new_active_entry));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_entry(&self) -> Option<ProjectEntry> {
|
||||
self.active_entry
|
||||
}
|
||||
|
||||
pub fn share_worktree(&self, remote_id: u64, cx: &mut ModelContext<Self>) {
|
||||
let rpc = self.rpc.clone();
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
rpc.authenticate_and_connect(&cx).await?;
|
||||
|
||||
let task = this.update(&mut cx, |this, cx| {
|
||||
for worktree in &this.worktrees {
|
||||
let task = worktree.update(cx, |worktree, cx| {
|
||||
worktree.as_local_mut().and_then(|worktree| {
|
||||
if worktree.remote_id() == Some(remote_id) {
|
||||
Some(worktree.share(cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
if task.is_some() {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn unshare_worktree(&mut self, remote_id: u64, cx: &mut ModelContext<Self>) {
|
||||
for worktree in &self.worktrees {
|
||||
if worktree.update(cx, |worktree, cx| {
|
||||
if let Some(worktree) = worktree.as_local_mut() {
|
||||
if worktree.remote_id() == Some(remote_id) {
|
||||
worktree.unshare(cx);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_remote_worktree(&mut self, id: u64, cx: &mut ModelContext<Self>) {
|
||||
self.worktrees.retain(|worktree| {
|
||||
let keep = worktree.update(cx, |worktree, cx| {
|
||||
if let Some(worktree) = worktree.as_remote_mut() {
|
||||
if worktree.remote_id() == id {
|
||||
worktree.close_all_buffers(cx);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
if !keep {
|
||||
cx.emit(Event::WorktreeRemoved(worktree.id()));
|
||||
}
|
||||
keep
|
||||
});
|
||||
}
|
||||
|
||||
pub fn match_paths<'a>(
|
||||
&self,
|
||||
query: &'a str,
|
||||
include_ignored: bool,
|
||||
smart_case: bool,
|
||||
max_results: usize,
|
||||
cancel_flag: &'a AtomicBool,
|
||||
cx: &AppContext,
|
||||
) -> impl 'a + Future<Output = Vec<PathMatch>> {
|
||||
let snapshots = self
|
||||
.worktrees
|
||||
.iter()
|
||||
.map(|worktree| worktree.read(cx).snapshot())
|
||||
.collect::<Vec<_>>();
|
||||
let background = cx.background().clone();
|
||||
|
||||
async move {
|
||||
fuzzy::match_paths(
|
||||
snapshots.as_slice(),
|
||||
query,
|
||||
include_ignored,
|
||||
smart_case,
|
||||
max_results,
|
||||
cancel_flag,
|
||||
background,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Project {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::test_app_state;
|
||||
use serde_json::json;
|
||||
use std::{os::unix, path::PathBuf};
|
||||
use util::test::temp_tree;
|
||||
use worktree::fs::RealFs;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_populate_and_search(mut cx: gpui::TestAppContext) {
|
||||
let mut app_state = cx.update(test_app_state);
|
||||
Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(RealFs);
|
||||
let dir = temp_tree(json!({
|
||||
"root": {
|
||||
"apple": "",
|
||||
"banana": {
|
||||
"carrot": {
|
||||
"date": "",
|
||||
"endive": "",
|
||||
}
|
||||
},
|
||||
"fennel": {
|
||||
"grape": "",
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let root_link_path = dir.path().join("root_link");
|
||||
unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
|
||||
unix::fs::symlink(
|
||||
&dir.path().join("root/fennel"),
|
||||
&dir.path().join("root/finnochio"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let project = cx.add_model(|_| Project::new(app_state.as_ref()));
|
||||
let tree = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.add_local_worktree(&root_link_path, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
assert_eq!(tree.file_count(), 5);
|
||||
assert_eq!(
|
||||
tree.inode_for_path("fennel/grape"),
|
||||
tree.inode_for_path("finnochio/grape")
|
||||
);
|
||||
});
|
||||
|
||||
let cancel_flag = Default::default();
|
||||
let results = project
|
||||
.read_with(&cx, |project, cx| {
|
||||
project.match_paths("bna", false, false, 10, &cancel_flag, cx)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
results
|
||||
.into_iter()
|
||||
.map(|result| result.path)
|
||||
.collect::<Vec<Arc<Path>>>(),
|
||||
vec![
|
||||
PathBuf::from("banana/carrot/date").into(),
|
||||
PathBuf::from("banana/carrot/endive").into(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) {
|
||||
let mut app_state = cx.update(test_app_state);
|
||||
Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(RealFs);
|
||||
let dir = temp_tree(json!({
|
||||
"root": {
|
||||
"dir1": {},
|
||||
"dir2": {
|
||||
"dir3": {}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let project = cx.add_model(|_| Project::new(app_state.as_ref()));
|
||||
let tree = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.add_local_worktree(&dir.path(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
let cancel_flag = Default::default();
|
||||
let results = project
|
||||
.read_with(&cx, |project, cx| {
|
||||
project.match_paths("dir", false, false, 10, &cancel_flag, cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
}
|
864
crates/zed/src/project_panel.rs
Normal file
|
@ -0,0 +1,864 @@
|
|||
use crate::{
|
||||
project::{self, Project, ProjectEntry, ProjectPath},
|
||||
theme,
|
||||
workspace::Workspace,
|
||||
Settings,
|
||||
};
|
||||
use gpui::{
|
||||
action,
|
||||
elements::{
|
||||
Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, Svg,
|
||||
UniformList, UniformListState,
|
||||
},
|
||||
keymap::{
|
||||
self,
|
||||
menu::{SelectNext, SelectPrev},
|
||||
Binding,
|
||||
},
|
||||
platform::CursorStyle,
|
||||
AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use std::{
|
||||
collections::{hash_map, HashMap},
|
||||
ffi::OsStr,
|
||||
ops::Range,
|
||||
};
|
||||
use worktree::Worktree;
|
||||
|
||||
pub struct ProjectPanel {
|
||||
project: ModelHandle<Project>,
|
||||
list: UniformListState,
|
||||
visible_entries: Vec<Vec<usize>>,
|
||||
expanded_dir_ids: HashMap<usize, Vec<usize>>,
|
||||
selection: Option<Selection>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
handle: WeakViewHandle<Self>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct Selection {
|
||||
worktree_id: usize,
|
||||
entry_id: usize,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct EntryDetails {
|
||||
filename: String,
|
||||
depth: usize,
|
||||
is_dir: bool,
|
||||
is_expanded: bool,
|
||||
is_selected: bool,
|
||||
}
|
||||
|
||||
action!(ExpandSelectedEntry);
|
||||
action!(CollapseSelectedEntry);
|
||||
action!(ToggleExpanded, ProjectEntry);
|
||||
action!(Open, ProjectEntry);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ProjectPanel::expand_selected_entry);
|
||||
cx.add_action(ProjectPanel::collapse_selected_entry);
|
||||
cx.add_action(ProjectPanel::toggle_expanded);
|
||||
cx.add_action(ProjectPanel::select_prev);
|
||||
cx.add_action(ProjectPanel::select_next);
|
||||
cx.add_action(ProjectPanel::open_entry);
|
||||
cx.add_bindings([
|
||||
Binding::new("right", ExpandSelectedEntry, Some("ProjectPanel")),
|
||||
Binding::new("left", CollapseSelectedEntry, Some("ProjectPanel")),
|
||||
]);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
OpenedEntry { worktree_id: usize, entry_id: usize },
|
||||
}
|
||||
|
||||
impl ProjectPanel {
|
||||
pub fn new(
|
||||
project: ModelHandle<Project>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> ViewHandle<Self> {
|
||||
let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
|
||||
cx.observe(&project, |this, _, cx| {
|
||||
this.update_visible_entries(None, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
cx.subscribe(&project, |this, _, event, cx| match event {
|
||||
project::Event::ActiveEntryChanged(Some(ProjectEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
})) => {
|
||||
this.expand_entry(*worktree_id, *entry_id, cx);
|
||||
this.update_visible_entries(Some((*worktree_id, *entry_id)), cx);
|
||||
this.autoscroll();
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::WorktreeRemoved(id) => {
|
||||
this.expanded_dir_ids.remove(id);
|
||||
this.update_visible_entries(None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut this = Self {
|
||||
project: project.clone(),
|
||||
settings,
|
||||
list: Default::default(),
|
||||
visible_entries: Default::default(),
|
||||
expanded_dir_ids: Default::default(),
|
||||
selection: None,
|
||||
handle: cx.handle().downgrade(),
|
||||
};
|
||||
this.update_visible_entries(None, cx);
|
||||
this
|
||||
});
|
||||
cx.subscribe(&project_panel, move |workspace, _, event, cx| match event {
|
||||
Event::OpenedEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
} => {
|
||||
if let Some(worktree) = project.read(cx).worktree_for_id(*worktree_id) {
|
||||
if let Some(entry) = worktree.read(cx).entry_for_id(*entry_id) {
|
||||
workspace
|
||||
.open_entry(
|
||||
ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: entry.path.clone(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.map(|t| t.detach());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
project_panel
|
||||
}
|
||||
|
||||
fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
|
||||
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||
let expanded_dir_ids =
|
||||
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) {
|
||||
expanded_dir_ids
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if entry.is_dir() {
|
||||
match expanded_dir_ids.binary_search(&entry.id) {
|
||||
Ok(_) => self.select_next(&SelectNext, cx),
|
||||
Err(ix) => {
|
||||
expanded_dir_ids.insert(ix, entry.id);
|
||||
self.update_visible_entries(None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let event = Event::OpenedEntry {
|
||||
worktree_id: worktree.id(),
|
||||
entry_id: entry.id,
|
||||
};
|
||||
cx.emit(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
|
||||
if let Some((worktree, mut entry)) = self.selected_entry(cx) {
|
||||
let expanded_dir_ids =
|
||||
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) {
|
||||
expanded_dir_ids
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
loop {
|
||||
match expanded_dir_ids.binary_search(&entry.id) {
|
||||
Ok(ix) => {
|
||||
expanded_dir_ids.remove(ix);
|
||||
self.update_visible_entries(Some((worktree.id(), entry.id)), cx);
|
||||
cx.notify();
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
if let Some(parent_entry) =
|
||||
entry.path.parent().and_then(|p| worktree.entry_for_path(p))
|
||||
{
|
||||
entry = parent_entry;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
|
||||
let ProjectEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
} = action.0;
|
||||
|
||||
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
|
||||
match expanded_dir_ids.binary_search(&entry_id) {
|
||||
Ok(ix) => {
|
||||
expanded_dir_ids.remove(ix);
|
||||
}
|
||||
Err(ix) => {
|
||||
expanded_dir_ids.insert(ix, entry_id);
|
||||
}
|
||||
}
|
||||
self.update_visible_entries(Some((worktree_id, entry_id)), cx);
|
||||
cx.focus_self();
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if let Some(selection) = self.selection {
|
||||
let prev_ix = selection.index.saturating_sub(1);
|
||||
let (worktree, entry) = self.visible_entry_for_index(prev_ix, cx).unwrap();
|
||||
self.selection = Some(Selection {
|
||||
worktree_id: worktree.id(),
|
||||
entry_id: entry.id,
|
||||
index: prev_ix,
|
||||
});
|
||||
self.autoscroll();
|
||||
cx.notify();
|
||||
} else {
|
||||
self.select_first(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn open_entry(&mut self, action: &Open, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::OpenedEntry {
|
||||
worktree_id: action.0.worktree_id,
|
||||
entry_id: action.0.entry_id,
|
||||
});
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if let Some(selection) = self.selection {
|
||||
let next_ix = selection.index + 1;
|
||||
if let Some((worktree, entry)) = self.visible_entry_for_index(next_ix, cx) {
|
||||
self.selection = Some(Selection {
|
||||
worktree_id: worktree.id(),
|
||||
entry_id: entry.id,
|
||||
index: next_ix,
|
||||
});
|
||||
self.autoscroll();
|
||||
cx.notify();
|
||||
}
|
||||
} else {
|
||||
self.select_first(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(worktree) = self.project.read(cx).worktrees().first() {
|
||||
let worktree_id = worktree.id();
|
||||
let worktree = worktree.read(cx);
|
||||
if let Some(root_entry) = worktree.root_entry() {
|
||||
self.selection = Some(Selection {
|
||||
worktree_id,
|
||||
entry_id: root_entry.id,
|
||||
index: 0,
|
||||
});
|
||||
self.autoscroll();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn autoscroll(&mut self) {
|
||||
if let Some(selection) = self.selection {
|
||||
self.list.scroll_to(selection.index);
|
||||
}
|
||||
}
|
||||
|
||||
fn visible_entry_for_index<'a>(
|
||||
&self,
|
||||
target_ix: usize,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<(&'a Worktree, &'a worktree::Entry)> {
|
||||
let project = self.project.read(cx);
|
||||
let mut offset = None;
|
||||
let mut ix = 0;
|
||||
for (worktree_ix, visible_entries) in self.visible_entries.iter().enumerate() {
|
||||
if target_ix < ix + visible_entries.len() {
|
||||
let worktree = project.worktrees()[worktree_ix].read(cx);
|
||||
offset = Some((worktree, visible_entries[target_ix - ix]));
|
||||
break;
|
||||
} else {
|
||||
ix += visible_entries.len();
|
||||
}
|
||||
}
|
||||
|
||||
offset.and_then(|(worktree, offset)| {
|
||||
let mut entries = worktree.entries(false);
|
||||
entries.advance_to_offset(offset);
|
||||
Some((worktree, entries.entry()?))
|
||||
})
|
||||
}
|
||||
|
||||
fn selected_entry<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<(&'a Worktree, &'a worktree::Entry)> {
|
||||
let selection = self.selection?;
|
||||
let project = self.project.read(cx);
|
||||
let worktree = project.worktree_for_id(selection.worktree_id)?.read(cx);
|
||||
Some((worktree, worktree.entry_for_id(selection.entry_id)?))
|
||||
}
|
||||
|
||||
fn update_visible_entries(
|
||||
&mut self,
|
||||
new_selected_entry: Option<(usize, usize)>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let worktrees = self.project.read(cx).worktrees();
|
||||
self.visible_entries.clear();
|
||||
|
||||
let mut entry_ix = 0;
|
||||
for worktree in worktrees {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let worktree_id = worktree.id();
|
||||
|
||||
let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
|
||||
hash_map::Entry::Occupied(e) => e.into_mut(),
|
||||
hash_map::Entry::Vacant(e) => {
|
||||
// The first time a worktree's root entry becomes available,
|
||||
// mark that root entry as expanded.
|
||||
if let Some(entry) = snapshot.root_entry() {
|
||||
e.insert(vec![entry.id]).as_slice()
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut visible_worktree_entries = Vec::new();
|
||||
let mut entry_iter = snapshot.entries(false);
|
||||
while let Some(item) = entry_iter.entry() {
|
||||
visible_worktree_entries.push(entry_iter.offset());
|
||||
if let Some(new_selected_entry) = new_selected_entry {
|
||||
if new_selected_entry == (worktree.id(), item.id) {
|
||||
self.selection = Some(Selection {
|
||||
worktree_id,
|
||||
entry_id: item.id,
|
||||
index: entry_ix,
|
||||
});
|
||||
}
|
||||
} else if self.selection.map_or(false, |e| {
|
||||
e.worktree_id == worktree_id && e.entry_id == item.id
|
||||
}) {
|
||||
self.selection = Some(Selection {
|
||||
worktree_id,
|
||||
entry_id: item.id,
|
||||
index: entry_ix,
|
||||
});
|
||||
}
|
||||
|
||||
entry_ix += 1;
|
||||
if expanded_dir_ids.binary_search(&item.id).is_err() {
|
||||
if entry_iter.advance_to_sibling() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
entry_iter.advance();
|
||||
}
|
||||
self.visible_entries.push(visible_worktree_entries);
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_entry(&mut self, worktree_id: usize, entry_id: usize, cx: &mut ViewContext<Self>) {
|
||||
let project = self.project.read(cx);
|
||||
if let Some((worktree, expanded_dir_ids)) = project
|
||||
.worktree_for_id(worktree_id)
|
||||
.zip(self.expanded_dir_ids.get_mut(&worktree_id))
|
||||
{
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
if let Some(mut entry) = worktree.entry_for_id(entry_id) {
|
||||
loop {
|
||||
if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
|
||||
expanded_dir_ids.insert(ix, entry.id);
|
||||
}
|
||||
|
||||
if let Some(parent_entry) =
|
||||
entry.path.parent().and_then(|p| worktree.entry_for_path(p))
|
||||
{
|
||||
entry = parent_entry;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn for_each_visible_entry<C: ReadModel>(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
cx: &mut C,
|
||||
mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut C),
|
||||
) {
|
||||
let project = self.project.read(cx);
|
||||
let worktrees = project.worktrees().to_vec();
|
||||
let mut ix = 0;
|
||||
for (worktree_ix, visible_worktree_entries) in self.visible_entries.iter().enumerate() {
|
||||
if ix >= range.end {
|
||||
return;
|
||||
}
|
||||
if ix + visible_worktree_entries.len() <= range.start {
|
||||
ix += visible_worktree_entries.len();
|
||||
continue;
|
||||
}
|
||||
|
||||
let end_ix = range.end.min(ix + visible_worktree_entries.len());
|
||||
let worktree = &worktrees[worktree_ix];
|
||||
let expanded_entry_ids = self
|
||||
.expanded_dir_ids
|
||||
.get(&worktree.id())
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[]);
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let root_name = OsStr::new(snapshot.root_name());
|
||||
let mut cursor = snapshot.entries(false);
|
||||
|
||||
for ix in visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix]
|
||||
.iter()
|
||||
.copied()
|
||||
{
|
||||
cursor.advance_to_offset(ix);
|
||||
if let Some(entry) = cursor.entry() {
|
||||
let filename = entry.path.file_name().unwrap_or(root_name);
|
||||
let details = EntryDetails {
|
||||
filename: filename.to_string_lossy().to_string(),
|
||||
depth: entry.path.components().count(),
|
||||
is_dir: entry.is_dir(),
|
||||
is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
|
||||
is_selected: self.selection.map_or(false, |e| {
|
||||
e.worktree_id == worktree.id() && e.entry_id == entry.id
|
||||
}),
|
||||
};
|
||||
let entry = ProjectEntry {
|
||||
worktree_id: worktree.id(),
|
||||
entry_id: entry.id,
|
||||
};
|
||||
callback(entry, details, cx);
|
||||
}
|
||||
}
|
||||
ix = end_ix;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_entry(
|
||||
entry: ProjectEntry,
|
||||
details: EntryDetails,
|
||||
theme: &theme::ProjectPanel,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ElementBox {
|
||||
let is_dir = details.is_dir;
|
||||
MouseEventHandler::new::<Self, _, _, _>(
|
||||
(entry.worktree_id, entry.entry_id),
|
||||
cx,
|
||||
|state, _| {
|
||||
let style = match (details.is_selected, state.hovered) {
|
||||
(false, false) => &theme.entry,
|
||||
(false, true) => &theme.hovered_entry,
|
||||
(true, false) => &theme.selected_entry,
|
||||
(true, true) => &theme.hovered_selected_entry,
|
||||
};
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ConstrainedBox::new(
|
||||
Align::new(
|
||||
ConstrainedBox::new(if is_dir {
|
||||
if details.is_expanded {
|
||||
Svg::new("icons/disclosure-open.svg")
|
||||
.with_color(style.icon_color)
|
||||
.boxed()
|
||||
} else {
|
||||
Svg::new("icons/disclosure-closed.svg")
|
||||
.with_color(style.icon_color)
|
||||
.boxed()
|
||||
}
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
})
|
||||
.with_max_width(style.icon_size)
|
||||
.with_max_height(style.icon_size)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_width(style.icon_size)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(details.filename, style.text.clone())
|
||||
.contained()
|
||||
.with_margin_left(style.icon_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed(),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.entry.height)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_padding_left(theme.container.padding.left + details.depth as f32 * 20.)
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
.on_click(move |cx| {
|
||||
if is_dir {
|
||||
cx.dispatch_action(ToggleExpanded(entry))
|
||||
} else {
|
||||
cx.dispatch_action(Open(entry))
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl View for ProjectPanel {
|
||||
fn ui_name() -> &'static str {
|
||||
"ProjectPanel"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||
let settings = self.settings.clone();
|
||||
let mut container_style = settings.borrow().theme.project_panel.container;
|
||||
let padding = std::mem::take(&mut container_style.padding);
|
||||
let handle = self.handle.clone();
|
||||
UniformList::new(
|
||||
self.list.clone(),
|
||||
self.visible_entries
|
||||
.iter()
|
||||
.map(|worktree_entries| worktree_entries.len())
|
||||
.sum(),
|
||||
move |range, items, cx| {
|
||||
let theme = &settings.borrow().theme.project_panel;
|
||||
let this = handle.upgrade(cx).unwrap();
|
||||
this.update(cx.app, |this, cx| {
|
||||
this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| {
|
||||
items.push(Self::render_entry(entry, details, theme, cx));
|
||||
});
|
||||
})
|
||||
},
|
||||
)
|
||||
.with_padding_top(padding.top)
|
||||
.with_padding_bottom(padding.bottom)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ProjectPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::test_app_state;
|
||||
use gpui::{TestAppContext, ViewHandle};
|
||||
use serde_json::json;
|
||||
use std::{collections::HashSet, path::Path};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visible_list(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
let settings = app_state.settings.clone();
|
||||
let fs = app_state.fs.as_fake();
|
||||
|
||||
fs.insert_tree(
|
||||
"/root1",
|
||||
json!({
|
||||
".dockerignore": "",
|
||||
".git": {
|
||||
"HEAD": "",
|
||||
},
|
||||
"a": {
|
||||
"0": { "q": "", "r": "", "s": "" },
|
||||
"1": { "t": "", "u": "" },
|
||||
"2": { "v": "", "w": "", "x": "", "y": "" },
|
||||
},
|
||||
"b": {
|
||||
"3": { "Q": "" },
|
||||
"4": { "R": "", "S": "", "T": "", "U": "" },
|
||||
},
|
||||
"c": {
|
||||
"5": {},
|
||||
"6": { "V": "", "W": "" },
|
||||
"7": { "X": "" },
|
||||
"8": { "Y": {}, "Z": "" }
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.insert_tree(
|
||||
"/root2",
|
||||
json!({
|
||||
"d": {
|
||||
"9": ""
|
||||
},
|
||||
"e": {}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = cx.add_model(|_| Project::new(&app_state));
|
||||
let root1 = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.add_local_worktree("/root1".as_ref(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
root1
|
||||
.read_with(&cx, |t, _| t.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
let root2 = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.add_local_worktree("/root2".as_ref(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
root2
|
||||
.read_with(&cx, |t, _| t.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
|
||||
let panel = workspace.update(&mut cx, |_, cx| ProjectPanel::new(project, settings, cx));
|
||||
assert_eq!(
|
||||
visible_entry_details(&panel, 0..50, &mut cx),
|
||||
&[
|
||||
EntryDetails {
|
||||
filename: "root1".to_string(),
|
||||
depth: 0,
|
||||
is_dir: true,
|
||||
is_expanded: true,
|
||||
is_selected: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: ".dockerignore".to_string(),
|
||||
depth: 1,
|
||||
is_dir: false,
|
||||
is_expanded: false,
|
||||
is_selected: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "a".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "b".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "c".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "root2".to_string(),
|
||||
depth: 0,
|
||||
is_dir: true,
|
||||
is_expanded: true,
|
||||
is_selected: false
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "d".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "e".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false
|
||||
}
|
||||
],
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "root1/b", &mut cx);
|
||||
assert_eq!(
|
||||
visible_entry_details(&panel, 0..50, &mut cx),
|
||||
&[
|
||||
EntryDetails {
|
||||
filename: "root1".to_string(),
|
||||
depth: 0,
|
||||
is_dir: true,
|
||||
is_expanded: true,
|
||||
is_selected: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: ".dockerignore".to_string(),
|
||||
depth: 1,
|
||||
is_dir: false,
|
||||
is_expanded: false,
|
||||
is_selected: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "a".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "b".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: true,
|
||||
is_selected: true,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "3".to_string(),
|
||||
depth: 2,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "4".to_string(),
|
||||
depth: 2,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "c".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "root2".to_string(),
|
||||
depth: 0,
|
||||
is_dir: true,
|
||||
is_expanded: true,
|
||||
is_selected: false
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "d".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "e".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
visible_entry_details(&panel, 5..8, &mut cx),
|
||||
[
|
||||
EntryDetails {
|
||||
filename: "4".to_string(),
|
||||
depth: 2,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "c".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
is_selected: false
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "root2".to_string(),
|
||||
depth: 0,
|
||||
is_dir: true,
|
||||
is_expanded: true,
|
||||
is_selected: false
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
fn toggle_expand_dir(
|
||||
panel: &ViewHandle<ProjectPanel>,
|
||||
path: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let path = path.as_ref();
|
||||
panel.update(cx, |panel, cx| {
|
||||
for worktree in panel.project.read(cx).worktrees() {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
|
||||
let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
|
||||
panel.toggle_expanded(
|
||||
&ToggleExpanded(ProjectEntry {
|
||||
worktree_id: worktree.id(),
|
||||
entry_id,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("no worktree for path {:?}", path);
|
||||
});
|
||||
}
|
||||
|
||||
fn visible_entry_details(
|
||||
panel: &ViewHandle<ProjectPanel>,
|
||||
range: Range<usize>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<EntryDetails> {
|
||||
let mut result = Vec::new();
|
||||
let mut project_entries = HashSet::new();
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
|
||||
assert!(
|
||||
project_entries.insert(project_entry),
|
||||
"duplicate project entry {:?} {:?}",
|
||||
project_entry,
|
||||
details
|
||||
);
|
||||
result.push(details);
|
||||
});
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
78
crates/zed/src/settings.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use crate::theme::{self, DEFAULT_THEME_NAME};
|
||||
use anyhow::Result;
|
||||
use gpui::font_cache::{FamilyId, FontCache};
|
||||
use postage::watch;
|
||||
use std::sync::Arc;
|
||||
pub use theme::{Theme, ThemeRegistry};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Settings {
|
||||
pub buffer_font_family: FamilyId,
|
||||
pub buffer_font_size: f32,
|
||||
pub tab_size: usize,
|
||||
pub theme: Arc<Theme>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test(cx: &gpui::AppContext) -> Self {
|
||||
use crate::assets::Assets;
|
||||
use gpui::AssetSource;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref DEFAULT_THEME: parking_lot::Mutex<Option<Arc<Theme>>> = Default::default();
|
||||
static ref FONTS: Vec<Arc<Vec<u8>>> = Assets
|
||||
.list("fonts")
|
||||
.into_iter()
|
||||
.map(|f| Arc::new(Assets.load(&f).unwrap().to_vec()))
|
||||
.collect();
|
||||
}
|
||||
|
||||
cx.platform().fonts().add_fonts(&FONTS).unwrap();
|
||||
|
||||
let mut theme_guard = DEFAULT_THEME.lock();
|
||||
let theme = if let Some(theme) = theme_guard.as_ref() {
|
||||
theme.clone()
|
||||
} else {
|
||||
let theme = ThemeRegistry::new(Assets, cx.font_cache().clone())
|
||||
.get(DEFAULT_THEME_NAME)
|
||||
.expect("failed to load default theme in tests");
|
||||
*theme_guard = Some(theme.clone());
|
||||
theme
|
||||
};
|
||||
|
||||
Self::new(cx.font_cache(), theme).unwrap()
|
||||
}
|
||||
|
||||
pub fn new(font_cache: &FontCache, theme: Arc<Theme>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
buffer_font_family: font_cache.load_family(&["Inconsolata"])?,
|
||||
buffer_font_size: 16.,
|
||||
tab_size: 4,
|
||||
theme,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_tab_size(mut self, tab_size: usize) -> Self {
|
||||
self.tab_size = tab_size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test(cx: &gpui::AppContext) -> (watch::Sender<Settings>, watch::Receiver<Settings>) {
|
||||
watch::channel_with(Settings::test(cx))
|
||||
}
|
||||
|
||||
pub fn channel(
|
||||
font_cache: &FontCache,
|
||||
themes: &ThemeRegistry,
|
||||
) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
|
||||
let theme = match themes.get(DEFAULT_THEME_NAME) {
|
||||
Ok(theme) => theme,
|
||||
Err(err) => {
|
||||
panic!("failed to deserialize default theme: {:?}", err)
|
||||
}
|
||||
};
|
||||
Ok(watch::channel_with(Settings::new(font_cache, theme)?))
|
||||
}
|
110
crates/zed/src/test.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use crate::{
|
||||
assets::Assets,
|
||||
channel::ChannelList,
|
||||
http::{HttpClient, Request, Response, ServerResponse},
|
||||
language,
|
||||
settings::{self, ThemeRegistry},
|
||||
user::UserStore,
|
||||
AppState,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use buffer::LanguageRegistry;
|
||||
use futures::{future::BoxFuture, Future};
|
||||
use gpui::{Entity, ModelHandle, MutableAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use rpc_client as rpc;
|
||||
use smol::channel;
|
||||
use std::{fmt, marker::PhantomData, sync::Arc};
|
||||
use worktree::fs::FakeFs;
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
env_logger::init();
|
||||
}
|
||||
|
||||
pub fn sample_text(rows: usize, cols: usize) -> String {
|
||||
let mut text = String::new();
|
||||
for row in 0..rows {
|
||||
let c: char = ('a' as u32 + row as u32) as u8 as char;
|
||||
let mut line = c.to_string().repeat(cols);
|
||||
if row < rows - 1 {
|
||||
line.push('\n');
|
||||
}
|
||||
text += &line;
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
|
||||
let (settings_tx, settings) = settings::test(cx);
|
||||
let mut languages = LanguageRegistry::new();
|
||||
languages.add(Arc::new(language::rust()));
|
||||
let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
|
||||
let rpc = rpc::Client::new();
|
||||
let http = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
|
||||
let user_store = cx.add_model(|cx| UserStore::new(rpc.clone(), http, cx));
|
||||
Arc::new(AppState {
|
||||
settings_tx: Arc::new(Mutex::new(settings_tx)),
|
||||
settings,
|
||||
themes,
|
||||
languages: Arc::new(languages),
|
||||
channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), rpc.clone(), cx)),
|
||||
rpc,
|
||||
user_store,
|
||||
fs: Arc::new(FakeFs::new()),
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Observer<T>(PhantomData<T>);
|
||||
|
||||
impl<T: 'static> Entity for Observer<T> {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl<T: Entity> Observer<T> {
|
||||
pub fn new(
|
||||
handle: &ModelHandle<T>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> (ModelHandle<Self>, channel::Receiver<()>) {
|
||||
let (notify_tx, notify_rx) = channel::unbounded();
|
||||
let observer = cx.add_model(|cx| {
|
||||
cx.observe(handle, move |_, _, _| {
|
||||
let _ = notify_tx.try_send(());
|
||||
})
|
||||
.detach();
|
||||
Observer(PhantomData)
|
||||
});
|
||||
(observer, notify_rx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FakeHttpClient {
|
||||
handler:
|
||||
Box<dyn 'static + Send + Sync + Fn(Request) -> BoxFuture<'static, Result<ServerResponse>>>,
|
||||
}
|
||||
|
||||
impl FakeHttpClient {
|
||||
pub fn new<Fut, F>(handler: F) -> Arc<dyn HttpClient>
|
||||
where
|
||||
Fut: 'static + Send + Future<Output = Result<ServerResponse>>,
|
||||
F: 'static + Send + Sync + Fn(Request) -> Fut,
|
||||
{
|
||||
Arc::new(Self {
|
||||
handler: Box::new(move |req| Box::pin(handler(req))),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for FakeHttpClient {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FakeHttpClient").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for FakeHttpClient {
|
||||
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>> {
|
||||
let future = (self.handler)(req);
|
||||
Box::pin(async move { future.await.map(Into::into) })
|
||||
}
|
||||
}
|
233
crates/zed/src/theme.rs
Normal file
|
@ -0,0 +1,233 @@
|
|||
mod resolution;
|
||||
mod theme_registry;
|
||||
|
||||
use crate::editor::{EditorStyle, SelectionStyle};
|
||||
use buffer::SyntaxTheme;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::{ContainerStyle, ImageStyle, LabelStyle},
|
||||
fonts::TextStyle,
|
||||
Border,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub use theme_registry::*;
|
||||
|
||||
pub const DEFAULT_THEME_NAME: &'static str = "black";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Theme {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
pub workspace: Workspace,
|
||||
pub chat_panel: ChatPanel,
|
||||
pub people_panel: PeoplePanel,
|
||||
pub project_panel: ProjectPanel,
|
||||
pub selector: Selector,
|
||||
pub editor: EditorStyle,
|
||||
pub syntax: SyntaxTheme,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Workspace {
|
||||
pub background: Color,
|
||||
pub titlebar: Titlebar,
|
||||
pub tab: Tab,
|
||||
pub active_tab: Tab,
|
||||
pub pane_divider: Border,
|
||||
pub left_sidebar: Sidebar,
|
||||
pub right_sidebar: Sidebar,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Titlebar {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub title: TextStyle,
|
||||
pub avatar_width: f32,
|
||||
pub offline_icon: OfflineIcon,
|
||||
pub icon_color: Color,
|
||||
pub avatar: ImageStyle,
|
||||
pub outdated_warning: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct OfflineIcon {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Tab {
|
||||
pub height: f32,
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
#[serde(flatten)]
|
||||
pub label: LabelStyle,
|
||||
pub spacing: f32,
|
||||
pub icon_width: f32,
|
||||
pub icon_close: Color,
|
||||
pub icon_close_active: Color,
|
||||
pub icon_dirty: Color,
|
||||
pub icon_conflict: Color,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Sidebar {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub width: f32,
|
||||
pub item: SidebarItem,
|
||||
pub active_item: SidebarItem,
|
||||
pub resize_handle: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SidebarItem {
|
||||
pub icon_color: Color,
|
||||
pub icon_size: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChatPanel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub message: ChatMessage,
|
||||
pub pending_message: ChatMessage,
|
||||
pub channel_select: ChannelSelect,
|
||||
pub input_editor: InputEditorStyle,
|
||||
pub sign_in_prompt: TextStyle,
|
||||
pub hovered_sign_in_prompt: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ProjectPanel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub entry: ProjectPanelEntry,
|
||||
pub hovered_entry: ProjectPanelEntry,
|
||||
pub selected_entry: ProjectPanelEntry,
|
||||
pub hovered_selected_entry: ProjectPanelEntry,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ProjectPanelEntry {
|
||||
pub height: f32,
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub text: TextStyle,
|
||||
pub icon_color: Color,
|
||||
pub icon_size: f32,
|
||||
pub icon_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PeoplePanel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub host_row_height: f32,
|
||||
pub host_avatar: ImageStyle,
|
||||
pub host_username: ContainedText,
|
||||
pub tree_branch_width: f32,
|
||||
pub tree_branch_color: Color,
|
||||
pub shared_worktree: WorktreeRow,
|
||||
pub hovered_shared_worktree: WorktreeRow,
|
||||
pub unshared_worktree: WorktreeRow,
|
||||
pub hovered_unshared_worktree: WorktreeRow,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct WorktreeRow {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub height: f32,
|
||||
pub name: ContainedText,
|
||||
pub guest_avatar: ImageStyle,
|
||||
pub guest_avatar_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub body: TextStyle,
|
||||
pub sender: ContainedText,
|
||||
pub timestamp: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChannelSelect {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub header: ChannelName,
|
||||
pub item: ChannelName,
|
||||
pub active_item: ChannelName,
|
||||
pub hovered_item: ChannelName,
|
||||
pub hovered_active_item: ChannelName,
|
||||
pub menu: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChannelName {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub hash: ContainedText,
|
||||
pub name: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Selector {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub empty: ContainedLabel,
|
||||
pub input_editor: InputEditorStyle,
|
||||
pub item: ContainedLabel,
|
||||
pub active_item: ContainedLabel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ContainedText {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
#[serde(flatten)]
|
||||
pub text: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ContainedLabel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
#[serde(flatten)]
|
||||
pub label: LabelStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct InputEditorStyle {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub text: TextStyle,
|
||||
#[serde(default)]
|
||||
pub placeholder_text: Option<TextStyle>,
|
||||
pub selection: SelectionStyle,
|
||||
}
|
||||
|
||||
impl InputEditorStyle {
|
||||
pub fn as_editor(&self) -> EditorStyle {
|
||||
EditorStyle {
|
||||
text: self.text.clone(),
|
||||
placeholder_text: self.placeholder_text.clone(),
|
||||
background: self
|
||||
.container
|
||||
.background_color
|
||||
.unwrap_or(Color::transparent_black()),
|
||||
selection: self.selection,
|
||||
gutter_background: Default::default(),
|
||||
active_line_background: Default::default(),
|
||||
line_number: Default::default(),
|
||||
line_number_active: Default::default(),
|
||||
guest_selections: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
497
crates/zed/src/theme/resolution.rs
Normal file
|
@ -0,0 +1,497 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use indexmap::IndexMap;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
mem,
|
||||
rc::{Rc, Weak},
|
||||
};
|
||||
|
||||
pub fn resolve_references(value: Value) -> Result<Value> {
|
||||
let tree = Tree::from_json(value)?;
|
||||
tree.resolve()?;
|
||||
tree.to_json()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Node {
|
||||
Reference {
|
||||
path: String,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
Object {
|
||||
base: Option<String>,
|
||||
children: IndexMap<String, Tree>,
|
||||
resolved: bool,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
Array {
|
||||
children: Vec<Tree>,
|
||||
resolved: bool,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
String {
|
||||
value: String,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
Number {
|
||||
value: serde_json::Number,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
Bool {
|
||||
value: bool,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
Null {
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Tree(Rc<RefCell<Node>>);
|
||||
|
||||
impl Tree {
|
||||
pub fn new(node: Node) -> Self {
|
||||
Self(Rc::new(RefCell::new(node)))
|
||||
}
|
||||
|
||||
fn from_json(value: Value) -> Result<Self> {
|
||||
match value {
|
||||
Value::String(value) => {
|
||||
if let Some(path) = value.strip_prefix("$") {
|
||||
Ok(Self::new(Node::Reference {
|
||||
path: path.to_string(),
|
||||
parent: None,
|
||||
}))
|
||||
} else {
|
||||
Ok(Self::new(Node::String {
|
||||
value,
|
||||
parent: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
Value::Number(value) => Ok(Self::new(Node::Number {
|
||||
value,
|
||||
parent: None,
|
||||
})),
|
||||
Value::Bool(value) => Ok(Self::new(Node::Bool {
|
||||
value,
|
||||
parent: None,
|
||||
})),
|
||||
Value::Null => Ok(Self::new(Node::Null { parent: None })),
|
||||
Value::Object(object) => {
|
||||
let tree = Self::new(Node::Object {
|
||||
base: Default::default(),
|
||||
children: Default::default(),
|
||||
resolved: false,
|
||||
parent: None,
|
||||
});
|
||||
let mut children = IndexMap::new();
|
||||
let mut resolved = true;
|
||||
let mut base = None;
|
||||
for (key, value) in object.into_iter() {
|
||||
let value = if key == "extends" {
|
||||
if value.is_string() {
|
||||
if let Value::String(value) = value {
|
||||
base = value.strip_prefix("$").map(str::to_string);
|
||||
resolved = false;
|
||||
Self::new(Node::String {
|
||||
value,
|
||||
parent: None,
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
} else {
|
||||
Tree::from_json(value)?
|
||||
}
|
||||
} else {
|
||||
Tree::from_json(value)?
|
||||
};
|
||||
value
|
||||
.0
|
||||
.borrow_mut()
|
||||
.set_parent(Some(Rc::downgrade(&tree.0)));
|
||||
resolved &= value.is_resolved();
|
||||
children.insert(key.clone(), value);
|
||||
}
|
||||
|
||||
*tree.0.borrow_mut() = Node::Object {
|
||||
base,
|
||||
children,
|
||||
resolved,
|
||||
parent: None,
|
||||
};
|
||||
Ok(tree)
|
||||
}
|
||||
Value::Array(elements) => {
|
||||
let tree = Self::new(Node::Array {
|
||||
children: Default::default(),
|
||||
resolved: false,
|
||||
parent: None,
|
||||
});
|
||||
|
||||
let mut children = Vec::new();
|
||||
let mut resolved = true;
|
||||
for element in elements {
|
||||
let child = Tree::from_json(element)?;
|
||||
child
|
||||
.0
|
||||
.borrow_mut()
|
||||
.set_parent(Some(Rc::downgrade(&tree.0)));
|
||||
resolved &= child.is_resolved();
|
||||
children.push(child);
|
||||
}
|
||||
|
||||
*tree.0.borrow_mut() = Node::Array {
|
||||
children,
|
||||
resolved,
|
||||
parent: None,
|
||||
};
|
||||
Ok(tree)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_json(&self) -> Result<Value> {
|
||||
match &*self.0.borrow() {
|
||||
Node::Reference { .. } => Err(anyhow!("unresolved tree")),
|
||||
Node::String { value, .. } => Ok(Value::String(value.clone())),
|
||||
Node::Number { value, .. } => Ok(Value::Number(value.clone())),
|
||||
Node::Bool { value, .. } => Ok(Value::Bool(*value)),
|
||||
Node::Null { .. } => Ok(Value::Null),
|
||||
Node::Object { children, .. } => {
|
||||
let mut json_children = serde_json::Map::new();
|
||||
for (key, value) in children {
|
||||
json_children.insert(key.clone(), value.to_json()?);
|
||||
}
|
||||
Ok(Value::Object(json_children))
|
||||
}
|
||||
Node::Array { children, .. } => {
|
||||
let mut json_children = Vec::new();
|
||||
for child in children {
|
||||
json_children.push(child.to_json()?);
|
||||
}
|
||||
Ok(Value::Array(json_children))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parent(&self) -> Option<Tree> {
|
||||
match &*self.0.borrow() {
|
||||
Node::Reference { parent, .. }
|
||||
| Node::Object { parent, .. }
|
||||
| Node::Array { parent, .. }
|
||||
| Node::String { parent, .. }
|
||||
| Node::Number { parent, .. }
|
||||
| Node::Bool { parent, .. }
|
||||
| Node::Null { parent } => parent.as_ref().and_then(|p| p.upgrade()).map(Tree),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, path: &str) -> Result<Option<Tree>> {
|
||||
let mut tree = self.clone();
|
||||
for component in path.split('.') {
|
||||
let node = tree.0.borrow();
|
||||
match &*node {
|
||||
Node::Object { children, .. } => {
|
||||
if let Some(subtree) = children.get(component).cloned() {
|
||||
drop(node);
|
||||
tree = subtree;
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"key \"{}\" does not exist in path \"{}\"",
|
||||
component,
|
||||
path
|
||||
));
|
||||
}
|
||||
}
|
||||
Node::Reference { .. } => return Ok(None),
|
||||
Node::Array { .. }
|
||||
| Node::String { .. }
|
||||
| Node::Number { .. }
|
||||
| Node::Bool { .. }
|
||||
| Node::Null { .. } => {
|
||||
return Err(anyhow!(
|
||||
"key \"{}\" in path \"{}\" is not an object",
|
||||
component,
|
||||
path
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(tree))
|
||||
}
|
||||
|
||||
fn is_resolved(&self) -> bool {
|
||||
match &*self.0.borrow() {
|
||||
Node::Reference { .. } => false,
|
||||
Node::Object { resolved, .. } | Node::Array { resolved, .. } => *resolved,
|
||||
Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_resolved(&self) {
|
||||
match &mut *self.0.borrow_mut() {
|
||||
Node::Object {
|
||||
resolved,
|
||||
base,
|
||||
children,
|
||||
..
|
||||
} => {
|
||||
*resolved = base.is_none() && children.values().all(|c| c.is_resolved());
|
||||
}
|
||||
Node::Array {
|
||||
resolved, children, ..
|
||||
} => {
|
||||
*resolved = children.iter().all(|c| c.is_resolved());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(&self) -> Result<()> {
|
||||
let mut unresolved = vec![self.clone()];
|
||||
let mut made_progress = true;
|
||||
|
||||
while made_progress && !unresolved.is_empty() {
|
||||
made_progress = false;
|
||||
for mut tree in mem::take(&mut unresolved) {
|
||||
made_progress |= tree.resolve_subtree(self, &mut unresolved)?;
|
||||
if tree.is_resolved() {
|
||||
while let Some(parent) = tree.parent() {
|
||||
parent.update_resolved();
|
||||
if !parent.is_resolved() {
|
||||
break;
|
||||
}
|
||||
tree = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if unresolved.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("tree contains cycles"))
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_subtree(&self, root: &Tree, unresolved: &mut Vec<Tree>) -> Result<bool> {
|
||||
let node = self.0.borrow();
|
||||
match &*node {
|
||||
Node::Reference { path, parent } => {
|
||||
if let Some(subtree) = root.get(&path)? {
|
||||
if subtree.is_resolved() {
|
||||
let parent = parent.clone();
|
||||
drop(node);
|
||||
let mut new_node = subtree.0.borrow().clone();
|
||||
new_node.set_parent(parent);
|
||||
*self.0.borrow_mut() = new_node;
|
||||
Ok(true)
|
||||
} else {
|
||||
unresolved.push(self.clone());
|
||||
Ok(false)
|
||||
}
|
||||
} else {
|
||||
unresolved.push(self.clone());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Node::Object {
|
||||
base,
|
||||
children,
|
||||
resolved,
|
||||
..
|
||||
} => {
|
||||
if *resolved {
|
||||
Ok(false)
|
||||
} else {
|
||||
let mut made_progress = false;
|
||||
let mut children_resolved = true;
|
||||
for child in children.values() {
|
||||
made_progress |= child.resolve_subtree(root, unresolved)?;
|
||||
children_resolved &= child.is_resolved();
|
||||
}
|
||||
|
||||
if children_resolved {
|
||||
let mut has_base = false;
|
||||
let mut resolved_base = None;
|
||||
if let Some(base) = base {
|
||||
has_base = true;
|
||||
if let Some(base) = root.get(base)? {
|
||||
if base.is_resolved() {
|
||||
resolved_base = Some(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(node);
|
||||
|
||||
if let Some(base) = resolved_base.as_ref() {
|
||||
self.extend_from(&base);
|
||||
made_progress = true;
|
||||
}
|
||||
|
||||
if let Node::Object { resolved, base, .. } = &mut *self.0.borrow_mut() {
|
||||
if has_base {
|
||||
if resolved_base.is_some() {
|
||||
base.take();
|
||||
*resolved = true;
|
||||
} else {
|
||||
unresolved.push(self.clone());
|
||||
}
|
||||
} else {
|
||||
*resolved = true;
|
||||
}
|
||||
}
|
||||
} else if base.is_some() {
|
||||
unresolved.push(self.clone());
|
||||
}
|
||||
|
||||
Ok(made_progress)
|
||||
}
|
||||
}
|
||||
Node::Array {
|
||||
children, resolved, ..
|
||||
} => {
|
||||
if *resolved {
|
||||
Ok(false)
|
||||
} else {
|
||||
let mut made_progress = false;
|
||||
let mut children_resolved = true;
|
||||
for child in children.iter() {
|
||||
made_progress |= child.resolve_subtree(root, unresolved)?;
|
||||
children_resolved &= child.is_resolved();
|
||||
}
|
||||
|
||||
if children_resolved {
|
||||
drop(node);
|
||||
|
||||
if let Node::Array { resolved, .. } = &mut *self.0.borrow_mut() {
|
||||
*resolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(made_progress)
|
||||
}
|
||||
}
|
||||
Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_from(&self, base: &Tree) {
|
||||
if Rc::ptr_eq(&self.0, &base.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let (
|
||||
Node::Object { children, .. },
|
||||
Node::Object {
|
||||
children: base_children,
|
||||
..
|
||||
},
|
||||
) = (&mut *self.0.borrow_mut(), &*base.0.borrow())
|
||||
{
|
||||
for (key, base_value) in base_children {
|
||||
if let Some(value) = children.get(key) {
|
||||
value.extend_from(base_value);
|
||||
} else {
|
||||
let base_value = base_value.clone();
|
||||
base_value
|
||||
.0
|
||||
.borrow_mut()
|
||||
.set_parent(Some(Rc::downgrade(&self.0)));
|
||||
children.insert(key.clone(), base_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn set_parent(&mut self, new_parent: Option<Weak<RefCell<Node>>>) {
|
||||
match self {
|
||||
Node::Reference { parent, .. }
|
||||
| Node::Object { parent, .. }
|
||||
| Node::Array { parent, .. }
|
||||
| Node::String { parent, .. }
|
||||
| Node::Number { parent, .. }
|
||||
| Node::Bool { parent, .. }
|
||||
| Node::Null { parent } => *parent = new_parent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_references() {
|
||||
let json = serde_json::json!({
|
||||
"a": {
|
||||
"extends": "$g",
|
||||
"x": "$b.d"
|
||||
},
|
||||
"b": {
|
||||
"c": "$a",
|
||||
"d": "$e.f"
|
||||
},
|
||||
"e": {
|
||||
"extends": "$a",
|
||||
"f": "1"
|
||||
},
|
||||
"g": {
|
||||
"h": 2
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
resolve_references(json).unwrap(),
|
||||
serde_json::json!({
|
||||
"a": {
|
||||
"extends": "$g",
|
||||
"x": "1",
|
||||
"h": 2
|
||||
},
|
||||
"b": {
|
||||
"c": {
|
||||
"extends": "$g",
|
||||
"x": "1",
|
||||
"h": 2
|
||||
},
|
||||
"d": "1"
|
||||
},
|
||||
"e": {
|
||||
"extends": "$a",
|
||||
"f": "1",
|
||||
"x": "1",
|
||||
"h": 2
|
||||
},
|
||||
"g": {
|
||||
"h": 2
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycles() {
|
||||
let json = serde_json::json!({
|
||||
"a": {
|
||||
"b": "$c.d"
|
||||
},
|
||||
"c": {
|
||||
"d": "$a.b",
|
||||
},
|
||||
});
|
||||
|
||||
assert!(resolve_references(json).is_err());
|
||||
}
|
||||
}
|
287
crates/zed/src/theme/theme_registry.rs
Normal file
|
@ -0,0 +1,287 @@
|
|||
use super::resolution::resolve_references;
|
||||
use anyhow::{Context, Result};
|
||||
use gpui::{fonts, AssetSource, FontCache};
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::{Map, Value};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use super::Theme;
|
||||
|
||||
pub struct ThemeRegistry {
|
||||
assets: Box<dyn AssetSource>,
|
||||
themes: Mutex<HashMap<String, Arc<Theme>>>,
|
||||
theme_data: Mutex<HashMap<String, Arc<Value>>>,
|
||||
font_cache: Arc<FontCache>,
|
||||
}
|
||||
|
||||
impl ThemeRegistry {
|
||||
pub fn new(source: impl AssetSource, font_cache: Arc<FontCache>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
assets: Box::new(source),
|
||||
themes: Default::default(),
|
||||
theme_data: Default::default(),
|
||||
font_cache,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list(&self) -> impl Iterator<Item = String> {
|
||||
self.assets.list("themes/").into_iter().filter_map(|path| {
|
||||
let filename = path.strip_prefix("themes/")?;
|
||||
let theme_name = filename.strip_suffix(".toml")?;
|
||||
if theme_name.starts_with('_') {
|
||||
None
|
||||
} else {
|
||||
Some(theme_name.to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
self.theme_data.lock().clear();
|
||||
self.themes.lock().clear();
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
|
||||
if let Some(theme) = self.themes.lock().get(name) {
|
||||
return Ok(theme.clone());
|
||||
}
|
||||
|
||||
let theme_data = self.load(name, true)?;
|
||||
let mut theme: Theme = fonts::with_font_cache(self.font_cache.clone(), || {
|
||||
serde_path_to_error::deserialize(theme_data.as_ref())
|
||||
})?;
|
||||
|
||||
theme.name = name.into();
|
||||
let theme = Arc::new(theme);
|
||||
self.themes.lock().insert(name.to_string(), theme.clone());
|
||||
Ok(theme)
|
||||
}
|
||||
|
||||
fn load(&self, name: &str, evaluate_references: bool) -> Result<Arc<Value>> {
|
||||
if let Some(data) = self.theme_data.lock().get(name) {
|
||||
return Ok(data.clone());
|
||||
}
|
||||
|
||||
let asset_path = format!("themes/{}.toml", name);
|
||||
let source_code = self
|
||||
.assets
|
||||
.load(&asset_path)
|
||||
.with_context(|| format!("failed to load theme file {}", asset_path))?;
|
||||
|
||||
let mut theme_data: Map<String, Value> = toml::from_slice(source_code.as_ref())
|
||||
.with_context(|| format!("failed to parse {}.toml", name))?;
|
||||
|
||||
// If this theme extends another base theme, deeply merge it into the base theme's data
|
||||
if let Some(base_name) = theme_data
|
||||
.get("extends")
|
||||
.and_then(|name| name.as_str())
|
||||
.map(str::to_string)
|
||||
{
|
||||
let base_theme_data = self
|
||||
.load(&base_name, false)
|
||||
.with_context(|| format!("failed to load base theme {}", base_name))?
|
||||
.as_ref()
|
||||
.clone();
|
||||
if let Value::Object(mut base_theme_object) = base_theme_data {
|
||||
deep_merge_json(&mut base_theme_object, theme_data);
|
||||
theme_data = base_theme_object;
|
||||
}
|
||||
}
|
||||
|
||||
let mut theme_data = Value::Object(theme_data);
|
||||
|
||||
// Find all of the key path references in the object, and then sort them according
|
||||
// to their dependencies.
|
||||
if evaluate_references {
|
||||
theme_data = resolve_references(theme_data)?;
|
||||
}
|
||||
|
||||
let result = Arc::new(theme_data);
|
||||
self.theme_data
|
||||
.lock()
|
||||
.insert(name.to_string(), result.clone());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn deep_merge_json(base: &mut Map<String, Value>, extension: Map<String, Value>) {
|
||||
for (key, extension_value) in extension {
|
||||
if let Value::Object(extension_object) = extension_value {
|
||||
if let Some(base_object) = base.get_mut(&key).and_then(|value| value.as_object_mut()) {
|
||||
deep_merge_json(base_object, extension_object);
|
||||
} else {
|
||||
base.insert(key, Value::Object(extension_object));
|
||||
}
|
||||
} else {
|
||||
base.insert(key, extension_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{test::test_app_state, theme::DEFAULT_THEME_NAME};
|
||||
use anyhow::anyhow;
|
||||
use gpui::MutableAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_bundled_themes(cx: &mut MutableAppContext) {
|
||||
let app_state = test_app_state(cx);
|
||||
let mut has_default_theme = false;
|
||||
for theme_name in app_state.themes.list() {
|
||||
let theme = app_state.themes.get(&theme_name).unwrap();
|
||||
if theme.name == DEFAULT_THEME_NAME {
|
||||
has_default_theme = true;
|
||||
}
|
||||
assert_eq!(theme.name, theme_name);
|
||||
}
|
||||
assert!(has_default_theme);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_theme_extension(cx: &mut MutableAppContext) {
|
||||
let assets = TestAssets(&[
|
||||
(
|
||||
"themes/_base.toml",
|
||||
r##"
|
||||
[ui.active_tab]
|
||||
extends = "$ui.tab"
|
||||
border.color = "#666666"
|
||||
text = "$text_colors.bright"
|
||||
|
||||
[ui.tab]
|
||||
extends = "$ui.element"
|
||||
text = "$text_colors.dull"
|
||||
|
||||
[ui.element]
|
||||
background = "#111111"
|
||||
border = {width = 2.0, color = "#00000000"}
|
||||
|
||||
[editor]
|
||||
background = "#222222"
|
||||
default_text = "$text_colors.regular"
|
||||
"##,
|
||||
),
|
||||
(
|
||||
"themes/light.toml",
|
||||
r##"
|
||||
extends = "_base"
|
||||
|
||||
[text_colors]
|
||||
bright = "#ffffff"
|
||||
regular = "#eeeeee"
|
||||
dull = "#dddddd"
|
||||
|
||||
[editor]
|
||||
background = "#232323"
|
||||
"##,
|
||||
),
|
||||
]);
|
||||
|
||||
let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
|
||||
let theme_data = registry.load("light", true).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
theme_data.as_ref(),
|
||||
&serde_json::json!({
|
||||
"ui": {
|
||||
"active_tab": {
|
||||
"background": "#111111",
|
||||
"border": {
|
||||
"width": 2.0,
|
||||
"color": "#666666"
|
||||
},
|
||||
"extends": "$ui.tab",
|
||||
"text": "#ffffff"
|
||||
},
|
||||
"tab": {
|
||||
"background": "#111111",
|
||||
"border": {
|
||||
"width": 2.0,
|
||||
"color": "#00000000"
|
||||
},
|
||||
"extends": "$ui.element",
|
||||
"text": "#dddddd"
|
||||
},
|
||||
"element": {
|
||||
"background": "#111111",
|
||||
"border": {
|
||||
"width": 2.0,
|
||||
"color": "#00000000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"background": "#232323",
|
||||
"default_text": "#eeeeee"
|
||||
},
|
||||
"extends": "_base",
|
||||
"text_colors": {
|
||||
"bright": "#ffffff",
|
||||
"regular": "#eeeeee",
|
||||
"dull": "#dddddd"
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_nested_extension(cx: &mut MutableAppContext) {
|
||||
let assets = TestAssets(&[(
|
||||
"themes/theme.toml",
|
||||
r##"
|
||||
[a]
|
||||
text = { extends = "$text.0" }
|
||||
|
||||
[b]
|
||||
extends = "$a"
|
||||
text = { extends = "$text.1" }
|
||||
|
||||
[text]
|
||||
0 = { color = "red" }
|
||||
1 = { color = "blue" }
|
||||
"##,
|
||||
)]);
|
||||
|
||||
let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
|
||||
let theme_data = registry.load("theme", true).unwrap();
|
||||
assert_eq!(
|
||||
theme_data
|
||||
.get("b")
|
||||
.unwrap()
|
||||
.get("text")
|
||||
.unwrap()
|
||||
.get("color")
|
||||
.unwrap(),
|
||||
"blue"
|
||||
);
|
||||
}
|
||||
|
||||
struct TestAssets(&'static [(&'static str, &'static str)]);
|
||||
|
||||
impl AssetSource for TestAssets {
|
||||
fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
|
||||
if let Some(row) = self.0.iter().find(|e| e.0 == path) {
|
||||
Ok(row.1.as_bytes().into())
|
||||
} else {
|
||||
Err(anyhow!("no such path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn list(&self, prefix: &str) -> Vec<std::borrow::Cow<'static, str>> {
|
||||
self.0
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(|(path, _)| {
|
||||
if path.starts_with(prefix) {
|
||||
Some(path.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
315
crates/zed/src/theme_selector.rs
Normal file
|
@ -0,0 +1,315 @@
|
|||
use std::{cmp, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
editor::{self, Editor},
|
||||
fuzzy::{match_strings, StringMatch, StringMatchCandidate},
|
||||
settings::ThemeRegistry,
|
||||
workspace::Workspace,
|
||||
AppState, Settings,
|
||||
};
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
keymap::{self, menu, Binding},
|
||||
AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use postage::watch;
|
||||
|
||||
pub struct ThemeSelector {
|
||||
settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
registry: Arc<ThemeRegistry>,
|
||||
matches: Vec<StringMatch>,
|
||||
query_editor: ViewHandle<Editor>,
|
||||
list_state: UniformListState,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
action!(Confirm);
|
||||
action!(Toggle, Arc<AppState>);
|
||||
action!(Reload, Arc<AppState>);
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
cx.add_action(ThemeSelector::confirm);
|
||||
cx.add_action(ThemeSelector::select_prev);
|
||||
cx.add_action(ThemeSelector::select_next);
|
||||
cx.add_action(ThemeSelector::toggle);
|
||||
cx.add_action(ThemeSelector::reload);
|
||||
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("cmd-k cmd-t", Toggle(app_state.clone()), None),
|
||||
Binding::new("cmd-k t", Reload(app_state.clone()), None),
|
||||
Binding::new("escape", Toggle(app_state.clone()), Some("ThemeSelector")),
|
||||
Binding::new("enter", Confirm, Some("ThemeSelector")),
|
||||
]);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
impl ThemeSelector {
|
||||
fn new(
|
||||
settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
registry: Arc<ThemeRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let query_editor = cx.add_view(|cx| {
|
||||
Editor::single_line(
|
||||
settings.clone(),
|
||||
{
|
||||
let settings = settings.clone();
|
||||
move |_| settings.borrow().theme.selector.input_editor.as_editor()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.subscribe(&query_editor, Self::on_query_editor_event)
|
||||
.detach();
|
||||
|
||||
let mut this = Self {
|
||||
settings,
|
||||
settings_tx,
|
||||
registry,
|
||||
query_editor,
|
||||
matches: Vec::new(),
|
||||
list_state: Default::default(),
|
||||
selected_index: 0,
|
||||
};
|
||||
this.update_matches(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn toggle(workspace: &mut Workspace, action: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |cx, _| {
|
||||
let selector = cx.add_view(|cx| {
|
||||
Self::new(
|
||||
action.0.settings_tx.clone(),
|
||||
action.0.settings.clone(),
|
||||
action.0.themes.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&selector, Self::on_event).detach();
|
||||
selector
|
||||
});
|
||||
}
|
||||
|
||||
fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext<Workspace>) {
|
||||
let current_theme_name = action.0.settings.borrow().theme.name.clone();
|
||||
action.0.themes.clear();
|
||||
match action.0.themes.get(¤t_theme_name) {
|
||||
Ok(theme) => {
|
||||
cx.refresh_windows();
|
||||
action.0.settings_tx.lock().borrow_mut().theme = theme;
|
||||
log::info!("reloaded theme {}", current_theme_name);
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("failed to load theme {}: {:?}", current_theme_name, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some(mat) = self.matches.get(self.selected_index) {
|
||||
match self.registry.get(&mat.string) {
|
||||
Ok(theme) => {
|
||||
self.settings_tx.lock().borrow_mut().theme = theme;
|
||||
cx.refresh_windows();
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
Err(error) => log::error!("error loading theme {}: {}", mat.string, error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_index > 0 {
|
||||
self.selected_index -= 1;
|
||||
}
|
||||
self.list_state.scroll_to(self.selected_index);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_index + 1 < self.matches.len() {
|
||||
self.selected_index += 1;
|
||||
}
|
||||
self.list_state.scroll_to(self.selected_index);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// fn select(&mut self, selected_index: &usize, cx: &mut ViewContext<Self>) {
|
||||
// self.selected_index = *selected_index;
|
||||
// self.confirm(&(), cx);
|
||||
// }
|
||||
|
||||
fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let background = cx.background().clone();
|
||||
let candidates = self
|
||||
.registry
|
||||
.list()
|
||||
.map(|name| StringMatchCandidate {
|
||||
char_bag: name.as_str().into(),
|
||||
string: name,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
|
||||
|
||||
self.matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.map(|candidate| StringMatch {
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
smol::block_on(match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
background,
|
||||
))
|
||||
};
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
workspace: &mut Workspace,
|
||||
_: ViewHandle<ThemeSelector>,
|
||||
event: &Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
Event::Dismissed => {
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_query_editor_event(
|
||||
&mut self,
|
||||
_: ViewHandle<Editor>,
|
||||
event: &editor::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Edited => self.update_matches(cx),
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_matches(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
if self.matches.is_empty() {
|
||||
let settings = self.settings.borrow();
|
||||
return Container::new(
|
||||
Label::new(
|
||||
"No matches".into(),
|
||||
settings.theme.selector.empty.label.clone(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(settings.theme.selector.empty.container)
|
||||
.named("empty matches");
|
||||
}
|
||||
|
||||
let handle = cx.handle();
|
||||
let list = UniformList::new(
|
||||
self.list_state.clone(),
|
||||
self.matches.len(),
|
||||
move |mut range, items, cx| {
|
||||
let cx = cx.as_ref();
|
||||
let selector = handle.upgrade(cx).unwrap();
|
||||
let selector = selector.read(cx);
|
||||
let start = range.start;
|
||||
range.end = cmp::min(range.end, selector.matches.len());
|
||||
items.extend(
|
||||
selector.matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(i, path_match)| selector.render_match(path_match, start + i)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Container::new(list.boxed())
|
||||
.with_margin_top(6.0)
|
||||
.named("matches")
|
||||
}
|
||||
|
||||
fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
let theme = &settings.theme;
|
||||
|
||||
let container = Container::new(
|
||||
Label::new(
|
||||
theme_match.string.clone(),
|
||||
if index == self.selected_index {
|
||||
theme.selector.active_item.label.clone()
|
||||
} else {
|
||||
theme.selector.item.label.clone()
|
||||
},
|
||||
)
|
||||
.with_highlights(theme_match.positions.clone())
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(if index == self.selected_index {
|
||||
theme.selector.active_item.container
|
||||
} else {
|
||||
theme.selector.item.container
|
||||
});
|
||||
|
||||
container.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ThemeSelector {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for ThemeSelector {
|
||||
fn ui_name() -> &'static str {
|
||||
"ThemeSelector"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(ChildView::new(self.query_editor.id()).boxed())
|
||||
.with_child(Flexible::new(1.0, self.render_matches(cx)).boxed())
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(settings.theme.selector.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_max_width(600.0)
|
||||
.with_max_height(400.0)
|
||||
.boxed(),
|
||||
)
|
||||
.top()
|
||||
.named("theme selector")
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.focus(&self.query_editor);
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx
|
||||
}
|
||||
}
|
268
crates/zed/src/user.rs
Normal file
|
@ -0,0 +1,268 @@
|
|||
use crate::http::{HttpClient, Method, Request, Url};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::future;
|
||||
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
|
||||
use postage::{prelude::Stream, sink::Sink, watch};
|
||||
use rpc_client as rpc;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::TryFutureExt as _;
|
||||
use zrpc::{proto, TypedEnvelope};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct User {
|
||||
pub id: u64,
|
||||
pub github_login: String,
|
||||
pub avatar: Option<Arc<ImageData>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Collaborator {
|
||||
pub user: Arc<User>,
|
||||
pub worktrees: Vec<WorktreeMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WorktreeMetadata {
|
||||
pub id: u64,
|
||||
pub root_name: String,
|
||||
pub is_shared: bool,
|
||||
pub guests: Vec<Arc<User>>,
|
||||
}
|
||||
|
||||
pub struct UserStore {
|
||||
users: HashMap<u64, Arc<User>>,
|
||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||
collaborators: Arc<[Collaborator]>,
|
||||
rpc: Arc<rpc::Client>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
_maintain_collaborators: Task<()>,
|
||||
_maintain_current_user: Task<()>,
|
||||
}
|
||||
|
||||
pub enum Event {}
|
||||
|
||||
impl Entity for UserStore {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl UserStore {
|
||||
pub fn new(
|
||||
rpc: Arc<rpc::Client>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let (mut current_user_tx, current_user_rx) = watch::channel();
|
||||
let (mut update_collaborators_tx, mut update_collaborators_rx) =
|
||||
watch::channel::<Option<proto::UpdateCollaborators>>();
|
||||
let update_collaborators_subscription = rpc.subscribe(
|
||||
cx,
|
||||
move |_: &mut Self, msg: TypedEnvelope<proto::UpdateCollaborators>, _, _| {
|
||||
let _ = update_collaborators_tx.blocking_send(Some(msg.payload));
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
Self {
|
||||
users: Default::default(),
|
||||
current_user: current_user_rx,
|
||||
collaborators: Arc::from([]),
|
||||
rpc: rpc.clone(),
|
||||
http,
|
||||
_maintain_collaborators: cx.spawn_weak(|this, mut cx| async move {
|
||||
let _subscription = update_collaborators_subscription;
|
||||
while let Some(message) = update_collaborators_rx.recv().await {
|
||||
if let Some((message, this)) = message.zip(this.upgrade(&cx)) {
|
||||
this.update(&mut cx, |this, cx| this.update_collaborators(message, cx))
|
||||
.log_err()
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}),
|
||||
_maintain_current_user: cx.spawn_weak(|this, mut cx| async move {
|
||||
let mut status = rpc.status();
|
||||
while let Some(status) = status.recv().await {
|
||||
match status {
|
||||
rpc::Status::Connected { .. } => {
|
||||
if let Some((this, user_id)) = this.upgrade(&cx).zip(rpc.user_id()) {
|
||||
let user = this
|
||||
.update(&mut cx, |this, cx| this.fetch_user(user_id, cx))
|
||||
.log_err()
|
||||
.await;
|
||||
current_user_tx.send(user).await.ok();
|
||||
}
|
||||
}
|
||||
rpc::Status::SignedOut => {
|
||||
current_user_tx.send(None).await.ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_collaborators(
|
||||
&mut self,
|
||||
message: proto::UpdateCollaborators,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let mut user_ids = HashSet::new();
|
||||
for collaborator in &message.collaborators {
|
||||
user_ids.insert(collaborator.user_id);
|
||||
user_ids.extend(
|
||||
collaborator
|
||||
.worktrees
|
||||
.iter()
|
||||
.flat_map(|w| &w.guests)
|
||||
.copied(),
|
||||
);
|
||||
}
|
||||
|
||||
let load_users = self.load_users(user_ids.into_iter().collect(), cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
load_users.await?;
|
||||
|
||||
let mut collaborators = Vec::new();
|
||||
for collaborator in message.collaborators {
|
||||
collaborators.push(Collaborator::from_proto(collaborator, &this, &mut cx).await?);
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
collaborators.sort_by(|a, b| a.user.github_login.cmp(&b.user.github_login));
|
||||
this.collaborators = collaborators.into();
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn collaborators(&self) -> &Arc<[Collaborator]> {
|
||||
&self.collaborators
|
||||
}
|
||||
|
||||
pub fn load_users(
|
||||
&mut self,
|
||||
mut user_ids: Vec<u64>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let rpc = self.rpc.clone();
|
||||
let http = self.http.clone();
|
||||
user_ids.retain(|id| !self.users.contains_key(id));
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
if !user_ids.is_empty() {
|
||||
let response = rpc.request(proto::GetUsers { user_ids }).await?;
|
||||
let new_users = future::join_all(
|
||||
response
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|user| User::new(user, http.as_ref())),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, _| {
|
||||
for user in new_users {
|
||||
this.users.insert(user.id, Arc::new(user));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetch_user(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Arc<User>>> {
|
||||
if let Some(user) = self.users.get(&user_id).cloned() {
|
||||
return cx.spawn_weak(|_, _| async move { Ok(user) });
|
||||
}
|
||||
|
||||
let load_users = self.load_users(vec![user_id], cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
load_users.await?;
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.users
|
||||
.get(&user_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("server responded with no users"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn current_user(&self) -> Option<Arc<User>> {
|
||||
self.current_user.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
|
||||
self.current_user.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
async fn new(message: proto::User, http: &dyn HttpClient) -> Self {
|
||||
User {
|
||||
id: message.id,
|
||||
github_login: message.github_login,
|
||||
avatar: fetch_avatar(http, &message.avatar_url).log_err().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Collaborator {
|
||||
async fn from_proto(
|
||||
collaborator: proto::Collaborator,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let user = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.fetch_user(collaborator.user_id, cx)
|
||||
})
|
||||
.await?;
|
||||
let mut worktrees = Vec::new();
|
||||
for worktree in collaborator.worktrees {
|
||||
let mut guests = Vec::new();
|
||||
for participant_id in worktree.guests {
|
||||
guests.push(
|
||||
user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.fetch_user(participant_id, cx)
|
||||
})
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
worktrees.push(WorktreeMetadata {
|
||||
id: worktree.id,
|
||||
root_name: worktree.root_name,
|
||||
is_shared: worktree.is_shared,
|
||||
guests,
|
||||
});
|
||||
}
|
||||
Ok(Self { user, worktrees })
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
||||
let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?;
|
||||
let mut request = Request::new(Method::Get, url);
|
||||
request.middleware(surf::middleware::Redirect::default());
|
||||
|
||||
let mut response = http
|
||||
.send(request)
|
||||
.await
|
||||
.map_err(|e| anyhow!("failed to send user avatar request: {}", e))?;
|
||||
let bytes = response
|
||||
.body_bytes()
|
||||
.await
|
||||
.map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?;
|
||||
let format = image::guess_format(&bytes)?;
|
||||
let image = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
|
||||
Ok(ImageData::new(image))
|
||||
}
|
1718
crates/zed/src/workspace.rs
Normal file
372
crates/zed/src/workspace/pane.rs
Normal file
|
@ -0,0 +1,372 @@
|
|||
use super::{ItemViewHandle, SplitDirection};
|
||||
use crate::{project::ProjectPath, settings::Settings};
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
keymap::Binding,
|
||||
platform::CursorStyle,
|
||||
Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use std::cmp;
|
||||
|
||||
action!(Split, SplitDirection);
|
||||
action!(ActivateItem, usize);
|
||||
action!(ActivatePrevItem);
|
||||
action!(ActivateNextItem);
|
||||
action!(CloseActiveItem);
|
||||
action!(CloseItem, usize);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
||||
pane.activate_item(action.0, cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
|
||||
pane.activate_prev_item(cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
|
||||
pane.activate_next_item(cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
|
||||
pane.close_active_item(cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
|
||||
pane.close_item(action.0, cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, action: &Split, cx| {
|
||||
pane.split(action.0, cx);
|
||||
});
|
||||
|
||||
cx.add_bindings(vec![
|
||||
Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
|
||||
Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
|
||||
Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
|
||||
Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
|
||||
Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
|
||||
Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
|
||||
Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
|
||||
]);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Activate,
|
||||
Remove,
|
||||
Split(SplitDirection),
|
||||
}
|
||||
|
||||
const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct State {
|
||||
pub tabs: Vec<TabState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct TabState {
|
||||
pub title: String,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
pub struct Pane {
|
||||
items: Vec<Box<dyn ItemViewHandle>>,
|
||||
active_item: usize,
|
||||
settings: watch::Receiver<Settings>,
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
pub fn new(settings: watch::Receiver<Settings>) -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
active_item: 0,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate(&self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Activate);
|
||||
}
|
||||
|
||||
pub fn add_item(&mut self, item: Box<dyn ItemViewHandle>, cx: &mut ViewContext<Self>) -> usize {
|
||||
let item_idx = cmp::min(self.active_item + 1, self.items.len());
|
||||
self.items.insert(item_idx, item);
|
||||
cx.notify();
|
||||
item_idx
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn items(&self) -> &[Box<dyn ItemViewHandle>] {
|
||||
&self.items
|
||||
}
|
||||
|
||||
pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
|
||||
self.items.get(self.active_item).cloned()
|
||||
}
|
||||
|
||||
pub fn activate_entry(
|
||||
&mut self,
|
||||
project_path: ProjectPath,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
if let Some(index) = self.items.iter().position(|item| {
|
||||
item.project_path(cx.as_ref())
|
||||
.map_or(false, |item_path| item_path == project_path)
|
||||
}) {
|
||||
self.activate_item(index, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
|
||||
self.items.iter().position(|i| i.id() == item.id())
|
||||
}
|
||||
|
||||
pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
|
||||
if index < self.items.len() {
|
||||
self.active_item = index;
|
||||
self.focus_active_item(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.active_item > 0 {
|
||||
self.active_item -= 1;
|
||||
} else if self.items.len() > 0 {
|
||||
self.active_item = self.items.len() - 1;
|
||||
}
|
||||
self.focus_active_item(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.active_item + 1 < self.items.len() {
|
||||
self.active_item += 1;
|
||||
} else {
|
||||
self.active_item = 0;
|
||||
}
|
||||
self.focus_active_item(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.items.is_empty() {
|
||||
self.close_item(self.items[self.active_item].id(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext<Self>) {
|
||||
self.items.retain(|item| item.id() != item_id);
|
||||
self.active_item = cmp::min(self.active_item, self.items.len().saturating_sub(1));
|
||||
if self.items.is_empty() {
|
||||
cx.emit(Event::Remove);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
cx.focus(active_item.to_any());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Split(direction));
|
||||
}
|
||||
|
||||
fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
let theme = &settings.theme;
|
||||
|
||||
enum Tabs {}
|
||||
let tabs = MouseEventHandler::new::<Tabs, _, _, _>(0, cx, |mouse_state, cx| {
|
||||
let mut row = Flex::row();
|
||||
for (ix, item) in self.items.iter().enumerate() {
|
||||
let is_active = ix == self.active_item;
|
||||
|
||||
row.add_child({
|
||||
let mut title = item.title(cx);
|
||||
if title.len() > MAX_TAB_TITLE_LEN {
|
||||
let mut truncated_len = MAX_TAB_TITLE_LEN;
|
||||
while !title.is_char_boundary(truncated_len) {
|
||||
truncated_len -= 1;
|
||||
}
|
||||
title.truncate(truncated_len);
|
||||
title.push('…');
|
||||
}
|
||||
|
||||
let mut style = if is_active {
|
||||
theme.workspace.active_tab.clone()
|
||||
} else {
|
||||
theme.workspace.tab.clone()
|
||||
};
|
||||
if ix == 0 {
|
||||
style.container.border.left = false;
|
||||
}
|
||||
|
||||
EventHandler::new(
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Align::new({
|
||||
let diameter = 7.0;
|
||||
let icon_color = if item.has_conflict(cx) {
|
||||
Some(style.icon_conflict)
|
||||
} else if item.is_dirty(cx) {
|
||||
Some(style.icon_dirty)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ConstrainedBox::new(
|
||||
Canvas::new(move |bounds, _, cx| {
|
||||
if let Some(color) = icon_color {
|
||||
let square = RectF::new(
|
||||
bounds.origin(),
|
||||
vec2f(diameter, diameter),
|
||||
);
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: square,
|
||||
background: Some(color),
|
||||
border: Default::default(),
|
||||
corner_radius: diameter / 2.,
|
||||
});
|
||||
}
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_width(diameter)
|
||||
.with_height(diameter)
|
||||
.boxed()
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
Align::new(
|
||||
Label::new(
|
||||
title,
|
||||
if is_active {
|
||||
theme.workspace.active_tab.label.clone()
|
||||
} else {
|
||||
theme.workspace.tab.label.clone()
|
||||
},
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(ContainerStyle {
|
||||
margin: Margin {
|
||||
left: style.spacing,
|
||||
right: style.spacing,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Align::new(
|
||||
ConstrainedBox::new(if mouse_state.hovered {
|
||||
let item_id = item.id();
|
||||
enum TabCloseButton {}
|
||||
let icon = Svg::new("icons/x.svg");
|
||||
MouseEventHandler::new::<TabCloseButton, _, _, _>(
|
||||
item_id,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
if mouse_state.hovered {
|
||||
icon.with_color(style.icon_close_active)
|
||||
.boxed()
|
||||
} else {
|
||||
icon.with_color(style.icon_close).boxed()
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_padding(Padding::uniform(4.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |cx| {
|
||||
cx.dispatch_action(CloseItem(item_id))
|
||||
})
|
||||
.named("close-tab-icon")
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
})
|
||||
.with_width(style.icon_width)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(style.container)
|
||||
.boxed(),
|
||||
)
|
||||
.on_mouse_down(move |cx| {
|
||||
cx.dispatch_action(ActivateItem(ix));
|
||||
true
|
||||
})
|
||||
.boxed()
|
||||
})
|
||||
}
|
||||
|
||||
row.add_child(
|
||||
Expanded::new(
|
||||
0.0,
|
||||
Container::new(Empty::new().boxed())
|
||||
.with_border(theme.workspace.tab.container.border)
|
||||
.boxed(),
|
||||
)
|
||||
.named("filler"),
|
||||
);
|
||||
|
||||
row.boxed()
|
||||
});
|
||||
|
||||
ConstrainedBox::new(tabs.boxed())
|
||||
.with_height(theme.workspace.tab.height)
|
||||
.named("tabs")
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Pane {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for Pane {
|
||||
fn ui_name() -> &'static str {
|
||||
"Pane"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
Flex::column()
|
||||
.with_child(self.render_tabs(cx))
|
||||
.with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed())
|
||||
.named("pane")
|
||||
} else {
|
||||
Empty::new().named("pane")
|
||||
}
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.focus_active_item(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PaneHandle {
|
||||
fn add_item_view(&self, item: Box<dyn ItemViewHandle>, cx: &mut MutableAppContext);
|
||||
}
|
||||
|
||||
impl PaneHandle for ViewHandle<Pane> {
|
||||
fn add_item_view(&self, item: Box<dyn ItemViewHandle>, cx: &mut MutableAppContext) {
|
||||
item.set_parent_pane(self, cx);
|
||||
self.update(cx, |pane, cx| {
|
||||
let item_idx = pane.add_item(item, cx);
|
||||
pane.activate_item(item_idx, cx);
|
||||
});
|
||||
}
|
||||
}
|
384
crates/zed/src/workspace/pane_group.rs
Normal file
|
@ -0,0 +1,384 @@
|
|||
use crate::theme::Theme;
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{elements::*, Axis};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PaneGroup {
|
||||
root: Member,
|
||||
}
|
||||
|
||||
impl PaneGroup {
|
||||
pub fn new(pane_id: usize) -> Self {
|
||||
Self {
|
||||
root: Member::Pane(pane_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split(
|
||||
&mut self,
|
||||
old_pane_id: usize,
|
||||
new_pane_id: usize,
|
||||
direction: SplitDirection,
|
||||
) -> Result<()> {
|
||||
match &mut self.root {
|
||||
Member::Pane(pane_id) => {
|
||||
if *pane_id == old_pane_id {
|
||||
self.root = Member::new_axis(old_pane_id, new_pane_id, direction);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Pane not found"))
|
||||
}
|
||||
}
|
||||
Member::Axis(axis) => axis.split(old_pane_id, new_pane_id, direction),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, pane_id: usize) -> Result<bool> {
|
||||
match &mut self.root {
|
||||
Member::Pane(_) => Ok(false),
|
||||
Member::Axis(axis) => {
|
||||
if let Some(last_pane) = axis.remove(pane_id)? {
|
||||
self.root = last_pane;
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render<'a>(&self, theme: &Theme) -> ElementBox {
|
||||
self.root.render(theme)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum Member {
|
||||
Axis(PaneAxis),
|
||||
Pane(usize),
|
||||
}
|
||||
|
||||
impl Member {
|
||||
fn new_axis(old_pane_id: usize, new_pane_id: usize, direction: SplitDirection) -> Self {
|
||||
use Axis::*;
|
||||
use SplitDirection::*;
|
||||
|
||||
let axis = match direction {
|
||||
Up | Down => Vertical,
|
||||
Left | Right => Horizontal,
|
||||
};
|
||||
|
||||
let members = match direction {
|
||||
Up | Left => vec![Member::Pane(new_pane_id), Member::Pane(old_pane_id)],
|
||||
Down | Right => vec![Member::Pane(old_pane_id), Member::Pane(new_pane_id)],
|
||||
};
|
||||
|
||||
Member::Axis(PaneAxis { axis, members })
|
||||
}
|
||||
|
||||
pub fn render<'a>(&self, theme: &Theme) -> ElementBox {
|
||||
match self {
|
||||
Member::Pane(view_id) => ChildView::new(*view_id).boxed(),
|
||||
Member::Axis(axis) => axis.render(theme),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct PaneAxis {
|
||||
axis: Axis,
|
||||
members: Vec<Member>,
|
||||
}
|
||||
|
||||
impl PaneAxis {
|
||||
fn split(
|
||||
&mut self,
|
||||
old_pane_id: usize,
|
||||
new_pane_id: usize,
|
||||
direction: SplitDirection,
|
||||
) -> Result<()> {
|
||||
use SplitDirection::*;
|
||||
|
||||
for (idx, member) in self.members.iter_mut().enumerate() {
|
||||
match member {
|
||||
Member::Axis(axis) => {
|
||||
if axis.split(old_pane_id, new_pane_id, direction).is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Member::Pane(pane_id) => {
|
||||
if *pane_id == old_pane_id {
|
||||
if direction.matches_axis(self.axis) {
|
||||
match direction {
|
||||
Up | Left => {
|
||||
self.members.insert(idx, Member::Pane(new_pane_id));
|
||||
}
|
||||
Down | Right => {
|
||||
self.members.insert(idx + 1, Member::Pane(new_pane_id));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
*member = Member::new_axis(old_pane_id, new_pane_id, direction);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(anyhow!("Pane not found"))
|
||||
}
|
||||
|
||||
fn remove(&mut self, pane_id_to_remove: usize) -> Result<Option<Member>> {
|
||||
let mut found_pane = false;
|
||||
let mut remove_member = None;
|
||||
for (idx, member) in self.members.iter_mut().enumerate() {
|
||||
match member {
|
||||
Member::Axis(axis) => {
|
||||
if let Ok(last_pane) = axis.remove(pane_id_to_remove) {
|
||||
if let Some(last_pane) = last_pane {
|
||||
*member = last_pane;
|
||||
}
|
||||
found_pane = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Member::Pane(pane_id) => {
|
||||
if *pane_id == pane_id_to_remove {
|
||||
found_pane = true;
|
||||
remove_member = Some(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found_pane {
|
||||
if let Some(idx) = remove_member {
|
||||
self.members.remove(idx);
|
||||
}
|
||||
|
||||
if self.members.len() == 1 {
|
||||
Ok(self.members.pop())
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!("Pane not found"))
|
||||
}
|
||||
}
|
||||
|
||||
fn render<'a>(&self, theme: &Theme) -> ElementBox {
|
||||
let last_member_ix = self.members.len() - 1;
|
||||
Flex::new(self.axis)
|
||||
.with_children(self.members.iter().enumerate().map(|(ix, member)| {
|
||||
let mut member = member.render(theme);
|
||||
if ix < last_member_ix {
|
||||
let mut border = theme.workspace.pane_divider;
|
||||
border.left = false;
|
||||
border.right = false;
|
||||
border.top = false;
|
||||
border.bottom = false;
|
||||
match self.axis {
|
||||
Axis::Vertical => border.bottom = true,
|
||||
Axis::Horizontal => border.right = true,
|
||||
}
|
||||
member = Container::new(member).with_border(border).boxed();
|
||||
}
|
||||
|
||||
Expanded::new(1.0, member).boxed()
|
||||
}))
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum SplitDirection {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl SplitDirection {
|
||||
fn matches_axis(self, orientation: Axis) -> bool {
|
||||
use Axis::*;
|
||||
use SplitDirection::*;
|
||||
|
||||
match self {
|
||||
Up | Down => match orientation {
|
||||
Vertical => true,
|
||||
Horizontal => false,
|
||||
},
|
||||
Left | Right => match orientation {
|
||||
Vertical => false,
|
||||
Horizontal => true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// use super::*;
|
||||
// use serde_json::json;
|
||||
|
||||
// #[test]
|
||||
// fn test_split_and_remove() -> Result<()> {
|
||||
// let mut group = PaneGroup::new(1);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "pane",
|
||||
// "paneId": 1,
|
||||
// })
|
||||
// );
|
||||
|
||||
// group.split(1, 2, SplitDirection::Right)?;
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// group.split(2, 3, SplitDirection::Up)?;
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {
|
||||
// "type": "axis",
|
||||
// "orientation": "vertical",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 3},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// group.split(1, 4, SplitDirection::Right)?;
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {"type": "pane", "paneId": 4},
|
||||
// {
|
||||
// "type": "axis",
|
||||
// "orientation": "vertical",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 3},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// group.split(2, 5, SplitDirection::Up)?;
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {"type": "pane", "paneId": 4},
|
||||
// {
|
||||
// "type": "axis",
|
||||
// "orientation": "vertical",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 3},
|
||||
// {"type": "pane", "paneId": 5},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// assert_eq!(true, group.remove(5)?);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {"type": "pane", "paneId": 4},
|
||||
// {
|
||||
// "type": "axis",
|
||||
// "orientation": "vertical",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 3},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// assert_eq!(true, group.remove(4)?);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {
|
||||
// "type": "axis",
|
||||
// "orientation": "vertical",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 3},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// assert_eq!(true, group.remove(3)?);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "axis",
|
||||
// "orientation": "horizontal",
|
||||
// "members": [
|
||||
// {"type": "pane", "paneId": 1},
|
||||
// {"type": "pane", "paneId": 2},
|
||||
// ]
|
||||
// })
|
||||
// );
|
||||
|
||||
// assert_eq!(true, group.remove(2)?);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "pane",
|
||||
// "paneId": 1,
|
||||
// })
|
||||
// );
|
||||
|
||||
// assert_eq!(false, group.remove(1)?);
|
||||
// assert_eq!(
|
||||
// serde_json::to_value(&group)?,
|
||||
// json!({
|
||||
// "type": "pane",
|
||||
// "paneId": 1,
|
||||
// })
|
||||
// );
|
||||
|
||||
// Ok(())
|
||||
// }
|
||||
}
|
200
crates/zed/src/workspace/sidebar.rs
Normal file
|
@ -0,0 +1,200 @@
|
|||
use super::Workspace;
|
||||
use crate::{theme, Settings};
|
||||
use gpui::{
|
||||
action, elements::*, platform::CursorStyle, AnyViewHandle, MutableAppContext, RenderContext,
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
pub struct Sidebar {
|
||||
side: Side,
|
||||
items: Vec<Item>,
|
||||
active_item_ix: Option<usize>,
|
||||
width: Rc<RefCell<f32>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Side {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
struct Item {
|
||||
icon_path: &'static str,
|
||||
view: AnyViewHandle,
|
||||
}
|
||||
|
||||
action!(ToggleSidebarItem, SidebarItemId);
|
||||
action!(ToggleSidebarItemFocus, SidebarItemId);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SidebarItemId {
|
||||
pub side: Side,
|
||||
pub item_index: usize,
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
pub fn new(side: Side) -> Self {
|
||||
Self {
|
||||
side,
|
||||
items: Default::default(),
|
||||
active_item_ix: None,
|
||||
width: Rc::new(RefCell::new(260.)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_item(&mut self, icon_path: &'static str, view: AnyViewHandle) {
|
||||
self.items.push(Item { icon_path, view });
|
||||
}
|
||||
|
||||
pub fn activate_item(&mut self, item_ix: usize) {
|
||||
self.active_item_ix = Some(item_ix);
|
||||
}
|
||||
|
||||
pub fn toggle_item(&mut self, item_ix: usize) {
|
||||
if self.active_item_ix == Some(item_ix) {
|
||||
self.active_item_ix = None;
|
||||
} else {
|
||||
self.active_item_ix = Some(item_ix);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_item(&self) -> Option<&AnyViewHandle> {
|
||||
self.active_item_ix
|
||||
.and_then(|ix| self.items.get(ix))
|
||||
.map(|item| &item.view)
|
||||
}
|
||||
|
||||
fn theme<'a>(&self, settings: &'a Settings) -> &'a theme::Sidebar {
|
||||
match self.side {
|
||||
Side::Left => &settings.theme.workspace.left_sidebar,
|
||||
Side::Right => &settings.theme.workspace.right_sidebar,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, settings: &Settings, cx: &mut RenderContext<Workspace>) -> ElementBox {
|
||||
let side = self.side;
|
||||
let theme = self.theme(settings);
|
||||
|
||||
ConstrainedBox::new(
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_children(self.items.iter().enumerate().map(|(item_index, item)| {
|
||||
let theme = if Some(item_index) == self.active_item_ix {
|
||||
&theme.active_item
|
||||
} else {
|
||||
&theme.item
|
||||
};
|
||||
enum SidebarButton {}
|
||||
MouseEventHandler::new::<SidebarButton, _, _, _>(
|
||||
item.view.id(),
|
||||
cx,
|
||||
|_, _| {
|
||||
ConstrainedBox::new(
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Svg::new(item.icon_path)
|
||||
.with_color(theme.icon_color)
|
||||
.boxed(),
|
||||
)
|
||||
.with_height(theme.icon_size)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_height(theme.height)
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_mouse_down(move |cx| {
|
||||
cx.dispatch_action(ToggleSidebarItem(SidebarItemId {
|
||||
side,
|
||||
item_index,
|
||||
}))
|
||||
})
|
||||
.boxed()
|
||||
}))
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_width(theme.width)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn render_active_item(
|
||||
&self,
|
||||
settings: &Settings,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<ElementBox> {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
let mut container = Flex::row();
|
||||
if matches!(self.side, Side::Right) {
|
||||
container.add_child(self.render_resize_handle(settings, cx));
|
||||
}
|
||||
|
||||
container.add_child(
|
||||
Flexible::new(
|
||||
1.,
|
||||
Hook::new(
|
||||
ConstrainedBox::new(ChildView::new(active_item.id()).boxed())
|
||||
.with_max_width(*self.width.borrow())
|
||||
.boxed(),
|
||||
)
|
||||
.on_after_layout({
|
||||
let width = self.width.clone();
|
||||
move |size, _| *width.borrow_mut() = size.x()
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
);
|
||||
if matches!(self.side, Side::Left) {
|
||||
container.add_child(self.render_resize_handle(settings, cx));
|
||||
}
|
||||
Some(container.boxed())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_resize_handle(
|
||||
&self,
|
||||
settings: &Settings,
|
||||
mut cx: &mut MutableAppContext,
|
||||
) -> ElementBox {
|
||||
let width = self.width.clone();
|
||||
let side = self.side;
|
||||
MouseEventHandler::new::<Self, _, _, _>(self.side.id(), &mut cx, |_, _| {
|
||||
Container::new(Empty::new().boxed())
|
||||
.with_style(self.theme(settings).resize_handle)
|
||||
.boxed()
|
||||
})
|
||||
.with_padding(Padding {
|
||||
left: 4.,
|
||||
right: 4.,
|
||||
..Default::default()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::ResizeLeftRight)
|
||||
.on_drag(move |delta, cx| {
|
||||
let prev_width = *width.borrow();
|
||||
match side {
|
||||
Side::Left => *width.borrow_mut() = 0f32.max(prev_width + delta.x()),
|
||||
Side::Right => *width.borrow_mut() = 0f32.max(prev_width - delta.x()),
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Side {
|
||||
fn id(self) -> usize {
|
||||
match self {
|
||||
Side::Left => 0,
|
||||
Side::Right => 1,
|
||||
}
|
||||
}
|
||||
}
|