Compare commits
12 Commits
notebook--
...
v0.107.2-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df63290a32 | ||
|
|
6098f94dc1 | ||
|
|
6f4dee5b1d | ||
|
|
4ca2645a54 | ||
|
|
c41a3ec01b | ||
|
|
4edd0365a1 | ||
|
|
cc4fb1c1b5 | ||
|
|
fc3d754aea | ||
|
|
643f3db2b2 | ||
|
|
b90c04009f | ||
|
|
11f7a2cb0e | ||
|
|
8bdc59703a |
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -1467,7 +1467,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.22.1"
|
||||
version = "0.22.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1563,6 +1563,7 @@ dependencies = [
|
||||
"postage",
|
||||
"project",
|
||||
"recent_projects",
|
||||
"rich_text",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -2405,6 +2406,7 @@ dependencies = [
|
||||
"project",
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.5",
|
||||
"rich_text",
|
||||
"rpc",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -6242,6 +6244,24 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich_text"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"pulldown-cmark",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"theme",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
@@ -10063,7 +10083,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.107.0"
|
||||
version = "0.107.2"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -64,6 +64,7 @@ members = [
|
||||
"crates/sqlez",
|
||||
"crates/sqlez_macros",
|
||||
"crates/feature_flags",
|
||||
"crates/rich_text",
|
||||
"crates/storybook",
|
||||
"crates/sum_tree",
|
||||
"crates/terminal",
|
||||
|
||||
@@ -36,7 +36,7 @@ pub struct ChannelMessage {
|
||||
pub nonce: u128,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ChannelMessageId {
|
||||
Saved(u64),
|
||||
Pending(usize),
|
||||
|
||||
@@ -8,6 +8,7 @@ use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
|
||||
use tempfile::NamedTempFile;
|
||||
use util::http::HttpClient;
|
||||
use util::{channel::ReleaseChannel, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Telemetry {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
@@ -18,7 +19,8 @@ pub struct Telemetry {
|
||||
#[derive(Default)]
|
||||
struct TelemetryState {
|
||||
metrics_id: Option<Arc<str>>, // Per logged-in user
|
||||
installation_id: Option<Arc<str>>, // Per app installation
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
|
||||
session_id: String, // Per app launch
|
||||
app_version: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
os_name: &'static str,
|
||||
@@ -41,6 +43,7 @@ lazy_static! {
|
||||
struct ClickhouseEventRequestBody {
|
||||
token: &'static str,
|
||||
installation_id: Option<Arc<str>>,
|
||||
session_id: String,
|
||||
is_staff: Option<bool>,
|
||||
app_version: Option<Arc<str>>,
|
||||
os_name: &'static str,
|
||||
@@ -131,6 +134,7 @@ impl Telemetry {
|
||||
release_channel,
|
||||
installation_id: None,
|
||||
metrics_id: None,
|
||||
session_id: Uuid::new_v4().to_string(),
|
||||
clickhouse_events_queue: Default::default(),
|
||||
flush_clickhouse_events_task: Default::default(),
|
||||
log_file: None,
|
||||
@@ -285,6 +289,7 @@ impl Telemetry {
|
||||
&ClickhouseEventRequestBody {
|
||||
token: ZED_SECRET_CLIENT_TOKEN,
|
||||
installation_id: state.installation_id.clone(),
|
||||
session_id: state.session_id.clone(),
|
||||
is_staff: state.is_staff.clone(),
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name,
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.22.1"
|
||||
version = "0.22.2"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -9,13 +9,3 @@ pub mod projects;
|
||||
pub mod rooms;
|
||||
pub mod servers;
|
||||
pub mod users;
|
||||
|
||||
fn max_assign<T: Ord>(max: &mut Option<T>, val: T) {
|
||||
if let Some(max_val) = max {
|
||||
if val > *max_val {
|
||||
*max = Some(val);
|
||||
}
|
||||
} else {
|
||||
*max = Some(val);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,17 +89,14 @@ impl Database {
|
||||
|
||||
let mut rows = channel_message::Entity::find()
|
||||
.filter(condition)
|
||||
.order_by_asc(channel_message::Column::Id)
|
||||
.limit(count as u64)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut max_id = None;
|
||||
let mut messages = Vec::new();
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
|
||||
max_assign(&mut max_id, row.id);
|
||||
|
||||
let nonce = row.nonce.as_u64_pair();
|
||||
messages.push(proto::ChannelMessage {
|
||||
id: row.id.to_proto(),
|
||||
@@ -113,50 +110,6 @@ impl Database {
|
||||
});
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
if let Some(max_id) = max_id {
|
||||
let has_older_message = observed_channel_messages::Entity::find()
|
||||
.filter(
|
||||
observed_channel_messages::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(observed_channel_messages::Column::ChannelId.eq(channel_id))
|
||||
.and(observed_channel_messages::Column::ChannelMessageId.lt(max_id)),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.is_some();
|
||||
|
||||
if has_older_message {
|
||||
observed_channel_messages::Entity::update(
|
||||
observed_channel_messages::ActiveModel {
|
||||
user_id: ActiveValue::Unchanged(user_id),
|
||||
channel_id: ActiveValue::Unchanged(channel_id),
|
||||
channel_message_id: ActiveValue::Set(max_id),
|
||||
},
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
} else {
|
||||
observed_channel_messages::Entity::insert(
|
||||
observed_channel_messages::ActiveModel {
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
channel_id: ActiveValue::Set(channel_id),
|
||||
channel_message_id: ActiveValue::Set(max_id),
|
||||
},
|
||||
)
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
observed_channel_messages::Column::UserId,
|
||||
observed_channel_messages::Column::ChannelId,
|
||||
])
|
||||
.update_columns([observed_channel_messages::Column::ChannelMessageId])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(messages)
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -1906,13 +1906,10 @@ async fn follow(
|
||||
.check_room_participants(room_id, leader_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
let mut response_payload = session
|
||||
let response_payload = session
|
||||
.peer
|
||||
.forward_request(session.connection_id, leader_id, request)
|
||||
.await?;
|
||||
response_payload
|
||||
.views
|
||||
.retain(|view| view.leader_id != Some(follower_id.into()));
|
||||
response.send(response_payload)?;
|
||||
|
||||
if let Some(project_id) = project_id {
|
||||
@@ -1973,14 +1970,17 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
|
||||
.await?
|
||||
};
|
||||
|
||||
let leader_id = request.variant.as_ref().and_then(|variant| match variant {
|
||||
proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
|
||||
// For now, don't send view update messages back to that view's current leader.
|
||||
let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
|
||||
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
|
||||
proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
|
||||
_ => None,
|
||||
});
|
||||
|
||||
for follower_peer_id in request.follower_ids.iter().copied() {
|
||||
let follower_connection_id = follower_peer_id.into();
|
||||
if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) {
|
||||
if Some(follower_peer_id) != connection_id_to_omit
|
||||
&& connection_ids.contains(&follower_connection_id)
|
||||
{
|
||||
session.peer.forward_send(
|
||||
session.connection_id,
|
||||
follower_connection_id,
|
||||
|
||||
@@ -4,6 +4,7 @@ use collab_ui::project_shared_notification::ProjectSharedNotification;
|
||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use rpc::proto::PeerId;
|
||||
use serde_json::json;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use workspace::{
|
||||
@@ -724,10 +725,9 @@ async fn test_peers_following_each_other(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A opens some editors.
|
||||
// Client A opens a file.
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let _editor_a1 = workspace_a
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
})
|
||||
@@ -736,10 +736,9 @@ async fn test_peers_following_each_other(
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
// Client B opens an editor.
|
||||
// Client B opens a different file.
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
let _editor_b1 = workspace_b
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
})
|
||||
@@ -754,9 +753,7 @@ async fn test_peers_following_each_other(
|
||||
});
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
assert_ne!(*workspace.active_pane(), pane_a1);
|
||||
let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
|
||||
workspace.follow(leader_id, cx).unwrap()
|
||||
workspace.follow(client_b.peer_id().unwrap(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -765,85 +762,443 @@ async fn test_peers_following_each_other(
|
||||
});
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_ne!(*workspace.active_pane(), pane_b1);
|
||||
let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
|
||||
workspace.follow(leader_id, cx).unwrap()
|
||||
workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.activate_next_pane(cx);
|
||||
});
|
||||
// Wait for focus effects to be fully flushed
|
||||
workspace_a.update(cx_a, |workspace, _| {
|
||||
assert_eq!(*workspace.active_pane(), pane_a1);
|
||||
});
|
||||
// Clients A and B return focus to the original files they had open
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Both clients see the other client's focused file in their right pane.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(true, "1.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(true, "2.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Clients A and B each open a new file.
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.activate_next_pane(cx);
|
||||
});
|
||||
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(*workspace.active_pane(), pane_b1);
|
||||
workspace.open_path((worktree_id, "4.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.foreground().run_until_parked();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Ensure leader updates don't change the active pane of followers
|
||||
workspace_a.read_with(cx_a, |workspace, _| {
|
||||
assert_eq!(*workspace.active_pane(), pane_a1);
|
||||
});
|
||||
workspace_b.read_with(cx_b, |workspace, _| {
|
||||
assert_eq!(*workspace.active_pane(), pane_b1);
|
||||
});
|
||||
|
||||
// Ensure peers following each other doesn't cause an infinite loop.
|
||||
// Both client's see the other client open the new file, but keep their
|
||||
// focus on their own active pane.
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.project_path(cx)),
|
||||
Some((worktree_id, "3.txt").into())
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(true, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "3.txt").into())
|
||||
);
|
||||
workspace.activate_next_pane(cx);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![
|
||||
(false, "2.txt".into()),
|
||||
(false, "1.txt".into()),
|
||||
(true, "3.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Client A focuses their right pane, in which they're following client B.
|
||||
workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Client B sees that client A is now looking at the same file as them.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(true, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![
|
||||
(false, "2.txt".into()),
|
||||
(false, "1.txt".into()),
|
||||
(false, "3.txt".into()),
|
||||
(true, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Client B focuses their right pane, in which they're following client A,
|
||||
// who is following them.
|
||||
workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Client A sees that client B is now looking at the same file as them.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![
|
||||
(false, "2.txt".into()),
|
||||
(false, "1.txt".into()),
|
||||
(false, "3.txt".into()),
|
||||
(true, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(true, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Client B focuses a file that they previously followed A to, breaking
|
||||
// the follow.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Both clients see that client B is looking at that previous file.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![
|
||||
(false, "2.txt".into()),
|
||||
(false, "1.txt".into()),
|
||||
(true, "3.txt".into()),
|
||||
(false, "4.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(true, "3.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Client B closes tabs, some of which were originally opened by client A,
|
||||
// and some of which were originally opened by client B.
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.close_inactive_items(&Default::default(), cx)
|
||||
.unwrap()
|
||||
.detach();
|
||||
});
|
||||
});
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Both clients see that Client B is looking at the previous tab.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![(true, "3.txt".into()),]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_b.peer_id(),
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(true, "3.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Client B follows client A again.
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A cycles through some tabs.
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Client B follows client A into those tabs.
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(true, "4.txt".into()),
|
||||
(false, "3.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "4.txt").into())
|
||||
);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "4.txt").into())
|
||||
);
|
||||
workspace.activate_next_pane(cx);
|
||||
});
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![
|
||||
(false, "1.txt".into()),
|
||||
(true, "2.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(false, "3.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![
|
||||
(false, "3.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(true, "2.txt".into())
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_item(cx).unwrap().project_path(cx),
|
||||
Some((worktree_id, "3.txt").into())
|
||||
);
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.activate_prev_item(true, cx);
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_a, cx_a),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: None,
|
||||
items: vec![
|
||||
(true, "1.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(false, "3.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
pane_summaries(&workspace_b, cx_b),
|
||||
&[
|
||||
PaneSummary {
|
||||
active: false,
|
||||
leader: None,
|
||||
items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
|
||||
},
|
||||
PaneSummary {
|
||||
active: true,
|
||||
leader: client_a.peer_id(),
|
||||
items: vec![
|
||||
(false, "3.txt".into()),
|
||||
(false, "4.txt".into()),
|
||||
(false, "2.txt".into()),
|
||||
(true, "1.txt".into()),
|
||||
]
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -1074,24 +1429,6 @@ async fn test_peers_simultaneously_following_each_other(
|
||||
});
|
||||
}
|
||||
|
||||
fn visible_push_notifications(
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
||||
let mut ret = Vec::new();
|
||||
for window in cx.windows() {
|
||||
window.read_with(cx, |window| {
|
||||
if let Some(handle) = window
|
||||
.root_view()
|
||||
.clone()
|
||||
.downcast::<ProjectSharedNotification>()
|
||||
{
|
||||
ret.push(handle)
|
||||
}
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_following_across_workspaces(
|
||||
deterministic: Arc<Deterministic>,
|
||||
@@ -1304,3 +1641,59 @@ async fn test_following_across_workspaces(
|
||||
assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
|
||||
});
|
||||
}
|
||||
|
||||
fn visible_push_notifications(
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
||||
let mut ret = Vec::new();
|
||||
for window in cx.windows() {
|
||||
window.read_with(cx, |window| {
|
||||
if let Some(handle) = window
|
||||
.root_view()
|
||||
.clone()
|
||||
.downcast::<ProjectSharedNotification>()
|
||||
{
|
||||
ret.push(handle)
|
||||
}
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct PaneSummary {
|
||||
active: bool,
|
||||
leader: Option<PeerId>,
|
||||
items: Vec<(bool, String)>,
|
||||
}
|
||||
|
||||
fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
|
||||
workspace.read_with(cx, |workspace, cx| {
|
||||
let active_pane = workspace.active_pane();
|
||||
workspace
|
||||
.panes()
|
||||
.iter()
|
||||
.map(|pane| {
|
||||
let leader = workspace.leader_for_pane(pane);
|
||||
let active = pane == active_pane;
|
||||
let pane = pane.read(cx);
|
||||
let active_ix = pane.active_item_index();
|
||||
PaneSummary {
|
||||
active,
|
||||
leader,
|
||||
items: pane
|
||||
.items()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| {
|
||||
(
|
||||
ix == active_ix,
|
||||
item.tab_description(0, cx)
|
||||
.map_or(String::new(), |s| s.to_string()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
rich_text = { path = "../rich_text" }
|
||||
picker = { path = "../picker" }
|
||||
project = { path = "../project" }
|
||||
recent_projects = {path = "../recent_projects"}
|
||||
|
||||
@@ -3,6 +3,7 @@ use anyhow::Result;
|
||||
use call::ActiveCall;
|
||||
use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
|
||||
use client::Client;
|
||||
use collections::HashMap;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::Editor;
|
||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
|
||||
@@ -12,12 +13,13 @@ use gpui::{
|
||||
platform::{CursorStyle, MouseButton},
|
||||
serde_json,
|
||||
views::{ItemType, Select, SelectStyle},
|
||||
AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task,
|
||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
use language::{language_settings::SoftWrap, LanguageRegistry};
|
||||
use menu::Confirm;
|
||||
use project::Fs;
|
||||
use rich_text::RichText;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
@@ -35,6 +37,7 @@ const CHAT_PANEL_KEY: &'static str = "ChatPanel";
|
||||
pub struct ChatPanel {
|
||||
client: Arc<Client>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
|
||||
message_list: ListState<ChatPanel>,
|
||||
input_editor: ViewHandle<Editor>,
|
||||
@@ -47,6 +50,7 @@ pub struct ChatPanel {
|
||||
subscriptions: Vec<gpui::Subscription>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
has_focus: bool,
|
||||
markdown_data: HashMap<ChannelMessageId, RichText>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -78,6 +82,7 @@ impl ChatPanel {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let client = workspace.app_state().client.clone();
|
||||
let channel_store = workspace.app_state().channel_store.clone();
|
||||
let languages = workspace.app_state().languages.clone();
|
||||
|
||||
let input_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::auto_height(
|
||||
@@ -130,6 +135,8 @@ impl ChatPanel {
|
||||
fs,
|
||||
client,
|
||||
channel_store,
|
||||
languages,
|
||||
|
||||
active_chat: Default::default(),
|
||||
pending_serialization: Task::ready(None),
|
||||
message_list,
|
||||
@@ -141,6 +148,7 @@ impl ChatPanel {
|
||||
workspace: workspace_handle,
|
||||
active: false,
|
||||
width: None,
|
||||
markdown_data: Default::default(),
|
||||
};
|
||||
|
||||
let mut old_dock_position = this.position(cx);
|
||||
@@ -177,6 +185,25 @@ impl ChatPanel {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let markdown = this.languages.language_for_name("Markdown");
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.input_editor.update(cx, |editor, cx| {
|
||||
editor.buffer().update(cx, |multi_buffer, cx| {
|
||||
multi_buffer
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx))
|
||||
})
|
||||
})
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
@@ -327,13 +354,33 @@ impl ChatPanel {
|
||||
messages.flex(1., true).into_any()
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let message = self.active_chat.as_ref().unwrap().0.read(cx).message(ix);
|
||||
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let (message, is_continuation, is_last) = {
|
||||
let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
|
||||
let last_message = active_chat.message(ix.saturating_sub(1));
|
||||
let this_message = active_chat.message(ix);
|
||||
let is_continuation = last_message.id != this_message.id
|
||||
&& this_message.sender.id == last_message.sender.id;
|
||||
|
||||
(
|
||||
active_chat.message(ix).clone(),
|
||||
is_continuation,
|
||||
active_chat.message_count() == ix + 1,
|
||||
)
|
||||
};
|
||||
|
||||
let is_pending = message.is_pending();
|
||||
let text = self
|
||||
.markdown_data
|
||||
.entry(message.id)
|
||||
.or_insert_with(|| rich_text::render_markdown(message.body, &self.languages, None));
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let theme = theme::current(cx);
|
||||
let style = if message.is_pending() {
|
||||
let style = if is_pending {
|
||||
&theme.chat_panel.pending_message
|
||||
} else if is_continuation {
|
||||
&theme.chat_panel.continuation_message
|
||||
} else {
|
||||
&theme.chat_panel.message
|
||||
};
|
||||
@@ -346,52 +393,90 @@ impl ChatPanel {
|
||||
None
|
||||
};
|
||||
|
||||
enum DeleteMessage {}
|
||||
|
||||
let body = message.body.clone();
|
||||
Flex::column()
|
||||
.with_child(
|
||||
enum MessageBackgroundHighlight {}
|
||||
MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
|
||||
let container = style.container.style_for(state);
|
||||
if is_continuation {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
message.sender.github_login.clone(),
|
||||
style.sender.text.clone(),
|
||||
text.element(
|
||||
theme.editor.syntax.clone(),
|
||||
style.body.clone(),
|
||||
theme.editor.document_highlight_read_background,
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.sender.container),
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(render_remove(message_id_to_remove, cx, &theme))
|
||||
.contained()
|
||||
.with_style(*container)
|
||||
.with_margin_bottom(if is_last {
|
||||
theme.chat_panel.last_message_bottom_spacing
|
||||
} else {
|
||||
0.
|
||||
})
|
||||
.into_any()
|
||||
} else {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(render_avatar(
|
||||
message.sender.avatar.clone(),
|
||||
&theme,
|
||||
))
|
||||
.with_child(
|
||||
Label::new(
|
||||
message.sender.github_login.clone(),
|
||||
style.sender.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.sender.container),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
format_timestamp(
|
||||
message.timestamp,
|
||||
now,
|
||||
self.local_timezone,
|
||||
),
|
||||
style.timestamp.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.timestamp.container),
|
||||
)
|
||||
.align_children_center()
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(render_remove(message_id_to_remove, cx, &theme))
|
||||
.align_children_center(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
format_timestamp(message.timestamp, now, self.local_timezone),
|
||||
style.timestamp.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.timestamp.container),
|
||||
Flex::row()
|
||||
.with_child(
|
||||
text.element(
|
||||
theme.editor.syntax.clone(),
|
||||
style.body.clone(),
|
||||
theme.editor.document_highlight_read_background,
|
||||
cx,
|
||||
)
|
||||
.flex(1., true),
|
||||
)
|
||||
// Add a spacer to make everything line up
|
||||
.with_child(render_remove(None, cx, &theme)),
|
||||
)
|
||||
.with_children(message_id_to_remove.map(|id| {
|
||||
MouseEventHandler::new::<DeleteMessage, _>(
|
||||
id as usize,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
let button_style =
|
||||
theme.chat_panel.icon_button.style_for(mouse_state);
|
||||
render_icon_button(button_style, "icons/x.svg")
|
||||
.aligned()
|
||||
.into_any()
|
||||
},
|
||||
)
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.remove_message(id, cx);
|
||||
})
|
||||
.flex_float()
|
||||
})),
|
||||
)
|
||||
.with_child(Text::new(body, style.body.clone()))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.into_any()
|
||||
.contained()
|
||||
.with_style(*container)
|
||||
.with_margin_bottom(if is_last {
|
||||
theme.chat_panel.last_message_bottom_spacing
|
||||
} else {
|
||||
0.
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
|
||||
@@ -565,6 +650,7 @@ impl ChatPanel {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let chat = open_chat.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.markdown_data = Default::default();
|
||||
this.set_active_chat(chat, cx);
|
||||
})
|
||||
})
|
||||
@@ -589,6 +675,72 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_avatar(avatar: Option<Arc<ImageData>>, theme: &Arc<Theme>) -> AnyElement<ChatPanel> {
|
||||
let avatar_style = theme.chat_panel.avatar;
|
||||
|
||||
avatar
|
||||
.map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(avatar_style.image)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_corner_radius(avatar_style.outer_corner_radius)
|
||||
.constrained()
|
||||
.with_width(avatar_style.outer_width)
|
||||
.with_height(avatar_style.outer_width)
|
||||
.into_any()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(avatar_style.outer_width)
|
||||
.into_any()
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.chat_panel.avatar_container)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_remove(
|
||||
message_id_to_remove: Option<u64>,
|
||||
cx: &mut ViewContext<'_, '_, ChatPanel>,
|
||||
theme: &Arc<Theme>,
|
||||
) -> AnyElement<ChatPanel> {
|
||||
enum DeleteMessage {}
|
||||
|
||||
message_id_to_remove
|
||||
.map(|id| {
|
||||
MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
|
||||
let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
|
||||
render_icon_button(button_style, "icons/x.svg")
|
||||
.aligned()
|
||||
.into_any()
|
||||
})
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.remove_message(id, cx);
|
||||
})
|
||||
.flex_float()
|
||||
.into_any()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let style = theme.chat_panel.icon_button.default;
|
||||
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_uniform_padding(2.)
|
||||
.flex_float()
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
impl Entity for ChatPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
@@ -1937,6 +1937,8 @@ impl CollabPanel {
|
||||
is_dragged_over = true;
|
||||
}
|
||||
|
||||
let has_messages_notification = channel.unseen_message_id.is_some();
|
||||
|
||||
MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
|
||||
let row_hovered = state.hovered();
|
||||
|
||||
@@ -1974,11 +1976,7 @@ impl CollabPanel {
|
||||
.left()
|
||||
.with_tooltip::<ChannelTooltip>(
|
||||
ix,
|
||||
if is_active {
|
||||
"Open channel notes"
|
||||
} else {
|
||||
"Join channel"
|
||||
},
|
||||
"Join channel",
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
@@ -2022,24 +2020,33 @@ impl CollabPanel {
|
||||
.flex(1., true)
|
||||
})
|
||||
.with_child(
|
||||
MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |_, _| {
|
||||
MouseEventHandler::new::<ChannelNote, _>(ix, cx, move |mouse_state, _| {
|
||||
let container_style = collab_theme
|
||||
.disclosure
|
||||
.button
|
||||
.style_for(mouse_state)
|
||||
.container;
|
||||
|
||||
if channel.unseen_message_id.is_some() {
|
||||
Svg::new("icons/conversations.svg")
|
||||
.with_color(collab_theme.channel_note_active_color)
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.with_uniform_padding(4.)
|
||||
.into_any()
|
||||
} else if row_hovered {
|
||||
Svg::new("icons/conversations.svg")
|
||||
.with_color(collab_theme.channel_hash.color)
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.with_uniform_padding(4.)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.into_any()
|
||||
Empty::new().into_any()
|
||||
}
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
@@ -2056,7 +2063,12 @@ impl CollabPanel {
|
||||
.with_margin_right(4.),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |_, cx| {
|
||||
MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |mouse_state, cx| {
|
||||
let container_style = collab_theme
|
||||
.disclosure
|
||||
.button
|
||||
.style_for(mouse_state)
|
||||
.container;
|
||||
if row_hovered || channel.unseen_note_version.is_some() {
|
||||
Svg::new("icons/file.svg")
|
||||
.with_color(if channel.unseen_note_version.is_some() {
|
||||
@@ -2067,6 +2079,8 @@ impl CollabPanel {
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.with_uniform_padding(4.)
|
||||
.with_margin_right(collab_theme.channel_hash.container.margin.left)
|
||||
.with_tooltip::<NotesTooltip>(
|
||||
ix as usize,
|
||||
@@ -2076,23 +2090,20 @@ impl CollabPanel {
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
} else if has_messages_notification {
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(collab_theme.channel_hash.width)
|
||||
.contained()
|
||||
.with_uniform_padding(4.)
|
||||
.with_margin_right(collab_theme.channel_hash.container.margin.left)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty::new().into_any()
|
||||
}
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
let participants =
|
||||
this.channel_store.read(cx).channel_participants(channel_id);
|
||||
if is_active || participants.is_empty() {
|
||||
this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
|
||||
} else {
|
||||
this.join_channel(channel_id, cx);
|
||||
};
|
||||
this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
|
||||
}),
|
||||
)
|
||||
.align_children_center()
|
||||
|
||||
@@ -36,6 +36,7 @@ language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
project = { path = "../project" }
|
||||
rpc = { path = "../rpc" }
|
||||
rich_text = { path = "../rich_text" }
|
||||
settings = { path = "../settings" }
|
||||
snippet = { path = "../snippet" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
|
||||
@@ -8,12 +8,12 @@ use futures::FutureExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
|
||||
fonts::{HighlightStyle, Underline, Weight},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
|
||||
AnyElement, AppContext, Element, ModelHandle, Task, ViewContext,
|
||||
};
|
||||
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
|
||||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
|
||||
use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use util::TryFutureExt;
|
||||
|
||||
@@ -346,158 +346,25 @@ fn show_hover(
|
||||
}
|
||||
|
||||
fn render_blocks(
|
||||
theme_id: usize,
|
||||
blocks: &[HoverBlock],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<&Arc<Language>>,
|
||||
style: &EditorStyle,
|
||||
) -> RenderedInfo {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut region_ranges = Vec::new();
|
||||
let mut regions = Vec::new();
|
||||
) -> RichText {
|
||||
let mut data = RichText {
|
||||
text: Default::default(),
|
||||
highlights: Default::default(),
|
||||
region_ranges: Default::default(),
|
||||
regions: Default::default(),
|
||||
};
|
||||
|
||||
for block in blocks {
|
||||
match &block.kind {
|
||||
HoverBlockKind::PlainText => {
|
||||
new_paragraph(&mut text, &mut Vec::new());
|
||||
text.push_str(&block.text);
|
||||
new_paragraph(&mut data.text, &mut Vec::new());
|
||||
data.text.push_str(&block.text);
|
||||
}
|
||||
HoverBlockKind::Markdown => {
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
|
||||
|
||||
let mut bold_depth = 0;
|
||||
let mut italic_depth = 0;
|
||||
let mut link_url = None;
|
||||
let mut current_language = None;
|
||||
let mut list_stack = Vec::new();
|
||||
|
||||
for event in Parser::new_ext(&block.text, Options::all()) {
|
||||
let prev_len = text.len();
|
||||
match event {
|
||||
Event::Text(t) => {
|
||||
if let Some(language) = ¤t_language {
|
||||
render_code(
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
t.as_ref(),
|
||||
language,
|
||||
style,
|
||||
);
|
||||
} else {
|
||||
text.push_str(t.as_ref());
|
||||
|
||||
let mut style = HighlightStyle::default();
|
||||
if bold_depth > 0 {
|
||||
style.weight = Some(Weight::BOLD);
|
||||
}
|
||||
if italic_depth > 0 {
|
||||
style.italic = Some(true);
|
||||
}
|
||||
if let Some(link_url) = link_url.clone() {
|
||||
region_ranges.push(prev_len..text.len());
|
||||
regions.push(RenderedRegion {
|
||||
link_url: Some(link_url),
|
||||
code: false,
|
||||
});
|
||||
style.underline = Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if style != HighlightStyle::default() {
|
||||
let mut new_highlight = true;
|
||||
if let Some((last_range, last_style)) = highlights.last_mut() {
|
||||
if last_range.end == prev_len && last_style == &style {
|
||||
last_range.end = text.len();
|
||||
new_highlight = false;
|
||||
}
|
||||
}
|
||||
if new_highlight {
|
||||
highlights.push((prev_len..text.len(), style));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Code(t) => {
|
||||
text.push_str(t.as_ref());
|
||||
region_ranges.push(prev_len..text.len());
|
||||
if link_url.is_some() {
|
||||
highlights.push((
|
||||
prev_len..text.len(),
|
||||
HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
regions.push(RenderedRegion {
|
||||
code: true,
|
||||
link_url: link_url.clone(),
|
||||
});
|
||||
}
|
||||
Event::Start(tag) => match tag {
|
||||
Tag::Paragraph => new_paragraph(&mut text, &mut list_stack),
|
||||
Tag::Heading(_, _, _) => {
|
||||
new_paragraph(&mut text, &mut list_stack);
|
||||
bold_depth += 1;
|
||||
}
|
||||
Tag::CodeBlock(kind) => {
|
||||
new_paragraph(&mut text, &mut list_stack);
|
||||
current_language = if let CodeBlockKind::Fenced(language) = kind {
|
||||
language_registry
|
||||
.language_for_name(language.as_ref())
|
||||
.now_or_never()
|
||||
.and_then(Result::ok)
|
||||
} else {
|
||||
language.cloned()
|
||||
}
|
||||
}
|
||||
Tag::Emphasis => italic_depth += 1,
|
||||
Tag::Strong => bold_depth += 1,
|
||||
Tag::Link(_, url, _) => link_url = Some(url.to_string()),
|
||||
Tag::List(number) => {
|
||||
list_stack.push((number, false));
|
||||
}
|
||||
Tag::Item => {
|
||||
let len = list_stack.len();
|
||||
if let Some((list_number, has_content)) = list_stack.last_mut() {
|
||||
*has_content = false;
|
||||
if !text.is_empty() && !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
for _ in 0..len - 1 {
|
||||
text.push_str(" ");
|
||||
}
|
||||
if let Some(number) = list_number {
|
||||
text.push_str(&format!("{}. ", number));
|
||||
*number += 1;
|
||||
*has_content = false;
|
||||
} else {
|
||||
text.push_str("- ");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::End(tag) => match tag {
|
||||
Tag::Heading(_, _, _) => bold_depth -= 1,
|
||||
Tag::CodeBlock(_) => current_language = None,
|
||||
Tag::Emphasis => italic_depth -= 1,
|
||||
Tag::Strong => bold_depth -= 1,
|
||||
Tag::Link(_, _, _) => link_url = None,
|
||||
Tag::List(_) => drop(list_stack.pop()),
|
||||
_ => {}
|
||||
},
|
||||
Event::HardBreak => text.push('\n'),
|
||||
Event::SoftBreak => text.push(' '),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
render_markdown_mut(&block.text, language_registry, language, &mut data)
|
||||
}
|
||||
HoverBlockKind::Code { language } => {
|
||||
if let Some(language) = language_registry
|
||||
@@ -505,62 +372,17 @@ fn render_blocks(
|
||||
.now_or_never()
|
||||
.and_then(Result::ok)
|
||||
{
|
||||
render_code(&mut text, &mut highlights, &block.text, &language, style);
|
||||
render_code(&mut data.text, &mut data.highlights, &block.text, &language);
|
||||
} else {
|
||||
text.push_str(&block.text);
|
||||
data.text.push_str(&block.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderedInfo {
|
||||
theme_id,
|
||||
text: text.trim().to_string(),
|
||||
highlights,
|
||||
region_ranges,
|
||||
regions,
|
||||
}
|
||||
}
|
||||
data.text = data.text.trim().to_string();
|
||||
|
||||
fn render_code(
|
||||
text: &mut String,
|
||||
highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
|
||||
content: &str,
|
||||
language: &Arc<Language>,
|
||||
style: &EditorStyle,
|
||||
) {
|
||||
let prev_len = text.len();
|
||||
text.push_str(content);
|
||||
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
|
||||
if let Some(style) = highlight_id.style(&style.syntax) {
|
||||
highlights.push((prev_len + range.start..prev_len + range.end, style));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
|
||||
let mut is_subsequent_paragraph_of_list = false;
|
||||
if let Some((_, has_content)) = list_stack.last_mut() {
|
||||
if *has_content {
|
||||
is_subsequent_paragraph_of_list = true;
|
||||
} else {
|
||||
*has_content = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
text.push('\n');
|
||||
}
|
||||
for _ in 0..list_stack.len().saturating_sub(1) {
|
||||
text.push_str(" ");
|
||||
}
|
||||
if is_subsequent_paragraph_of_list {
|
||||
text.push_str(" ");
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -623,22 +445,7 @@ pub struct InfoPopover {
|
||||
symbol_range: RangeInEditor,
|
||||
pub blocks: Vec<HoverBlock>,
|
||||
language: Option<Arc<Language>>,
|
||||
rendered_content: Option<RenderedInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RenderedInfo {
|
||||
theme_id: usize,
|
||||
text: String,
|
||||
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
region_ranges: Vec<Range<usize>>,
|
||||
regions: Vec<RenderedRegion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RenderedRegion {
|
||||
code: bool,
|
||||
link_url: Option<String>,
|
||||
rendered_content: Option<RichText>,
|
||||
}
|
||||
|
||||
impl InfoPopover {
|
||||
@@ -647,63 +454,24 @@ impl InfoPopover {
|
||||
style: &EditorStyle,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement<Editor> {
|
||||
if let Some(rendered) = &self.rendered_content {
|
||||
if rendered.theme_id != style.theme_id {
|
||||
self.rendered_content = None;
|
||||
}
|
||||
}
|
||||
|
||||
let rendered_content = self.rendered_content.get_or_insert_with(|| {
|
||||
render_blocks(
|
||||
style.theme_id,
|
||||
&self.blocks,
|
||||
self.project.read(cx).languages(),
|
||||
self.language.as_ref(),
|
||||
style,
|
||||
)
|
||||
});
|
||||
|
||||
MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
|
||||
let mut region_id = 0;
|
||||
let view_id = cx.view_id();
|
||||
|
||||
MouseEventHandler::new::<InfoPopover, _>(0, cx, move |_, cx| {
|
||||
let code_span_background_color = style.document_highlight_read_background;
|
||||
let regions = rendered_content.regions.clone();
|
||||
Flex::column()
|
||||
.scrollable::<HoverBlock>(1, None, cx)
|
||||
.with_child(
|
||||
Text::new(rendered_content.text.clone(), style.text.clone())
|
||||
.with_highlights(rendered_content.highlights.clone())
|
||||
.with_custom_runs(
|
||||
rendered_content.region_ranges.clone(),
|
||||
move |ix, bounds, cx| {
|
||||
region_id += 1;
|
||||
let region = regions[ix].clone();
|
||||
if let Some(url) = region.link_url {
|
||||
cx.scene().push_cursor_region(CursorRegion {
|
||||
bounds,
|
||||
style: CursorStyle::PointingHand,
|
||||
});
|
||||
cx.scene().push_mouse_region(
|
||||
MouseRegion::new::<Self>(view_id, region_id, bounds)
|
||||
.on_click::<Editor, _>(
|
||||
MouseButton::Left,
|
||||
move |_, _, cx| cx.platform().open_url(&url),
|
||||
),
|
||||
);
|
||||
}
|
||||
if region.code {
|
||||
cx.scene().push_quad(gpui::Quad {
|
||||
bounds,
|
||||
background: Some(code_span_background_color),
|
||||
border: Default::default(),
|
||||
corner_radii: (2.0).into(),
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_soft_wrap(true),
|
||||
)
|
||||
.with_child(rendered_content.element(
|
||||
style.syntax.clone(),
|
||||
style.text.clone(),
|
||||
code_span_background_color,
|
||||
cx,
|
||||
))
|
||||
.contained()
|
||||
.with_style(style.hover_popover.container)
|
||||
})
|
||||
@@ -799,11 +567,12 @@ mod tests {
|
||||
InlayId,
|
||||
};
|
||||
use collections::BTreeSet;
|
||||
use gpui::fonts::Weight;
|
||||
use gpui::fonts::{HighlightStyle, Underline, Weight};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{HoverBlock, HoverBlockKind};
|
||||
use rich_text::Highlight;
|
||||
use smol::stream::StreamExt;
|
||||
use unindent::Unindent;
|
||||
use util::test::marked_text_ranges;
|
||||
@@ -1014,7 +783,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, cx| {
|
||||
cx.editor(|editor, _| {
|
||||
let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
|
||||
assert_eq!(
|
||||
blocks,
|
||||
@@ -1024,8 +793,7 @@ mod tests {
|
||||
}],
|
||||
);
|
||||
|
||||
let style = editor.style(cx);
|
||||
let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
|
||||
let rendered = render_blocks(&blocks, &Default::default(), None);
|
||||
assert_eq!(
|
||||
rendered.text,
|
||||
code_str.trim(),
|
||||
@@ -1217,7 +985,7 @@ mod tests {
|
||||
expected_styles,
|
||||
} in &rows[0..]
|
||||
{
|
||||
let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
|
||||
let rendered = render_blocks(&blocks, &Default::default(), None);
|
||||
|
||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||
let expected_highlights = ranges
|
||||
@@ -1228,8 +996,21 @@ mod tests {
|
||||
rendered.text, expected_text,
|
||||
"wrong text for input {blocks:?}"
|
||||
);
|
||||
|
||||
let rendered_highlights: Vec<_> = rendered
|
||||
.highlights
|
||||
.iter()
|
||||
.filter_map(|(range, highlight)| {
|
||||
let style = match highlight {
|
||||
Highlight::Id(id) => id.style(&style.syntax)?,
|
||||
Highlight::Highlight(style) => style.clone(),
|
||||
};
|
||||
Some((range.clone(), style))
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
rendered.highlights, expected_highlights,
|
||||
rendered_highlights, expected_highlights,
|
||||
"wrong highlights for input {blocks:?}"
|
||||
);
|
||||
}
|
||||
|
||||
30
crates/rich_text/Cargo.toml
Normal file
30
crates/rich_text/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "rich_text"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/rich_text.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"gpui/test-support",
|
||||
"util/test-support",
|
||||
]
|
||||
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
theme = { path = "../theme" }
|
||||
language = { path = "../language" }
|
||||
util = { path = "../util" }
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
lazy_static.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
287
crates/rich_text/src/rich_text.rs
Normal file
287
crates/rich_text/src/rich_text.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::Text,
|
||||
fonts::{HighlightStyle, TextStyle, Underline, Weight},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, CursorRegion, Element, MouseRegion, ViewContext,
|
||||
};
|
||||
use language::{HighlightId, Language, LanguageRegistry};
|
||||
use theme::SyntaxTheme;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Highlight {
|
||||
Id(HighlightId),
|
||||
Highlight(HighlightStyle),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RichText {
|
||||
pub text: String,
|
||||
pub highlights: Vec<(Range<usize>, Highlight)>,
|
||||
pub region_ranges: Vec<Range<usize>>,
|
||||
pub regions: Vec<RenderedRegion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderedRegion {
|
||||
code: bool,
|
||||
link_url: Option<String>,
|
||||
}
|
||||
|
||||
impl RichText {
|
||||
pub fn element<V: 'static>(
|
||||
&self,
|
||||
syntax: Arc<SyntaxTheme>,
|
||||
style: TextStyle,
|
||||
code_span_background_color: Color,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> AnyElement<V> {
|
||||
let mut region_id = 0;
|
||||
let view_id = cx.view_id();
|
||||
|
||||
let regions = self.regions.clone();
|
||||
|
||||
enum Markdown {}
|
||||
Text::new(self.text.clone(), style.clone())
|
||||
.with_highlights(
|
||||
self.highlights
|
||||
.iter()
|
||||
.filter_map(|(range, highlight)| {
|
||||
let style = match highlight {
|
||||
Highlight::Id(id) => id.style(&syntax)?,
|
||||
Highlight::Highlight(style) => style.clone(),
|
||||
};
|
||||
Some((range.clone(), style))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| {
|
||||
region_id += 1;
|
||||
let region = regions[ix].clone();
|
||||
if let Some(url) = region.link_url {
|
||||
cx.scene().push_cursor_region(CursorRegion {
|
||||
bounds,
|
||||
style: CursorStyle::PointingHand,
|
||||
});
|
||||
cx.scene().push_mouse_region(
|
||||
MouseRegion::new::<Markdown>(view_id, region_id, bounds)
|
||||
.on_click::<V, _>(MouseButton::Left, move |_, _, cx| {
|
||||
cx.platform().open_url(&url)
|
||||
}),
|
||||
);
|
||||
}
|
||||
if region.code {
|
||||
cx.scene().push_quad(gpui::Quad {
|
||||
bounds,
|
||||
background: Some(code_span_background_color),
|
||||
border: Default::default(),
|
||||
corner_radii: (2.0).into(),
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_soft_wrap(true)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_markdown_mut(
|
||||
block: &str,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<&Arc<Language>>,
|
||||
data: &mut RichText,
|
||||
) {
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
|
||||
|
||||
let mut bold_depth = 0;
|
||||
let mut italic_depth = 0;
|
||||
let mut link_url = None;
|
||||
let mut current_language = None;
|
||||
let mut list_stack = Vec::new();
|
||||
|
||||
for event in Parser::new_ext(&block, Options::all()) {
|
||||
let prev_len = data.text.len();
|
||||
match event {
|
||||
Event::Text(t) => {
|
||||
if let Some(language) = ¤t_language {
|
||||
render_code(&mut data.text, &mut data.highlights, t.as_ref(), language);
|
||||
} else {
|
||||
data.text.push_str(t.as_ref());
|
||||
|
||||
let mut style = HighlightStyle::default();
|
||||
if bold_depth > 0 {
|
||||
style.weight = Some(Weight::BOLD);
|
||||
}
|
||||
if italic_depth > 0 {
|
||||
style.italic = Some(true);
|
||||
}
|
||||
if let Some(link_url) = link_url.clone() {
|
||||
data.region_ranges.push(prev_len..data.text.len());
|
||||
data.regions.push(RenderedRegion {
|
||||
link_url: Some(link_url),
|
||||
code: false,
|
||||
});
|
||||
style.underline = Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if style != HighlightStyle::default() {
|
||||
let mut new_highlight = true;
|
||||
if let Some((last_range, last_style)) = data.highlights.last_mut() {
|
||||
if last_range.end == prev_len
|
||||
&& last_style == &Highlight::Highlight(style)
|
||||
{
|
||||
last_range.end = data.text.len();
|
||||
new_highlight = false;
|
||||
}
|
||||
}
|
||||
if new_highlight {
|
||||
data.highlights
|
||||
.push((prev_len..data.text.len(), Highlight::Highlight(style)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Code(t) => {
|
||||
data.text.push_str(t.as_ref());
|
||||
data.region_ranges.push(prev_len..data.text.len());
|
||||
if link_url.is_some() {
|
||||
data.highlights.push((
|
||||
prev_len..data.text.len(),
|
||||
Highlight::Highlight(HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
}
|
||||
data.regions.push(RenderedRegion {
|
||||
code: true,
|
||||
link_url: link_url.clone(),
|
||||
});
|
||||
}
|
||||
Event::Start(tag) => match tag {
|
||||
Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack),
|
||||
Tag::Heading(_, _, _) => {
|
||||
new_paragraph(&mut data.text, &mut list_stack);
|
||||
bold_depth += 1;
|
||||
}
|
||||
Tag::CodeBlock(kind) => {
|
||||
new_paragraph(&mut data.text, &mut list_stack);
|
||||
current_language = if let CodeBlockKind::Fenced(language) = kind {
|
||||
language_registry
|
||||
.language_for_name(language.as_ref())
|
||||
.now_or_never()
|
||||
.and_then(Result::ok)
|
||||
} else {
|
||||
language.cloned()
|
||||
}
|
||||
}
|
||||
Tag::Emphasis => italic_depth += 1,
|
||||
Tag::Strong => bold_depth += 1,
|
||||
Tag::Link(_, url, _) => link_url = Some(url.to_string()),
|
||||
Tag::List(number) => {
|
||||
list_stack.push((number, false));
|
||||
}
|
||||
Tag::Item => {
|
||||
let len = list_stack.len();
|
||||
if let Some((list_number, has_content)) = list_stack.last_mut() {
|
||||
*has_content = false;
|
||||
if !data.text.is_empty() && !data.text.ends_with('\n') {
|
||||
data.text.push('\n');
|
||||
}
|
||||
for _ in 0..len - 1 {
|
||||
data.text.push_str(" ");
|
||||
}
|
||||
if let Some(number) = list_number {
|
||||
data.text.push_str(&format!("{}. ", number));
|
||||
*number += 1;
|
||||
*has_content = false;
|
||||
} else {
|
||||
data.text.push_str("- ");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::End(tag) => match tag {
|
||||
Tag::Heading(_, _, _) => bold_depth -= 1,
|
||||
Tag::CodeBlock(_) => current_language = None,
|
||||
Tag::Emphasis => italic_depth -= 1,
|
||||
Tag::Strong => bold_depth -= 1,
|
||||
Tag::Link(_, _, _) => link_url = None,
|
||||
Tag::List(_) => drop(list_stack.pop()),
|
||||
_ => {}
|
||||
},
|
||||
Event::HardBreak => data.text.push('\n'),
|
||||
Event::SoftBreak => data.text.push(' '),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_markdown(
|
||||
block: String,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<&Arc<Language>>,
|
||||
) -> RichText {
|
||||
let mut data = RichText {
|
||||
text: Default::default(),
|
||||
highlights: Default::default(),
|
||||
region_ranges: Default::default(),
|
||||
regions: Default::default(),
|
||||
};
|
||||
|
||||
render_markdown_mut(&block, language_registry, language, &mut data);
|
||||
|
||||
data.text = data.text.trim().to_string();
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
pub fn render_code(
|
||||
text: &mut String,
|
||||
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
||||
content: &str,
|
||||
language: &Arc<Language>,
|
||||
) {
|
||||
let prev_len = text.len();
|
||||
text.push_str(content);
|
||||
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
|
||||
highlights.push((
|
||||
prev_len + range.start..prev_len + range.end,
|
||||
Highlight::Id(highlight_id),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
|
||||
let mut is_subsequent_paragraph_of_list = false;
|
||||
if let Some((_, has_content)) = list_stack.last_mut() {
|
||||
if *has_content {
|
||||
is_subsequent_paragraph_of_list = true;
|
||||
} else {
|
||||
*has_content = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
text.push('\n');
|
||||
}
|
||||
for _ in 0..list_stack.len().saturating_sub(1) {
|
||||
text.push_str(" ");
|
||||
}
|
||||
if is_subsequent_paragraph_of_list {
|
||||
text.push_str(" ");
|
||||
}
|
||||
}
|
||||
@@ -634,7 +634,11 @@ pub struct ChatPanel {
|
||||
pub list: ContainerStyle,
|
||||
pub channel_select: ChannelSelect,
|
||||
pub input_editor: FieldEditor,
|
||||
pub avatar: AvatarStyle,
|
||||
pub avatar_container: ContainerStyle,
|
||||
pub message: ChatMessage,
|
||||
pub continuation_message: ChatMessage,
|
||||
pub last_message_bottom_spacing: f32,
|
||||
pub pending_message: ChatMessage,
|
||||
pub sign_in_prompt: Interactive<TextStyle>,
|
||||
pub icon_button: Interactive<IconButton>,
|
||||
@@ -643,7 +647,7 @@ pub struct ChatPanel {
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ChatMessage {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub container: Interactive<ContainerStyle>,
|
||||
pub body: TextStyle,
|
||||
pub sender: ContainedText,
|
||||
pub timestamp: ContainedText,
|
||||
|
||||
@@ -78,10 +78,14 @@ fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
|
||||
2 => format!("{:b}", result),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if selection.is_empty() {
|
||||
new_anchors.push((false, snapshot.anchor_after(range.end)))
|
||||
}
|
||||
edits.push((range, replace));
|
||||
edits.push((range.clone(), replace));
|
||||
}
|
||||
if selection.is_empty() {
|
||||
new_anchors.push((false, snapshot.anchor_after(range.end)))
|
||||
}
|
||||
} else {
|
||||
if selection.is_empty() {
|
||||
new_anchors.push((true, snapshot.anchor_after(start)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +230,8 @@ mod test {
|
||||
cx.assert_matches_neovim("(ˇ0b10f)", ["ctrl-a"], "(0b1ˇ1f)")
|
||||
.await;
|
||||
cx.assert_matches_neovim("ˇ-1", ["ctrl-a"], "ˇ0").await;
|
||||
cx.assert_matches_neovim("banˇana", ["ctrl-a"], "banˇana")
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -13,3 +13,6 @@
|
||||
{"Put":{"state":"ˇ-1"}}
|
||||
{"Key":"ctrl-a"}
|
||||
{"Get":{"state":"ˇ0","mode":"Normal"}}
|
||||
{"Put":{"state":"banˇana"}}
|
||||
{"Key":"ctrl-a"}
|
||||
{"Get":{"state":"banˇana","mode":"Normal"}}
|
||||
|
||||
@@ -79,7 +79,7 @@ use status_bar::StatusBar;
|
||||
pub use status_bar::StatusItemView;
|
||||
use theme::{Theme, ThemeSettings};
|
||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||
use util::{async_iife, ResultExt};
|
||||
use util::ResultExt;
|
||||
pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
|
||||
|
||||
lazy_static! {
|
||||
@@ -573,6 +573,7 @@ pub struct Workspace {
|
||||
panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
|
||||
active_pane: ViewHandle<Pane>,
|
||||
last_active_center_pane: Option<WeakViewHandle<Pane>>,
|
||||
last_active_view_id: Option<proto::ViewId>,
|
||||
status_bar: ViewHandle<StatusBar>,
|
||||
titlebar_item: Option<AnyViewHandle>,
|
||||
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
|
||||
@@ -786,6 +787,7 @@ impl Workspace {
|
||||
panes_by_item: Default::default(),
|
||||
active_pane: center_pane.clone(),
|
||||
last_active_center_pane: Some(center_pane.downgrade()),
|
||||
last_active_view_id: None,
|
||||
status_bar,
|
||||
titlebar_item: None,
|
||||
notifications: Default::default(),
|
||||
@@ -934,7 +936,8 @@ impl Workspace {
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
(workspace, opened_items)
|
||||
})
|
||||
@@ -2862,6 +2865,7 @@ impl Workspace {
|
||||
|
||||
cx.notify();
|
||||
|
||||
self.last_active_view_id = active_view_id.clone();
|
||||
proto::FollowResponse {
|
||||
active_view_id,
|
||||
views: self
|
||||
@@ -3028,7 +3032,7 @@ impl Workspace {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_active_view_for_followers(&self, cx: &AppContext) {
|
||||
fn update_active_view_for_followers(&mut self, cx: &AppContext) {
|
||||
let mut is_project_item = true;
|
||||
let mut update = proto::UpdateActiveView::default();
|
||||
if self.active_pane.read(cx).has_focus() {
|
||||
@@ -3046,11 +3050,14 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
self.update_followers(
|
||||
is_project_item,
|
||||
proto::update_followers::Variant::UpdateActiveView(update),
|
||||
cx,
|
||||
);
|
||||
if update.id != self.last_active_view_id {
|
||||
self.last_active_view_id = update.id.clone();
|
||||
self.update_followers(
|
||||
is_project_item,
|
||||
proto::update_followers::Variant::UpdateActiveView(update),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_followers(
|
||||
@@ -3394,140 +3401,124 @@ impl Workspace {
|
||||
serialized_workspace: SerializedWorkspace,
|
||||
paths_to_open: Vec<Option<ProjectPath>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
|
||||
) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let result = async_iife! {{
|
||||
let (project, old_center_pane) =
|
||||
workspace.read_with(&cx, |workspace, _| {
|
||||
(
|
||||
workspace.project().clone(),
|
||||
workspace.last_active_center_pane.clone(),
|
||||
)
|
||||
})?;
|
||||
let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
|
||||
(
|
||||
workspace.project().clone(),
|
||||
workspace.last_active_center_pane.clone(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut center_items = None;
|
||||
let mut center_group = None;
|
||||
// Traverse the splits tree and add to things
|
||||
if let Some((group, active_pane, items)) = serialized_workspace
|
||||
.center_group
|
||||
.deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
|
||||
.await {
|
||||
center_items = Some(items);
|
||||
center_group = Some((group, active_pane))
|
||||
let mut center_group = None;
|
||||
let mut center_items = None;
|
||||
// Traverse the splits tree and add to things
|
||||
if let Some((group, active_pane, items)) = serialized_workspace
|
||||
.center_group
|
||||
.deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
|
||||
.await
|
||||
{
|
||||
center_items = Some(items);
|
||||
center_group = Some((group, active_pane))
|
||||
}
|
||||
|
||||
let mut items_by_project_path = cx.read(|cx| {
|
||||
center_items
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|item| {
|
||||
let item = item?;
|
||||
let project_path = item.project_path(cx)?;
|
||||
Some((project_path, item))
|
||||
})
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
|
||||
let opened_items = paths_to_open
|
||||
.into_iter()
|
||||
.map(|path_to_open| {
|
||||
path_to_open
|
||||
.and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Remove old panes from workspace panes list
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
if let Some((center_group, active_pane)) = center_group {
|
||||
workspace.remove_panes(workspace.center.root.clone(), cx);
|
||||
|
||||
// Swap workspace center group
|
||||
workspace.center = PaneGroup::with_root(center_group);
|
||||
|
||||
// Change the focus to the workspace first so that we retrigger focus in on the pane.
|
||||
cx.focus_self();
|
||||
|
||||
if let Some(active_pane) = active_pane {
|
||||
cx.focus(&active_pane);
|
||||
} else {
|
||||
cx.focus(workspace.panes.last().unwrap());
|
||||
}
|
||||
} else {
|
||||
let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
|
||||
if let Some(old_center_handle) = old_center_handle {
|
||||
cx.focus(&old_center_handle)
|
||||
} else {
|
||||
cx.focus_self()
|
||||
}
|
||||
}
|
||||
|
||||
let resulting_list = cx.read(|cx| {
|
||||
let mut opened_items = center_items
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|item| {
|
||||
let item = item?;
|
||||
let project_path = item.project_path(cx)?;
|
||||
Some((project_path, item))
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
paths_to_open
|
||||
.into_iter()
|
||||
.map(|path_to_open| {
|
||||
path_to_open.map(|path_to_open| {
|
||||
Ok(opened_items.remove(&path_to_open))
|
||||
})
|
||||
.transpose()
|
||||
.map(|item| item.flatten())
|
||||
.transpose()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
// Remove old panes from workspace panes list
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
if let Some((center_group, active_pane)) = center_group {
|
||||
workspace.remove_panes(workspace.center.root.clone(), cx);
|
||||
|
||||
// Swap workspace center group
|
||||
workspace.center = PaneGroup::with_root(center_group);
|
||||
|
||||
// Change the focus to the workspace first so that we retrigger focus in on the pane.
|
||||
cx.focus_self();
|
||||
|
||||
if let Some(active_pane) = active_pane {
|
||||
cx.focus(&active_pane);
|
||||
} else {
|
||||
cx.focus(workspace.panes.last().unwrap());
|
||||
let docks = serialized_workspace.docks;
|
||||
workspace.left_dock.update(cx, |dock, cx| {
|
||||
dock.set_open(docks.left.visible, cx);
|
||||
if let Some(active_panel) = docks.left.active_panel {
|
||||
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
||||
dock.activate_panel(ix, cx);
|
||||
}
|
||||
} else {
|
||||
let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
|
||||
if let Some(old_center_handle) = old_center_handle {
|
||||
cx.focus(&old_center_handle)
|
||||
} else {
|
||||
cx.focus_self()
|
||||
}
|
||||
dock.active_panel()
|
||||
.map(|panel| panel.set_zoomed(docks.left.zoom, cx));
|
||||
if docks.left.visible && docks.left.zoom {
|
||||
cx.focus_self()
|
||||
}
|
||||
});
|
||||
// TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
|
||||
workspace.right_dock.update(cx, |dock, cx| {
|
||||
dock.set_open(docks.right.visible, cx);
|
||||
if let Some(active_panel) = docks.right.active_panel {
|
||||
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
||||
dock.activate_panel(ix, cx);
|
||||
}
|
||||
}
|
||||
dock.active_panel()
|
||||
.map(|panel| panel.set_zoomed(docks.right.zoom, cx));
|
||||
|
||||
if docks.right.visible && docks.right.zoom {
|
||||
cx.focus_self()
|
||||
}
|
||||
});
|
||||
workspace.bottom_dock.update(cx, |dock, cx| {
|
||||
dock.set_open(docks.bottom.visible, cx);
|
||||
if let Some(active_panel) = docks.bottom.active_panel {
|
||||
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
||||
dock.activate_panel(ix, cx);
|
||||
}
|
||||
}
|
||||
|
||||
let docks = serialized_workspace.docks;
|
||||
workspace.left_dock.update(cx, |dock, cx| {
|
||||
dock.set_open(docks.left.visible, cx);
|
||||
if let Some(active_panel) = docks.left.active_panel {
|
||||
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
||||
dock.activate_panel(ix, cx);
|
||||
}
|
||||
}
|
||||
dock.active_panel()
|
||||
.map(|panel| {
|
||||
panel.set_zoomed(docks.left.zoom, cx)
|
||||
});
|
||||
if docks.left.visible && docks.left.zoom {
|
||||
cx.focus_self()
|
||||
}
|
||||
});
|
||||
// TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
|
||||
workspace.right_dock.update(cx, |dock, cx| {
|
||||
dock.set_open(docks.right.visible, cx);
|
||||
if let Some(active_panel) = docks.right.active_panel {
|
||||
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
||||
dock.activate_panel(ix, cx);
|
||||
dock.active_panel()
|
||||
.map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
|
||||
|
||||
}
|
||||
}
|
||||
dock.active_panel()
|
||||
.map(|panel| {
|
||||
panel.set_zoomed(docks.right.zoom, cx)
|
||||
});
|
||||
if docks.bottom.visible && docks.bottom.zoom {
|
||||
cx.focus_self()
|
||||
}
|
||||
});
|
||||
|
||||
if docks.right.visible && docks.right.zoom {
|
||||
cx.focus_self()
|
||||
}
|
||||
});
|
||||
workspace.bottom_dock.update(cx, |dock, cx| {
|
||||
dock.set_open(docks.bottom.visible, cx);
|
||||
if let Some(active_panel) = docks.bottom.active_panel {
|
||||
if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
|
||||
dock.activate_panel(ix, cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
dock.active_panel()
|
||||
.map(|panel| {
|
||||
panel.set_zoomed(docks.bottom.zoom, cx)
|
||||
});
|
||||
// Serialize ourself to make sure our timestamps and any pane / item changes are replicated
|
||||
workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
|
||||
|
||||
if docks.bottom.visible && docks.bottom.zoom {
|
||||
cx.focus_self()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
// Serialize ourself to make sure our timestamps and any pane / item changes are replicated
|
||||
workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
|
||||
|
||||
Ok::<_, anyhow::Error>(resulting_list)
|
||||
}};
|
||||
|
||||
result.await.unwrap_or_default()
|
||||
Ok(opened_items)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3601,7 +3592,7 @@ async fn open_items(
|
||||
mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
|
||||
app_state: Arc<AppState>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
|
||||
) -> Result<Vec<Option<Result<Box<dyn ItemHandle>>>>> {
|
||||
let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
|
||||
|
||||
if let Some(serialized_workspace) = serialized_workspace {
|
||||
@@ -3619,16 +3610,19 @@ async fn open_items(
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
let restored_project_paths = cx.read(|cx| {
|
||||
restored_items
|
||||
.iter()
|
||||
.filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
|
||||
.filter_map(|item| item.as_ref()?.project_path(cx))
|
||||
.collect::<HashSet<_>>()
|
||||
});
|
||||
|
||||
opened_items = restored_items;
|
||||
for restored_item in restored_items {
|
||||
opened_items.push(restored_item.map(Ok));
|
||||
}
|
||||
|
||||
project_paths_to_open
|
||||
.iter_mut()
|
||||
.for_each(|(_, project_path)| {
|
||||
@@ -3681,7 +3675,7 @@ async fn open_items(
|
||||
}
|
||||
}
|
||||
|
||||
opened_items
|
||||
Ok(opened_items)
|
||||
}
|
||||
|
||||
fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.107.0"
|
||||
version = "0.107.2"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from "./components"
|
||||
import { icon_button } from "../component/icon_button"
|
||||
import { useTheme } from "../theme"
|
||||
import { interactive } from "../element"
|
||||
|
||||
export default function chat_panel(): any {
|
||||
const theme = useTheme()
|
||||
@@ -27,11 +28,23 @@ export default function chat_panel(): any {
|
||||
|
||||
return {
|
||||
background: background(layer),
|
||||
list: {
|
||||
margin: {
|
||||
left: SPACING,
|
||||
right: SPACING,
|
||||
avatar: {
|
||||
icon_width: 24,
|
||||
icon_height: 24,
|
||||
corner_radius: 4,
|
||||
outer_width: 24,
|
||||
outer_corner_radius: 16,
|
||||
},
|
||||
avatar_container: {
|
||||
padding: {
|
||||
right: 6,
|
||||
left: 2,
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
}
|
||||
},
|
||||
list: {
|
||||
|
||||
},
|
||||
channel_select: {
|
||||
header: {
|
||||
@@ -79,6 +92,22 @@ export default function chat_panel(): any {
|
||||
},
|
||||
},
|
||||
message: {
|
||||
...interactive({
|
||||
base: {
|
||||
margin: { top: SPACING },
|
||||
padding: {
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
left: SPACING / 2,
|
||||
right: SPACING / 3,
|
||||
}
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
body: text(layer, "sans", "base"),
|
||||
sender: {
|
||||
margin: {
|
||||
@@ -87,7 +116,32 @@ export default function chat_panel(): any {
|
||||
...text(layer, "sans", "base", { weight: "bold" }),
|
||||
},
|
||||
timestamp: text(layer, "sans", "base", "disabled"),
|
||||
margin: { bottom: SPACING }
|
||||
},
|
||||
last_message_bottom_spacing: SPACING,
|
||||
continuation_message: {
|
||||
body: text(layer, "sans", "base"),
|
||||
sender: {
|
||||
margin: {
|
||||
right: 8,
|
||||
},
|
||||
...text(layer, "sans", "base", { weight: "bold" }),
|
||||
},
|
||||
timestamp: text(layer, "sans", "base", "disabled"),
|
||||
...interactive({
|
||||
base: {
|
||||
padding: {
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
left: SPACING / 2,
|
||||
right: SPACING / 3,
|
||||
}
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
pending_message: {
|
||||
body: text(layer, "sans", "base"),
|
||||
@@ -98,6 +152,21 @@ export default function chat_panel(): any {
|
||||
...text(layer, "sans", "base", "disabled"),
|
||||
},
|
||||
timestamp: text(layer, "sans", "base"),
|
||||
...interactive({
|
||||
base: {
|
||||
padding: {
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
left: SPACING / 2,
|
||||
right: SPACING / 3,
|
||||
}
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
sign_in_prompt: {
|
||||
default: text(layer, "sans", "base"),
|
||||
|
||||
@@ -21,6 +21,7 @@ export default function contacts_panel(): any {
|
||||
...text(theme.lowest, "sans", "base"),
|
||||
button: icon_button({ variant: "ghost" }),
|
||||
spacing: 4,
|
||||
padding: 4,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user