Move all crates to a top-level crates folder

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Nathan Sobo 2021-10-04 13:22:21 -06:00
parent d768224182
commit fdfed3d7db
282 changed files with 195588 additions and 16 deletions

20
crates/zed/src/assets.rs Normal file
View 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
View 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())
]
);
});
}
}

View 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

File diff suppressed because it is too large Load diff

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

File diff suppressed because it is too large Load diff

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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

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

View 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
View 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
View 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
View 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),
},
],
},
]
}

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

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

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

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

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

View 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(&current_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
View 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

File diff suppressed because it is too large Load diff

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

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

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