Merge branch 'main' into add-link-to-community-repo-in-feedback-editor
4
.github/workflows/ci.yml
vendored
@ -19,7 +19,9 @@ env:
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on: self-hosted
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
|
7
Cargo.lock
generated
@ -794,7 +794,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "bromberg_sl2"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=dac565a90e8f9245f48ff46225c915dc50f76920#dac565a90e8f9245f48ff46225c915dc50f76920"
|
||||
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=950bc5482c216c395049ae33ae4501e08975f17f#950bc5482c216c395049ae33ae4501e08975f17f"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"lazy_static",
|
||||
@ -1188,7 +1188,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.5.4"
|
||||
version = "0.6.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-tungstenite",
|
||||
@ -1257,6 +1257,7 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"context_menu",
|
||||
"editor",
|
||||
"futures 0.3.25",
|
||||
"fuzzy",
|
||||
@ -8356,7 +8357,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.75.0"
|
||||
version = "0.76.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
3
assets/icons/ellipsis_14.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="4" viewBox="0 0 14 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 2C3.125 2.62132 2.62132 3.125 2 3.125C1.37868 3.125 0.875 2.62132 0.875 2C0.875 1.37868 1.37868 0.875 2 0.875C2.62132 0.875 3.125 1.37868 3.125 2ZM8.125 2C8.125 2.62132 7.62132 3.125 7 3.125C6.37868 3.125 5.875 2.62132 5.875 2C5.875 1.37868 6.37868 0.875 7 0.875C7.62132 0.875 8.125 1.37868 8.125 2ZM12 3.125C12.6213 3.125 13.125 2.62132 13.125 2C13.125 1.37868 12.6213 0.875 12 0.875C11.3787 0.875 10.875 1.37868 10.875 2C10.875 2.62132 11.3787 3.125 12 3.125Z" fill="#ABB2BF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 637 B |
3
assets/icons/leave_12.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.585786 0.335786 0.25 0.75 0.25H7.25C7.66421 0.25 8 0.585786 8 1C8 1.41421 7.66421 1.75 7.25 1.75H1.5V10.25H7.25C7.66421 10.25 8 10.5858 8 11C8 11.4142 7.66421 11.75 7.25 11.75H0.75C0.335786 11.75 0 11.4142 0 11V1ZM8.78148 2.91435C9.10493 2.65559 9.57689 2.70803 9.83565 3.03148L11.8357 5.53148C12.0548 5.80539 12.0548 6.19461 11.8357 6.46852L9.83565 8.96852C9.57689 9.29197 9.10493 9.34441 8.78148 9.08565C8.45803 8.82689 8.40559 8.35493 8.66435 8.03148L9.68953 6.75H3.75C3.33579 6.75 3 6.41421 3 6C3 5.58579 3.33579 5.25 3.75 5.25H9.68953L8.66435 3.96852C8.40559 3.64507 8.45803 3.17311 8.78148 2.91435Z" fill="#ABB2BF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 784 B |
@ -228,6 +228,7 @@
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
@ -418,7 +419,7 @@
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
|
||||
"cmd-shift-c": "collab::ToggleCollaborationMenu",
|
||||
"cmd-shift-c": "collab::ToggleContactsMenu",
|
||||
"cmd-alt-i": "zed::DebugElements"
|
||||
}
|
||||
},
|
||||
@ -456,7 +457,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"context": "Pane && docked",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::HideDock",
|
||||
"cmd-escape": "dock::RemoveTabFromDock"
|
||||
|
@ -27,6 +27,7 @@
|
||||
"h": "vim::Left",
|
||||
"backspace": "vim::Backspace",
|
||||
"j": "vim::Down",
|
||||
"enter": "vim::NextLineStart",
|
||||
"k": "vim::Up",
|
||||
"l": "vim::Right",
|
||||
"$": "vim::EndOfLine",
|
||||
|
@ -83,7 +83,7 @@
|
||||
"hard_tabs": false,
|
||||
// How many columns a tab should occupy.
|
||||
"tab_size": 4,
|
||||
// Control what info Zed sends to our servers
|
||||
// Control what info is collected by Zed.
|
||||
"telemetry": {
|
||||
// Send debug info like crash reports.
|
||||
"diagnostics": true,
|
||||
|
@ -33,6 +33,19 @@ struct LspStatus {
|
||||
status: LanguageServerBinaryStatus,
|
||||
}
|
||||
|
||||
struct PendingWork<'a> {
|
||||
language_server_name: &'a str,
|
||||
progress_token: &'a str,
|
||||
progress: &'a LanguageServerProgress,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Content {
|
||||
icon: Option<&'static str>,
|
||||
message: String,
|
||||
action: Option<Box<dyn Action>>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ActivityIndicator::show_error_message);
|
||||
cx.add_action(ActivityIndicator::dismiss_error_message);
|
||||
@ -69,6 +82,8 @@ impl ActivityIndicator {
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
cx.observe_active_labeled_tasks(|_, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
statuses: Default::default(),
|
||||
@ -130,7 +145,7 @@ impl ActivityIndicator {
|
||||
fn pending_language_server_work<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> impl Iterator<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
|
||||
) -> impl Iterator<Item = PendingWork<'a>> {
|
||||
self.project
|
||||
.read(cx)
|
||||
.language_server_statuses()
|
||||
@ -142,23 +157,29 @@ impl ActivityIndicator {
|
||||
let mut pending_work = status
|
||||
.pending_work
|
||||
.iter()
|
||||
.map(|(token, progress)| (status.name.as_str(), token.as_str(), progress))
|
||||
.map(|(token, progress)| PendingWork {
|
||||
language_server_name: status.name.as_str(),
|
||||
progress_token: token.as_str(),
|
||||
progress,
|
||||
})
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at));
|
||||
pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
|
||||
Some(pending_work)
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn content_to_render(
|
||||
&mut self,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> (Option<&'static str>, String, Option<Box<dyn Action>>) {
|
||||
fn content_to_render(&mut self, cx: &mut RenderContext<Self>) -> Content {
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
|
||||
let mut message = lang_server_name.to_string();
|
||||
if let Some(PendingWork {
|
||||
language_server_name,
|
||||
progress_token,
|
||||
progress,
|
||||
}) = pending_work.next()
|
||||
{
|
||||
let mut message = language_server_name.to_string();
|
||||
|
||||
message.push_str(": ");
|
||||
if let Some(progress_message) = progress.message.as_ref() {
|
||||
@ -176,7 +197,11 @@ impl ActivityIndicator {
|
||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||
}
|
||||
|
||||
return (None, message, None);
|
||||
return Content {
|
||||
icon: None,
|
||||
message,
|
||||
action: None,
|
||||
};
|
||||
}
|
||||
|
||||
// Show any language server installation info.
|
||||
@ -199,19 +224,19 @@ impl ActivityIndicator {
|
||||
}
|
||||
|
||||
if !downloading.is_empty() {
|
||||
return (
|
||||
Some(DOWNLOAD_ICON),
|
||||
format!(
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Downloading {} language server{}...",
|
||||
downloading.join(", "),
|
||||
if downloading.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
None,
|
||||
);
|
||||
action: None,
|
||||
};
|
||||
} else if !checking_for_update.is_empty() {
|
||||
return (
|
||||
Some(DOWNLOAD_ICON),
|
||||
format!(
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Checking for updates to {} language server{}...",
|
||||
checking_for_update.join(", "),
|
||||
if checking_for_update.len() > 1 {
|
||||
@ -220,53 +245,61 @@ impl ActivityIndicator {
|
||||
""
|
||||
}
|
||||
),
|
||||
None,
|
||||
);
|
||||
action: None,
|
||||
};
|
||||
} else if !failed.is_empty() {
|
||||
return (
|
||||
Some(WARNING_ICON),
|
||||
format!(
|
||||
return Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: format!(
|
||||
"Failed to download {} language server{}. Click to show error.",
|
||||
failed.join(", "),
|
||||
if failed.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
Some(Box::new(ShowErrorMessage)),
|
||||
);
|
||||
action: Some(Box::new(ShowErrorMessage)),
|
||||
};
|
||||
}
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
"Checking for Zed updates…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Downloading => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
"Downloading Zed update…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Installing => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
"Installing Zed update…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Updated => (
|
||||
None,
|
||||
"Click to restart and update Zed".to_string(),
|
||||
Some(Box::new(workspace::Restart)),
|
||||
),
|
||||
AutoUpdateStatus::Errored => (
|
||||
Some(WARNING_ICON),
|
||||
"Auto update failed".to_string(),
|
||||
Some(Box::new(DismissErrorMessage)),
|
||||
),
|
||||
return match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
action: None,
|
||||
},
|
||||
AutoUpdateStatus::Downloading => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
action: None,
|
||||
},
|
||||
AutoUpdateStatus::Installing => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
action: None,
|
||||
},
|
||||
AutoUpdateStatus::Updated => Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
action: Some(Box::new(workspace::Restart)),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: "Auto update failed".to_string(),
|
||||
action: Some(Box::new(DismissErrorMessage)),
|
||||
},
|
||||
AutoUpdateStatus::Idle => Default::default(),
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
|
||||
return Content {
|
||||
icon: None,
|
||||
message: most_recent_active_task.to_string(),
|
||||
action: None,
|
||||
};
|
||||
}
|
||||
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,7 +313,11 @@ impl View for ActivityIndicator {
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let (icon, message, action) = self.content_to_render(cx);
|
||||
let Content {
|
||||
icon,
|
||||
message,
|
||||
action,
|
||||
} = self.content_to_render(cx);
|
||||
|
||||
let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||
let theme = &cx
|
||||
|
@ -284,6 +284,18 @@ impl ActiveCall {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_project(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_location(
|
||||
&mut self,
|
||||
project: Option<&ModelHandle<Project>>,
|
||||
|
@ -55,6 +55,7 @@ pub struct Room {
|
||||
leave_when_empty: bool,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
|
||||
subscriptions: Vec<client::Subscription>,
|
||||
pending_room_update: Option<Task<()>>,
|
||||
maintain_connection: Option<Task<Option<()>>>,
|
||||
@ -148,6 +149,7 @@ impl Room {
|
||||
pending_room_update: None,
|
||||
client,
|
||||
user_store,
|
||||
follows_by_leader_id_project_id: Default::default(),
|
||||
maintain_connection: Some(maintain_connection),
|
||||
}
|
||||
}
|
||||
@ -275,14 +277,12 @@ impl Room {
|
||||
) -> Result<()> {
|
||||
let mut client_status = client.status();
|
||||
loop {
|
||||
let is_connected = client_status
|
||||
.next()
|
||||
.await
|
||||
.map_or(false, |s| s.is_connected());
|
||||
|
||||
let _ = client_status.try_recv();
|
||||
let is_connected = client_status.borrow().is_connected();
|
||||
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
|
||||
if !is_connected || client_status.next().await.is_some() {
|
||||
log::info!("detected client disconnection");
|
||||
|
||||
this.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
@ -296,12 +296,7 @@ impl Room {
|
||||
let client_reconnection = async {
|
||||
let mut remaining_attempts = 3;
|
||||
while remaining_attempts > 0 {
|
||||
log::info!(
|
||||
"waiting for client status change, remaining attempts {}",
|
||||
remaining_attempts
|
||||
);
|
||||
let Some(status) = client_status.next().await else { break };
|
||||
if status.is_connected() {
|
||||
if client_status.borrow().is_connected() {
|
||||
log::info!("client reconnected, attempting to rejoin room");
|
||||
|
||||
let Some(this) = this.upgrade(&cx) else { break };
|
||||
@ -315,7 +310,15 @@ impl Room {
|
||||
} else {
|
||||
remaining_attempts -= 1;
|
||||
}
|
||||
} else if client_status.borrow().is_signed_out() {
|
||||
return false;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"waiting for client status change, remaining attempts {}",
|
||||
remaining_attempts
|
||||
);
|
||||
client_status.next().await;
|
||||
}
|
||||
false
|
||||
}
|
||||
@ -337,18 +340,20 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
// The client failed to re-establish a connection to the server
|
||||
// or an error occurred while trying to re-join the room. Either way
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
|
||||
}
|
||||
return Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The client failed to re-establish a connection to the server
|
||||
// or an error occurred while trying to re-join the room. Either way
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
|
||||
}
|
||||
Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
))
|
||||
}
|
||||
|
||||
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
@ -457,6 +462,12 @@ impl Room {
|
||||
self.participant_user_ids.contains(&user_id)
|
||||
}
|
||||
|
||||
pub fn followers_for(&self, leader_id: PeerId, project_id: u64) -> &[PeerId] {
|
||||
self.follows_by_leader_id_project_id
|
||||
.get(&(leader_id, project_id))
|
||||
.map_or(&[], |v| v.as_slice())
|
||||
}
|
||||
|
||||
async fn handle_room_updated(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::RoomUpdated>,
|
||||
@ -487,11 +498,13 @@ impl Room {
|
||||
.iter()
|
||||
.map(|p| p.user_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let remote_participant_user_ids = room
|
||||
.participants
|
||||
.iter()
|
||||
.map(|p| p.user_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (remote_participants, pending_participants) =
|
||||
self.user_store.update(cx, move |user_store, cx| {
|
||||
(
|
||||
@ -499,6 +512,7 @@ impl Room {
|
||||
user_store.get_users(pending_participant_user_ids, cx),
|
||||
)
|
||||
});
|
||||
|
||||
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
|
||||
let (remote_participants, pending_participants) =
|
||||
futures::join!(remote_participants, pending_participants);
|
||||
@ -620,6 +634,27 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
this.follows_by_leader_id_project_id.clear();
|
||||
for follower in room.followers {
|
||||
let project_id = follower.project_id;
|
||||
let (leader, follower) = match (follower.leader_id, follower.follower_id) {
|
||||
(Some(leader), Some(follower)) => (leader, follower),
|
||||
|
||||
_ => {
|
||||
log::error!("Follower message {follower:?} missing some state");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let list = this
|
||||
.follows_by_leader_id_project_id
|
||||
.entry((leader, project_id))
|
||||
.or_insert(Vec::new());
|
||||
if !list.contains(&follower) {
|
||||
list.push(follower);
|
||||
}
|
||||
}
|
||||
|
||||
this.pending_room_update.take();
|
||||
if this.should_leave() {
|
||||
log::info!("room is empty, leaving");
|
||||
@ -793,6 +828,20 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn unshare_project(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
let project_id = match project.read(cx).remote_id() {
|
||||
Some(project_id) => project_id,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
self.client.send(proto::UnshareProject { project_id })?;
|
||||
project.update(cx, |this, cx| this.unshare(cx))
|
||||
}
|
||||
|
||||
pub(crate) fn set_location(
|
||||
&mut self,
|
||||
project: Option<&ModelHandle<Project>>,
|
||||
|
@ -66,7 +66,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
|
||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
actions!(client, [Authenticate]);
|
||||
actions!(client, [Authenticate, SignOut]);
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
cx.add_global_action({
|
||||
@ -79,6 +79,16 @@ pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
cx.add_global_action({
|
||||
let client = client.clone();
|
||||
move |_: &SignOut, cx| {
|
||||
let client = client.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
client.disconnect(&cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
@ -169,6 +179,10 @@ impl Status {
|
||||
pub fn is_connected(&self) -> bool {
|
||||
matches!(self, Self::Connected { .. })
|
||||
}
|
||||
|
||||
pub fn is_signed_out(&self) -> bool {
|
||||
matches!(self, Self::SignedOut | Self::UpgradeRequired)
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientState {
|
||||
@ -1152,11 +1166,9 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
|
||||
let conn_id = self.connection_id()?;
|
||||
self.peer.disconnect(conn_id);
|
||||
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
|
||||
self.peer.teardown();
|
||||
self.set_status(Status::SignedOut, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn connection_id(&self) -> Result<ConnectionId> {
|
||||
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.5.4"
|
||||
version = "0.6.1"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
@ -143,3 +143,17 @@ CREATE TABLE "servers" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"environment" VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "followers" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"leader_connection_id" INTEGER NOT NULL,
|
||||
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"follower_connection_id" INTEGER NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX
|
||||
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
|
||||
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
|
||||
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
||||
|
15
crates/collab/migrations/20230202155735_followers.sql
Normal file
@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS "followers" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"leader_connection_id" INTEGER NOT NULL,
|
||||
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"follower_connection_id" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX
|
||||
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
|
||||
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
|
||||
|
||||
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
@ -1,5 +1,6 @@
|
||||
mod access_token;
|
||||
mod contact;
|
||||
mod follower;
|
||||
mod language_server;
|
||||
mod project;
|
||||
mod project_collaborator;
|
||||
@ -157,7 +158,7 @@ impl Database {
|
||||
room_id: RoomId,
|
||||
new_server_id: ServerId,
|
||||
) -> Result<RoomGuard<RefreshedRoom>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let stale_participant_filter = Condition::all()
|
||||
.add(room_participant::Column::RoomId.eq(room_id))
|
||||
.add(room_participant::Column::AnsweringConnectionId.is_not_null())
|
||||
@ -190,17 +191,18 @@ impl Database {
|
||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
project::Entity::delete_many()
|
||||
.filter(project::Column::RoomId.eq(room_id))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
||||
}
|
||||
|
||||
Ok((
|
||||
room_id,
|
||||
RefreshedRoom {
|
||||
room,
|
||||
stale_participant_user_ids,
|
||||
canceled_calls_to_user_ids,
|
||||
},
|
||||
))
|
||||
Ok(RefreshedRoom {
|
||||
room,
|
||||
stale_participant_user_ids,
|
||||
canceled_calls_to_user_ids,
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -1129,18 +1131,16 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
live_kit_room: &str,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
) -> Result<proto::Room> {
|
||||
self.transaction(|tx| async move {
|
||||
let room = room::ActiveModel {
|
||||
live_kit_room: ActiveValue::set(live_kit_room.into()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
let room_id = room.id;
|
||||
|
||||
room_participant::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
room_id: ActiveValue::set(room.id),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
@ -1157,8 +1157,8 @@ impl Database {
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, room))
|
||||
let room = self.get_room(room.id, &tx).await?;
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -1171,7 +1171,7 @@ impl Database {
|
||||
called_user_id: UserId,
|
||||
initial_project_id: Option<ProjectId>,
|
||||
) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
room_participant::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
user_id: ActiveValue::set(called_user_id),
|
||||
@ -1190,7 +1190,7 @@ impl Database {
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
let incoming_call = Self::build_incoming_call(&room, called_user_id)
|
||||
.ok_or_else(|| anyhow!("failed to build incoming call"))?;
|
||||
Ok((room_id, (room, incoming_call)))
|
||||
Ok((room, incoming_call))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -1200,7 +1200,7 @@ impl Database {
|
||||
room_id: RoomId,
|
||||
called_user_id: UserId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
room_participant::Entity::delete_many()
|
||||
.filter(
|
||||
room_participant::Column::RoomId
|
||||
@ -1210,7 +1210,7 @@ impl Database {
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, room))
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -1257,7 +1257,7 @@ impl Database {
|
||||
calling_connection: ConnectionId,
|
||||
called_user_id: UserId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@ -1276,14 +1276,13 @@ impl Database {
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no call to cancel"))?;
|
||||
let room_id = participant.room_id;
|
||||
|
||||
room_participant::Entity::delete(participant.into_active_model())
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, room))
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -1294,7 +1293,7 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let result = room_participant::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@ -1316,7 +1315,7 @@ impl Database {
|
||||
Err(anyhow!("room does not exist or was already joined"))?
|
||||
} else {
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, room))
|
||||
Ok(room)
|
||||
}
|
||||
})
|
||||
.await
|
||||
@ -1328,9 +1327,9 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<RejoinedRoom>> {
|
||||
self.room_transaction(|tx| async {
|
||||
let room_id = RoomId::from_proto(rejoin_room.id);
|
||||
self.room_transaction(room_id, |tx| async {
|
||||
let tx = tx;
|
||||
let room_id = RoomId::from_proto(rejoin_room.id);
|
||||
let participant_update = room_participant::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@ -1549,14 +1548,11 @@ impl Database {
|
||||
}
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((
|
||||
room_id,
|
||||
RejoinedRoom {
|
||||
room,
|
||||
rejoined_projects,
|
||||
reshared_projects,
|
||||
},
|
||||
))
|
||||
Ok(RejoinedRoom {
|
||||
room,
|
||||
rejoined_projects,
|
||||
reshared_projects,
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -1717,13 +1713,78 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn follow(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
leader_connection: ConnectionId,
|
||||
follower_connection: ConnectionId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
follower::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
project_id: ActiveValue::set(project_id),
|
||||
leader_connection_server_id: ActiveValue::set(ServerId(
|
||||
leader_connection.owner_id as i32,
|
||||
)),
|
||||
leader_connection_id: ActiveValue::set(leader_connection.id as i32),
|
||||
follower_connection_server_id: ActiveValue::set(ServerId(
|
||||
follower_connection.owner_id as i32,
|
||||
)),
|
||||
follower_connection_id: ActiveValue::set(follower_connection.id as i32),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &*tx).await?;
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn unfollow(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
leader_connection: ConnectionId,
|
||||
follower_connection: ConnectionId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
follower::Entity::delete_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(follower::Column::ProjectId.eq(project_id))
|
||||
.add(
|
||||
follower::Column::LeaderConnectionServerId
|
||||
.eq(leader_connection.owner_id)
|
||||
.and(follower::Column::LeaderConnectionId.eq(leader_connection.id)),
|
||||
)
|
||||
.add(
|
||||
follower::Column::FollowerConnectionServerId
|
||||
.eq(follower_connection.owner_id)
|
||||
.and(
|
||||
follower::Column::FollowerConnectionId
|
||||
.eq(follower_connection.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &*tx).await?;
|
||||
Ok(room)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_room_participant_location(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
connection: ConnectionId,
|
||||
location: proto::ParticipantLocation,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
self.room_transaction(|tx| async {
|
||||
self.room_transaction(room_id, |tx| async {
|
||||
let tx = tx;
|
||||
let location_kind;
|
||||
let location_project_id;
|
||||
@ -1769,7 +1830,7 @@ impl Database {
|
||||
|
||||
if result.rows_affected == 1 {
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, room))
|
||||
Ok(room)
|
||||
} else {
|
||||
Err(anyhow!("could not update room participant location"))?
|
||||
}
|
||||
@ -1926,12 +1987,25 @@ impl Database {
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(db_projects);
|
||||
|
||||
let mut db_followers = db_room.find_related(follower::Entity).stream(tx).await?;
|
||||
let mut followers = Vec::new();
|
||||
while let Some(db_follower) = db_followers.next().await {
|
||||
let db_follower = db_follower?;
|
||||
followers.push(proto::Follower {
|
||||
leader_id: Some(db_follower.leader_connection().into()),
|
||||
follower_id: Some(db_follower.follower_connection().into()),
|
||||
project_id: db_follower.project_id.to_proto(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(proto::Room {
|
||||
id: db_room.id.to_proto(),
|
||||
live_kit_room: db_room.live_kit_room,
|
||||
participants: participants.into_values().collect(),
|
||||
pending_participants,
|
||||
followers,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1963,7 +2037,7 @@ impl Database {
|
||||
connection: ConnectionId,
|
||||
worktrees: &[proto::WorktreeMetadata],
|
||||
) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@ -2024,7 +2098,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, (project.id, room)))
|
||||
Ok((project.id, room))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -2034,7 +2108,8 @@ impl Database {
|
||||
project_id: ProjectId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
@ -2042,12 +2117,11 @@ impl Database {
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("project not found"))?;
|
||||
if project.host_connection()? == connection {
|
||||
let room_id = project.room_id;
|
||||
project::Entity::delete(project.into_active_model())
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
let room = self.get_room(room_id, &tx).await?;
|
||||
Ok((room_id, (room, guest_connection_ids)))
|
||||
Ok((room, guest_connection_ids))
|
||||
} else {
|
||||
Err(anyhow!("cannot unshare a project hosted by another user"))?
|
||||
}
|
||||
@ -2061,7 +2135,8 @@ impl Database {
|
||||
connection: ConnectionId,
|
||||
worktrees: &[proto::WorktreeMetadata],
|
||||
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.filter(
|
||||
Condition::all()
|
||||
@ -2079,7 +2154,7 @@ impl Database {
|
||||
|
||||
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
|
||||
let room = self.get_room(project.room_id, &tx).await?;
|
||||
Ok((project.room_id, (room, guest_connection_ids)))
|
||||
Ok((room, guest_connection_ids))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -2124,12 +2199,12 @@ impl Database {
|
||||
update: &proto::UpdateWorktree,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let worktree_id = update.worktree_id as i64;
|
||||
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let worktree_id = update.worktree_id as i64;
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
// Ensure the update comes from the host.
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
let _project = project::Entity::find_by_id(project_id)
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(project::Column::HostConnectionId.eq(connection.id as i32))
|
||||
@ -2140,7 +2215,6 @@ impl Database {
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let room_id = project.room_id;
|
||||
|
||||
// Update metadata.
|
||||
worktree::Entity::update(worktree::ActiveModel {
|
||||
@ -2220,7 +2294,7 @@ impl Database {
|
||||
}
|
||||
|
||||
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
Ok((room_id, connection_ids))
|
||||
Ok(connection_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -2230,9 +2304,10 @@ impl Database {
|
||||
update: &proto::UpdateDiagnosticSummary,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let worktree_id = update.worktree_id as i64;
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let worktree_id = update.worktree_id as i64;
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let summary = update
|
||||
.summary
|
||||
.as_ref()
|
||||
@ -2274,7 +2349,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
Ok((project.room_id, connection_ids))
|
||||
Ok(connection_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -2284,8 +2359,9 @@ impl Database {
|
||||
update: &proto::StartLanguageServer,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let server = update
|
||||
.server
|
||||
.as_ref()
|
||||
@ -2319,7 +2395,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
Ok((project.room_id, connection_ids))
|
||||
Ok(connection_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -2329,7 +2405,8 @@ impl Database {
|
||||
project_id: ProjectId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<(Project, ReplicaId)>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let participant = room_participant::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@ -2455,7 +2532,6 @@ impl Database {
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let room_id = project.room_id;
|
||||
let project = Project {
|
||||
collaborators: collaborators
|
||||
.into_iter()
|
||||
@ -2475,7 +2551,7 @@ impl Database {
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
Ok((room_id, (project, replica_id as ReplicaId)))
|
||||
Ok((project, replica_id as ReplicaId))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -2485,7 +2561,8 @@ impl Database {
|
||||
project_id: ProjectId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<LeftProject>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let result = project_collaborator::Entity::delete_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@ -2521,7 +2598,7 @@ impl Database {
|
||||
host_connection_id: project.host_connection()?,
|
||||
connection_ids,
|
||||
};
|
||||
Ok((project.room_id, left_project))
|
||||
Ok(left_project)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -2531,11 +2608,8 @@ impl Database {
|
||||
project_id: ProjectId,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let collaborators = project_collaborator::Entity::find()
|
||||
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
||||
.all(&*tx)
|
||||
@ -2553,7 +2627,7 @@ impl Database {
|
||||
.iter()
|
||||
.any(|collaborator| collaborator.connection_id == connection_id)
|
||||
{
|
||||
Ok((project.room_id, collaborators))
|
||||
Ok(collaborators)
|
||||
} else {
|
||||
Err(anyhow!("no such project"))?
|
||||
}
|
||||
@ -2566,11 +2640,8 @@ impl Database {
|
||||
project_id: ProjectId,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
|
||||
self.room_transaction(|tx| async move {
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let mut collaborators = project_collaborator::Entity::find()
|
||||
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
||||
.stream(&*tx)
|
||||
@ -2583,7 +2654,7 @@ impl Database {
|
||||
}
|
||||
|
||||
if connection_ids.contains(&connection_id) {
|
||||
Ok((project.room_id, connection_ids))
|
||||
Ok(connection_ids)
|
||||
} else {
|
||||
Err(anyhow!("no such project"))?
|
||||
}
|
||||
@ -2613,6 +2684,17 @@ impl Database {
|
||||
Ok(guest_connection_ids)
|
||||
}
|
||||
|
||||
async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
|
||||
self.transaction(|tx| async move {
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("project {} not found", project_id))?;
|
||||
Ok(project.room_id)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
// access tokens
|
||||
|
||||
pub async fn create_access_token_hash(
|
||||
@ -2763,21 +2845,48 @@ impl Database {
|
||||
self.run(body).await
|
||||
}
|
||||
|
||||
async fn room_transaction<F, Fut, T>(&self, f: F) -> Result<RoomGuard<T>>
|
||||
async fn room_transaction<F, Fut, T>(&self, room_id: RoomId, f: F) -> Result<RoomGuard<T>>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
Fut: Send + Future<Output = Result<(RoomId, T)>>,
|
||||
Fut: Send + Future<Output = Result<T>>,
|
||||
{
|
||||
let data = self
|
||||
.optional_room_transaction(move |tx| {
|
||||
let future = f(tx);
|
||||
async {
|
||||
let data = future.await?;
|
||||
Ok(Some(data))
|
||||
let body = async {
|
||||
loop {
|
||||
let lock = self.rooms.entry(room_id).or_default().clone();
|
||||
let _guard = lock.lock_owned().await;
|
||||
let (tx, result) = self.with_transaction(&f).await?;
|
||||
match result {
|
||||
Ok(data) => {
|
||||
match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => {
|
||||
return Ok(RoomGuard {
|
||||
data,
|
||||
_guard,
|
||||
_not_send: PhantomData,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
if is_serialization_error(&error) {
|
||||
// Retry (don't break the loop)
|
||||
} else {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tx.rollback().await?;
|
||||
if is_serialization_error(&error) {
|
||||
// Retry (don't break the loop)
|
||||
} else {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(data.unwrap())
|
||||
}
|
||||
};
|
||||
|
||||
self.run(body).await
|
||||
}
|
||||
|
||||
async fn with_transaction<F, Fut, T>(&self, f: &F) -> Result<(DatabaseTransaction, Result<T>)>
|
||||
@ -3011,6 +3120,7 @@ macro_rules! id_type {
|
||||
|
||||
id_type!(AccessTokenId);
|
||||
id_type!(ContactId);
|
||||
id_type!(FollowerId);
|
||||
id_type!(RoomId);
|
||||
id_type!(RoomParticipantId);
|
||||
id_type!(ProjectId);
|
||||
|
51
crates/collab/src/db/follower.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use super::{FollowerId, ProjectId, RoomId, ServerId};
|
||||
use rpc::ConnectionId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
|
||||
#[sea_orm(table_name = "followers")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: FollowerId,
|
||||
pub room_id: RoomId,
|
||||
pub project_id: ProjectId,
|
||||
pub leader_connection_server_id: ServerId,
|
||||
pub leader_connection_id: i32,
|
||||
pub follower_connection_server_id: ServerId,
|
||||
pub follower_connection_id: i32,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn leader_connection(&self) -> ConnectionId {
|
||||
ConnectionId {
|
||||
owner_id: self.leader_connection_server_id.0 as u32,
|
||||
id: self.leader_connection_id as u32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn follower_connection(&self) -> ConnectionId {
|
||||
ConnectionId {
|
||||
owner_id: self.follower_connection_server_id.0 as u32,
|
||||
id: self.follower_connection_id as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::room::Entity",
|
||||
from = "Column::RoomId",
|
||||
to = "super::room::Column::Id"
|
||||
)]
|
||||
Room,
|
||||
}
|
||||
|
||||
impl Related<super::room::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Room.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
@ -15,6 +15,8 @@ pub enum Relation {
|
||||
RoomParticipant,
|
||||
#[sea_orm(has_many = "super::project::Entity")]
|
||||
Project,
|
||||
#[sea_orm(has_many = "super::follower::Entity")]
|
||||
Follower,
|
||||
}
|
||||
|
||||
impl Related<super::room_participant::Entity> for Entity {
|
||||
@ -29,4 +31,10 @@ impl Related<super::project::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::follower::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Follower.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
@ -270,8 +270,11 @@ impl Server {
|
||||
let mut live_kit_room = String::new();
|
||||
let mut delete_live_kit_room = false;
|
||||
|
||||
if let Ok(mut refreshed_room) =
|
||||
app_state.db.refresh_room(room_id, server_id).await
|
||||
if let Some(mut refreshed_room) = app_state
|
||||
.db
|
||||
.refresh_room(room_id, server_id)
|
||||
.await
|
||||
.trace_err()
|
||||
{
|
||||
tracing::info!(
|
||||
room_id = room_id.0,
|
||||
@ -1312,6 +1315,7 @@ async fn join_project(
|
||||
.filter(|collaborator| collaborator.connection_id != session.connection_id)
|
||||
.map(|collaborator| collaborator.to_proto())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let worktrees = project
|
||||
.worktrees
|
||||
.iter()
|
||||
@ -1724,6 +1728,7 @@ async fn follow(
|
||||
.ok_or_else(|| anyhow!("invalid leader id"))?
|
||||
.into();
|
||||
let follower_id = session.connection_id;
|
||||
|
||||
{
|
||||
let project_connection_ids = session
|
||||
.db()
|
||||
@ -1744,6 +1749,14 @@ async fn follow(
|
||||
.views
|
||||
.retain(|view| view.leader_id != Some(follower_id.into()));
|
||||
response.send(response_payload)?;
|
||||
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.follow(project_id, leader_id, follower_id)
|
||||
.await?;
|
||||
room_updated(&room, &session.peer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1753,17 +1766,29 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
|
||||
.leader_id
|
||||
.ok_or_else(|| anyhow!("invalid leader id"))?
|
||||
.into();
|
||||
let project_connection_ids = session
|
||||
let follower_id = session.connection_id;
|
||||
|
||||
if !session
|
||||
.db()
|
||||
.await
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
if !project_connection_ids.contains(&leader_id) {
|
||||
.await?
|
||||
.contains(&leader_id)
|
||||
{
|
||||
Err(anyhow!("no such peer"))?;
|
||||
}
|
||||
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, leader_id, request)?;
|
||||
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.unfollow(project_id, leader_id, follower_id)
|
||||
.await?;
|
||||
room_updated(&room, &session.peer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -733,6 +733,14 @@ async fn test_server_restarts(
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree("/a", json!({ "a.txt": "a-contents" }))
|
||||
.await;
|
||||
|
||||
// Invite client B to collaborate on a project
|
||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
let client_d = server.create_client(cx_d, "user_d").await;
|
||||
@ -753,19 +761,19 @@ async fn test_server_restarts(
|
||||
// User A calls users B, C, and D.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_b.user_id().unwrap(), None, cx)
|
||||
call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_c.user_id().unwrap(), None, cx)
|
||||
call.invite(client_c.user_id().unwrap(), Some(project_a.clone()), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_d.user_id().unwrap(), None, cx)
|
||||
call.invite(client_d.user_id().unwrap(), Some(project_a.clone()), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@ -1083,7 +1091,7 @@ async fn test_calls_on_multiple_connections(
|
||||
assert!(incoming_call_b2.next().await.unwrap().is_none());
|
||||
|
||||
// User B disconnects the client that is not on the call. Everything should be fine.
|
||||
client_b1.disconnect(&cx_b1.to_async()).unwrap();
|
||||
client_b1.disconnect(&cx_b1.to_async());
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
||||
client_b1
|
||||
.authenticate_and_connect(false, &cx_b1.to_async())
|
||||
@ -3227,7 +3235,7 @@ async fn test_leaving_project(
|
||||
buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
|
||||
|
||||
// Drop client B's connection and ensure client A and client C observe client B leaving.
|
||||
client_b.disconnect(&cx_b.to_async()).unwrap();
|
||||
client_b.disconnect(&cx_b.to_async());
|
||||
deterministic.advance_clock(RECONNECT_TIMEOUT);
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
@ -5772,7 +5780,7 @@ async fn test_contact_requests(
|
||||
.is_empty());
|
||||
|
||||
async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
|
||||
client.disconnect(&cx.to_async()).unwrap();
|
||||
client.disconnect(&cx.to_async());
|
||||
client.clear_contacts(cx).await;
|
||||
client
|
||||
.authenticate_and_connect(false, &cx.to_async())
|
||||
@ -5786,6 +5794,7 @@ async fn test_following(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
cx_a.update(editor::init);
|
||||
@ -5794,9 +5803,13 @@ async fn test_following(
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
server
|
||||
.make_contacts(&mut [(&client_a, cx_a), (&client_c, cx_c)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
@ -5827,8 +5840,10 @@ async fn test_following(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A opens some editors.
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||
|
||||
// Client A opens some editors.
|
||||
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let editor_a1 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
@ -5848,7 +5863,6 @@ async fn test_following(
|
||||
.unwrap();
|
||||
|
||||
// Client B opens an editor.
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b1 = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
@ -5858,29 +5872,97 @@ async fn test_following(
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let client_a_id = project_b.read_with(cx_b, |project, _| {
|
||||
project.collaborators().values().next().unwrap().peer_id
|
||||
});
|
||||
let client_b_id = project_a.read_with(cx_a, |project, _| {
|
||||
project.collaborators().values().next().unwrap().peer_id
|
||||
});
|
||||
let peer_id_a = client_a.peer_id().unwrap();
|
||||
let peer_id_b = client_b.peer_id().unwrap();
|
||||
let peer_id_c = client_c.peer_id().unwrap();
|
||||
|
||||
// When client B starts following client A, all visible view states are replicated to client B.
|
||||
// Client A updates their selections in those editors
|
||||
editor_a1.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
|
||||
});
|
||||
editor_a2.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
|
||||
});
|
||||
|
||||
// When client B starts following client A, all visible view states are replicated to client B.
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(client_a_id), cx)
|
||||
.toggle_follow(&ToggleFollow(peer_id_a), cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A invites client C to the call.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_c.current_user_id(cx_c).to_proto(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_c.foreground().run_until_parked();
|
||||
let active_call_c = cx_c.read(ActiveCall::global);
|
||||
active_call_c
|
||||
.update(cx_c, |call, cx| call.accept_incoming(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
let workspace_c = client_c.build_workspace(&project_c, cx_c);
|
||||
active_call_c
|
||||
.update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client C also follows client A.
|
||||
workspace_c
|
||||
.update(cx_c, |workspace, cx| {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(peer_id_a), cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// All clients see that clients B and C are following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b, peer_id_c],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Client C unfollows client A.
|
||||
workspace_c.update(cx_c, |workspace, cx| {
|
||||
workspace.toggle_follow(&ToggleFollow(peer_id_a), cx);
|
||||
});
|
||||
|
||||
// All clients see that clients B is following client A.
|
||||
cx_c.foreground().run_until_parked();
|
||||
for (name, active_call, cx) in [
|
||||
("A", &active_call_a, &cx_a),
|
||||
("B", &active_call_b, &cx_b),
|
||||
("C", &active_call_c, &cx_c),
|
||||
] {
|
||||
active_call.read_with(*cx, |call, cx| {
|
||||
let room = call.room().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
room.followers_for(peer_id_a, project_id),
|
||||
&[peer_id_b],
|
||||
"checking followers for A as {name}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
@ -6033,14 +6115,14 @@ async fn test_following(
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(client_b_id), cx)
|
||||
.toggle_follow(&ToggleFollow(peer_id_b), cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
Some(client_b_id)
|
||||
Some(peer_id_b)
|
||||
);
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
@ -6112,7 +6194,7 @@ async fn test_following(
|
||||
);
|
||||
|
||||
// Following interrupts when client B disconnects.
|
||||
client_b.disconnect(&cx_b.to_async()).unwrap();
|
||||
client_b.disconnect(&cx_b.to_async());
|
||||
deterministic.advance_clock(RECONNECT_TIMEOUT);
|
||||
assert_eq!(
|
||||
workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
|
@ -27,6 +27,7 @@ call = { path = "../call" }
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
editor = { path = "../editor" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
|
@ -1,33 +1,60 @@
|
||||
use crate::{contact_notification::ContactNotification, contacts_popover, ToggleScreenSharing};
|
||||
use call::{ActiveCall, ParticipantLocation};
|
||||
use client::{proto::PeerId, Authenticate, ContactEventKind, User, UserStore};
|
||||
use crate::{
|
||||
collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
|
||||
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
|
||||
ToggleScreenSharing,
|
||||
};
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, Authenticate, ContactEventKind, SignOut, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
use contacts_popover::ContactsPopover;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use gpui::{
|
||||
actions,
|
||||
color::Color,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f, PathBuilder},
|
||||
impl_internal_actions,
|
||||
json::{self, ToJson},
|
||||
Border, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
|
||||
CursorStyle, Entity, ImageData, ModelHandle, MouseButton, MutableAppContext, RenderContext,
|
||||
Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::ops::Range;
|
||||
use theme::Theme;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use theme::{AvatarStyle, Theme};
|
||||
use util::ResultExt;
|
||||
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
|
||||
|
||||
actions!(collab, [ToggleCollaborationMenu, ShareProject]);
|
||||
actions!(
|
||||
collab,
|
||||
[
|
||||
ToggleCollaboratorList,
|
||||
ToggleContactsMenu,
|
||||
ToggleUserMenu,
|
||||
ShareProject,
|
||||
UnshareProject,
|
||||
]
|
||||
);
|
||||
|
||||
impl_internal_actions!(collab, [LeaveCall]);
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub(crate) struct LeaveCall;
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
|
||||
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
|
||||
cx.add_action(CollabTitlebarItem::share_project);
|
||||
cx.add_action(CollabTitlebarItem::unshare_project);
|
||||
cx.add_action(CollabTitlebarItem::leave_call);
|
||||
cx.add_action(CollabTitlebarItem::toggle_user_menu);
|
||||
}
|
||||
|
||||
pub struct CollabTitlebarItem {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
||||
user_menu: ViewHandle<ContextMenu>,
|
||||
collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@ -47,27 +74,61 @@ impl View for CollabTitlebarItem {
|
||||
return Empty::new().boxed();
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
let mut project_title = String::new();
|
||||
for (i, name) in project.worktree_root_names(cx).enumerate() {
|
||||
if i > 0 {
|
||||
project_title.push_str(", ");
|
||||
}
|
||||
project_title.push_str(name);
|
||||
}
|
||||
if project_title.is_empty() {
|
||||
project_title = "empty project".to_owned();
|
||||
}
|
||||
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
let mut container = Flex::row();
|
||||
let mut left_container = Flex::row();
|
||||
let mut right_container = Flex::row();
|
||||
|
||||
container.add_children(self.render_toggle_screen_sharing_button(&theme, cx));
|
||||
left_container.add_child(
|
||||
Label::new(project_title, theme.workspace.titlebar.title.clone())
|
||||
.contained()
|
||||
.with_margin_right(theme.workspace.titlebar.item_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed(),
|
||||
);
|
||||
|
||||
if workspace.read(cx).client().status().borrow().is_connected() {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
if project.is_shared()
|
||||
|| project.is_remote()
|
||||
|| ActiveCall::global(cx).read(cx).room().is_none()
|
||||
{
|
||||
container.add_child(self.render_toggle_contacts_button(&theme, cx));
|
||||
} else {
|
||||
container.add_child(self.render_share_button(&theme, cx));
|
||||
}
|
||||
let user = workspace.read(cx).user_store().read(cx).current_user();
|
||||
let peer_id = workspace.read(cx).client().peer_id();
|
||||
if let Some(((user, peer_id), room)) = user
|
||||
.zip(peer_id)
|
||||
.zip(ActiveCall::global(cx).read(cx).room().cloned())
|
||||
{
|
||||
left_container
|
||||
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
|
||||
|
||||
right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
|
||||
right_container
|
||||
.add_child(self.render_current_user(&workspace, &theme, &user, peer_id, cx));
|
||||
right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
|
||||
}
|
||||
container.add_children(self.render_collaborators(&workspace, &theme, cx));
|
||||
container.add_children(self.render_current_user(&workspace, &theme, cx));
|
||||
container.add_children(self.render_connection_status(&workspace, cx));
|
||||
container.boxed()
|
||||
|
||||
let status = workspace.read(cx).client().status();
|
||||
let status = &*status.borrow();
|
||||
if matches!(status, client::Status::Connected { .. }) {
|
||||
right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
|
||||
} else {
|
||||
right_container.add_children(self.render_connection_status(status, cx));
|
||||
}
|
||||
|
||||
right_container.add_child(self.render_user_menu_button(&theme, cx));
|
||||
|
||||
Stack::new()
|
||||
.with_child(left_container.boxed())
|
||||
.with_child(right_container.aligned().right().boxed())
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +141,7 @@ impl CollabTitlebarItem {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let mut subscriptions = Vec::new();
|
||||
subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&active_call, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
|
||||
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
|
||||
this.window_activation_changed(active, cx)
|
||||
}));
|
||||
@ -112,6 +173,12 @@ impl CollabTitlebarItem {
|
||||
workspace: workspace.downgrade(),
|
||||
user_store: user_store.clone(),
|
||||
contacts_popover: None,
|
||||
user_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
collaborator_list_popover: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@ -129,6 +196,13 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if ActiveCall::global(cx).read(cx).room().is_none() {
|
||||
self.contacts_popover = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
@ -139,41 +213,135 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_contacts_popover(
|
||||
fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let project = workspace.read(cx).project().clone();
|
||||
active_call
|
||||
.update(cx, |call, cx| call.unshare_project(project, cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_collaborator_list_popover(
|
||||
&mut self,
|
||||
_: &ToggleCollaborationMenu,
|
||||
_: &ToggleCollaboratorList,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match self.contacts_popover.take() {
|
||||
match self.collaborator_list_popover.take() {
|
||||
Some(_) => {}
|
||||
None => {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let user_store = workspace.read(cx).user_store().clone();
|
||||
let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
|
||||
let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx));
|
||||
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
contacts_popover::Event::Dismissed => {
|
||||
this.contacts_popover = None;
|
||||
collaborator_list_popover::Event::Dismissed => {
|
||||
this.collaborator_list_popover = None;
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
self.contacts_popover = Some(view);
|
||||
|
||||
self.collaborator_list_popover = Some(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
|
||||
if self.contacts_popover.take().is_none() {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let user_store = workspace.read(cx).user_store().clone();
|
||||
let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
contacts_popover::Event::Dismissed => {
|
||||
this.contacts_popover = None;
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
self.contacts_popover = Some(view);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
|
||||
let item_style = theme.context_menu.item.disabled_style().clone();
|
||||
self.user_menu.update(cx, |user_menu, cx| {
|
||||
let items = if let Some(user) = self.user_store.read(cx).current_user() {
|
||||
vec![
|
||||
ContextMenuItem::Static(Box::new(move |_| {
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Self::render_face(
|
||||
avatar,
|
||||
avatar_style.clone(),
|
||||
Color::transparent_black(),
|
||||
)
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), item_style.label.clone())
|
||||
.boxed(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(item_style.container)
|
||||
.boxed()
|
||||
})),
|
||||
ContextMenuItem::Item {
|
||||
label: "Sign out".into(),
|
||||
action: Box::new(SignOut),
|
||||
},
|
||||
]
|
||||
} else {
|
||||
vec![ContextMenuItem::Item {
|
||||
label: "Sign in".into(),
|
||||
action: Box::new(Authenticate),
|
||||
}]
|
||||
};
|
||||
|
||||
user_menu.show(
|
||||
vec2f(
|
||||
theme
|
||||
.workspace
|
||||
.titlebar
|
||||
.user_menu_button
|
||||
.default
|
||||
.button_width,
|
||||
theme.workspace.titlebar.height,
|
||||
),
|
||||
AnchorCorner::TopRight,
|
||||
items,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_toggle_contacts_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
|
||||
let badge = if self
|
||||
.user_store
|
||||
.read(cx)
|
||||
@ -194,9 +362,10 @@ impl CollabTitlebarItem {
|
||||
.boxed(),
|
||||
)
|
||||
};
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ToggleCollaborationMenu>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
|
||||
let style = titlebar
|
||||
.toggle_contacts_button
|
||||
.style_for(state, self.contacts_popover.is_some());
|
||||
@ -214,39 +383,31 @@ impl CollabTitlebarItem {
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleCollaborationMenu);
|
||||
cx.dispatch_action(ToggleContactsMenu);
|
||||
})
|
||||
.with_tooltip::<ToggleContactsMenu, _>(
|
||||
0,
|
||||
"Show contacts menu".into(),
|
||||
Some(Box::new(ToggleContactsMenu)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(badge)
|
||||
.with_children(self.contacts_popover.as_ref().map(|popover| {
|
||||
Overlay::new(
|
||||
ChildView::new(popover, cx)
|
||||
.contained()
|
||||
.with_margin_top(titlebar.height)
|
||||
.with_margin_left(titlebar.toggle_contacts_button.default.button_width)
|
||||
.with_margin_right(-titlebar.toggle_contacts_button.default.button_width)
|
||||
.boxed(),
|
||||
)
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_corner(AnchorCorner::BottomLeft)
|
||||
.with_z_index(999)
|
||||
.boxed()
|
||||
}))
|
||||
.with_children(self.render_contacts_popover_host(titlebar, cx))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_toggle_screen_sharing_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
room: &ModelHandle<Room>,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Option<ElementBox> {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let room = active_call.read(cx).room().cloned()?;
|
||||
) -> ElementBox {
|
||||
let icon;
|
||||
let tooltip;
|
||||
|
||||
if room.read(cx).is_screen_sharing() {
|
||||
icon = "icons/disable_screen_sharing_12.svg";
|
||||
tooltip = "Stop Sharing Screen"
|
||||
@ -256,226 +417,368 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
Some(
|
||||
MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
|
||||
let style = titlebar.call_control.style_for(state, false);
|
||||
Svg::new(icon)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleScreenSharing);
|
||||
})
|
||||
.with_tooltip::<ToggleScreenSharing, _>(
|
||||
0,
|
||||
tooltip.into(),
|
||||
Some(Box::new(ToggleScreenSharing)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_share_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
enum Share {}
|
||||
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
MouseEventHandler::<Share>::new(0, cx, |state, _| {
|
||||
let style = titlebar.share_button.style_for(state, false);
|
||||
Label::new("Share", style.text.clone())
|
||||
MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
|
||||
let style = titlebar.call_control.style_for(state, false);
|
||||
Svg::new(icon)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(ShareProject))
|
||||
.with_tooltip::<Share, _>(
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleScreenSharing);
|
||||
})
|
||||
.with_tooltip::<ToggleScreenSharing, _>(
|
||||
0,
|
||||
"Share project with call participants".into(),
|
||||
None,
|
||||
tooltip.into(),
|
||||
Some(Box::new(ToggleScreenSharing)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.avatar_margin)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_in_call_share_unshare_button(
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
theme: &Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Option<ElementBox> {
|
||||
let project = workspace.read(cx).project();
|
||||
if project.read(cx).is_remote() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let is_shared = project.read(cx).is_shared();
|
||||
let label = if is_shared { "Unshare" } else { "Share" };
|
||||
let tooltip = if is_shared {
|
||||
"Unshare project from call participants"
|
||||
} else {
|
||||
"Share project with call participants"
|
||||
};
|
||||
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
|
||||
enum ShareUnshare {}
|
||||
Some(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ShareUnshare>::new(0, cx, |state, _| {
|
||||
//TODO: Ensure this button has consistant width for both text variations
|
||||
let style = titlebar
|
||||
.share_button
|
||||
.style_for(state, self.contacts_popover.is_some());
|
||||
Label::new(label, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
if is_shared {
|
||||
cx.dispatch_action(UnshareProject);
|
||||
} else {
|
||||
cx.dispatch_action(ShareProject);
|
||||
}
|
||||
})
|
||||
.with_tooltip::<ShareUnshare, _>(
|
||||
0,
|
||||
tooltip.to_owned(),
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing)
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
|
||||
let style = titlebar.call_control.style_for(state, false);
|
||||
Svg::new("icons/ellipsis_14.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleUserMenu);
|
||||
})
|
||||
.with_tooltip::<ToggleUserMenu, _>(
|
||||
0,
|
||||
"Toggle user menu".to_owned(),
|
||||
Some(Box::new(ToggleUserMenu)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(ChildView::new(&self.user_menu, cx).boxed())
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_contacts_popover_host<'a>(
|
||||
&'a self,
|
||||
theme: &'a theme::Titlebar,
|
||||
cx: &'a RenderContext<Self>,
|
||||
) -> Option<ElementBox> {
|
||||
self.contacts_popover.as_ref().map(|popover| {
|
||||
Overlay::new(
|
||||
ChildView::new(popover, cx)
|
||||
.contained()
|
||||
.with_margin_top(theme.height)
|
||||
.with_margin_left(theme.toggle_contacts_button.default.button_width)
|
||||
.with_margin_right(-theme.toggle_contacts_button.default.button_width)
|
||||
.boxed(),
|
||||
)
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_corner(AnchorCorner::BottomLeft)
|
||||
.with_z_index(999)
|
||||
.boxed()
|
||||
})
|
||||
}
|
||||
|
||||
fn render_collaborators(
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
theme: &Theme,
|
||||
room: &ModelHandle<Room>,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Vec<ElementBox> {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
if let Some(room) = active_call.read(cx).room().cloned() {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
let mut participants = room
|
||||
.read(cx)
|
||||
.remote_participants()
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
|
||||
participants
|
||||
.into_iter()
|
||||
.filter_map(|participant| {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
let replica_id = project
|
||||
.collaborators()
|
||||
.get(&participant.peer_id)
|
||||
.map(|collaborator| collaborator.replica_id);
|
||||
let user = participant.user.clone();
|
||||
Some(self.render_avatar(
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
|
||||
let mut participants = room
|
||||
.read(cx)
|
||||
.remote_participants()
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
participants.sort_by_key(|p| Some(project.collaborators().get(&p.peer_id)?.replica_id));
|
||||
|
||||
participants
|
||||
.into_iter()
|
||||
.filter_map(|participant| {
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
let replica_id = project
|
||||
.collaborators()
|
||||
.get(&participant.peer_id)
|
||||
.map(|collaborator| collaborator.replica_id);
|
||||
let user = participant.user.clone();
|
||||
Some(
|
||||
Container::new(self.render_face_pile(
|
||||
&user,
|
||||
replica_id,
|
||||
Some((
|
||||
participant.peer_id,
|
||||
&user.github_login,
|
||||
participant.location,
|
||||
)),
|
||||
participant.peer_id,
|
||||
Some(participant.location),
|
||||
workspace,
|
||||
theme,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
.with_margin_right(theme.workspace.titlebar.face_pile_spacing)
|
||||
.boxed(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_current_user(
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
theme: &Theme,
|
||||
user: &Arc<User>,
|
||||
peer_id: PeerId,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Option<ElementBox> {
|
||||
let user = workspace.read(cx).user_store().read(cx).current_user();
|
||||
) -> ElementBox {
|
||||
let replica_id = workspace.read(cx).project().read(cx).replica_id();
|
||||
let status = *workspace.read(cx).client().status().borrow();
|
||||
if let Some(user) = user {
|
||||
Some(self.render_avatar(&user, Some(replica_id), None, workspace, theme, cx))
|
||||
} else if matches!(status, client::Status::UpgradeRequired) {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.titlebar
|
||||
.sign_in_prompt
|
||||
.style_for(state, false);
|
||||
Label::new("Sign in", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
Container::new(self.render_face_pile(
|
||||
user,
|
||||
Some(replica_id),
|
||||
peer_id,
|
||||
None,
|
||||
workspace,
|
||||
theme,
|
||||
cx,
|
||||
))
|
||||
.with_margin_right(theme.workspace.titlebar.item_spacing)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_avatar(
|
||||
fn render_face_pile(
|
||||
&self,
|
||||
user: &User,
|
||||
replica_id: Option<ReplicaId>,
|
||||
peer: Option<(PeerId, &str, ParticipantLocation)>,
|
||||
peer_id: PeerId,
|
||||
location: Option<ParticipantLocation>,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
theme: &Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let is_followed = peer.map_or(false, |(peer_id, _, _)| {
|
||||
workspace.read(cx).is_following(peer_id)
|
||||
});
|
||||
let project_id = workspace.read(cx).project().read(cx).remote_id();
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
|
||||
let followed_by_self = room
|
||||
.and_then(|room| {
|
||||
Some(
|
||||
is_being_followed
|
||||
&& room
|
||||
.read(cx)
|
||||
.followers_for(peer_id, project_id?)
|
||||
.iter()
|
||||
.any(|&follower| {
|
||||
Some(follower) == workspace.read(cx).client().peer_id()
|
||||
}),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let mut avatar_style;
|
||||
if let Some((_, _, location)) = peer.as_ref() {
|
||||
if let ParticipantLocation::SharedProject { project_id } = *location {
|
||||
if Some(project_id) == workspace.read(cx).project().read(cx).remote_id() {
|
||||
avatar_style = theme.workspace.titlebar.avatar;
|
||||
} else {
|
||||
avatar_style = theme.workspace.titlebar.inactive_avatar;
|
||||
}
|
||||
} else {
|
||||
avatar_style = theme.workspace.titlebar.inactive_avatar;
|
||||
}
|
||||
} else {
|
||||
avatar_style = theme.workspace.titlebar.avatar;
|
||||
}
|
||||
let leader_style = theme.workspace.titlebar.leader_avatar;
|
||||
let follower_style = theme.workspace.titlebar.follower_avatar;
|
||||
|
||||
let mut replica_color = None;
|
||||
let mut background_color = theme
|
||||
.workspace
|
||||
.titlebar
|
||||
.container
|
||||
.background_color
|
||||
.unwrap_or_default();
|
||||
if let Some(replica_id) = replica_id {
|
||||
let color = theme.editor.replica_selection_style(replica_id).cursor;
|
||||
replica_color = Some(color);
|
||||
if is_followed {
|
||||
avatar_style.border = Border::all(1.0, color);
|
||||
if followed_by_self {
|
||||
let selection = theme.editor.replica_selection_style(replica_id).selection;
|
||||
background_color = Color::blend(selection, background_color);
|
||||
background_color.a = 255;
|
||||
}
|
||||
}
|
||||
|
||||
let content = Stack::new()
|
||||
let mut content = Stack::new()
|
||||
.with_children(user.avatar.as_ref().map(|avatar| {
|
||||
Image::new(avatar.clone())
|
||||
.with_style(avatar_style)
|
||||
.constrained()
|
||||
.with_width(theme.workspace.titlebar.avatar_width)
|
||||
.aligned()
|
||||
.boxed()
|
||||
let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
|
||||
.with_child(Self::render_face(
|
||||
avatar.clone(),
|
||||
Self::location_style(workspace, location, leader_style, cx),
|
||||
background_color,
|
||||
))
|
||||
.with_children(
|
||||
(|| {
|
||||
let project_id = project_id?;
|
||||
let room = room?.read(cx);
|
||||
let followers = room.followers_for(peer_id, project_id);
|
||||
|
||||
Some(followers.into_iter().flat_map(|&follower| {
|
||||
let remote_participant =
|
||||
room.remote_participant_for_peer_id(follower);
|
||||
|
||||
let avatar = remote_participant
|
||||
.and_then(|p| p.user.avatar.clone())
|
||||
.or_else(|| {
|
||||
if follower == workspace.read(cx).client().peer_id()? {
|
||||
workspace
|
||||
.read(cx)
|
||||
.user_store()
|
||||
.read(cx)
|
||||
.current_user()?
|
||||
.avatar
|
||||
.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
let location = remote_participant.map(|p| p.location);
|
||||
|
||||
Some(Self::render_face(
|
||||
avatar.clone(),
|
||||
Self::location_style(workspace, location, follower_style, cx),
|
||||
background_color,
|
||||
))
|
||||
}))
|
||||
})()
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
let mut container = face_pile
|
||||
.contained()
|
||||
.with_style(theme.workspace.titlebar.leader_selection);
|
||||
|
||||
if let Some(replica_id) = replica_id {
|
||||
if followed_by_self {
|
||||
let color = theme.editor.replica_selection_style(replica_id).selection;
|
||||
container = container.with_background_color(color);
|
||||
}
|
||||
}
|
||||
|
||||
container.boxed()
|
||||
}))
|
||||
.with_children(replica_color.map(|replica_color| {
|
||||
AvatarRibbon::new(replica_color)
|
||||
.constrained()
|
||||
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
|
||||
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.boxed()
|
||||
}))
|
||||
.constrained()
|
||||
.with_width(theme.workspace.titlebar.avatar_width)
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.avatar_margin)
|
||||
.with_children((|| {
|
||||
let replica_id = replica_id?;
|
||||
let color = theme.editor.replica_selection_style(replica_id).cursor;
|
||||
Some(
|
||||
AvatarRibbon::new(color)
|
||||
.constrained()
|
||||
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
|
||||
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.boxed(),
|
||||
)
|
||||
})())
|
||||
.boxed();
|
||||
|
||||
if let Some((peer_id, peer_github_login, location)) = peer {
|
||||
if let Some(location) = location {
|
||||
if let Some(replica_id) = replica_id {
|
||||
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
|
||||
content =
|
||||
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
|
||||
content
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleFollow(peer_id))
|
||||
})
|
||||
.with_tooltip::<ToggleFollow, _>(
|
||||
peer_id.as_u64() as usize,
|
||||
if is_followed {
|
||||
format!("Unfollow {}", peer_github_login)
|
||||
if is_being_followed {
|
||||
format!("Unfollow {}", user.github_login)
|
||||
} else {
|
||||
format!("Follow {}", peer_github_login)
|
||||
format!("Follow {}", user.github_login)
|
||||
},
|
||||
Some(Box::new(FollowNextCollaborator)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed()
|
||||
.boxed();
|
||||
} else if let ParticipantLocation::SharedProject { project_id } = location {
|
||||
let user_id = user.id;
|
||||
MouseEventHandler::<JoinProject>::new(peer_id.as_u64() as usize, cx, move |_, _| {
|
||||
content
|
||||
})
|
||||
content = MouseEventHandler::<JoinProject>::new(
|
||||
peer_id.as_u64() as usize,
|
||||
cx,
|
||||
move |_, _| content,
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(JoinProject {
|
||||
@ -485,29 +788,63 @@ impl CollabTitlebarItem {
|
||||
})
|
||||
.with_tooltip::<JoinProject, _>(
|
||||
peer_id.as_u64() as usize,
|
||||
format!("Follow {} into external project", peer_github_login),
|
||||
format!("Follow {} into external project", user.github_login),
|
||||
Some(Box::new(FollowNextCollaborator)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed()
|
||||
} else {
|
||||
content
|
||||
.boxed();
|
||||
}
|
||||
} else {
|
||||
content
|
||||
}
|
||||
content
|
||||
}
|
||||
|
||||
fn location_style(
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
location: Option<ParticipantLocation>,
|
||||
mut style: AvatarStyle,
|
||||
cx: &RenderContext<Self>,
|
||||
) -> AvatarStyle {
|
||||
if let Some(location) = location {
|
||||
if let ParticipantLocation::SharedProject { project_id } = location {
|
||||
if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
|
||||
style.image.grayscale = true;
|
||||
}
|
||||
} else {
|
||||
style.image.grayscale = true;
|
||||
}
|
||||
}
|
||||
|
||||
style
|
||||
}
|
||||
|
||||
fn render_face(
|
||||
avatar: Arc<ImageData>,
|
||||
avatar_style: AvatarStyle,
|
||||
background_color: Color,
|
||||
) -> ElementBox {
|
||||
Image::new(avatar)
|
||||
.with_style(avatar_style.image)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_background_color(background_color)
|
||||
.with_corner_radius(avatar_style.outer_corner_radius)
|
||||
.constrained()
|
||||
.with_width(avatar_style.outer_width)
|
||||
.with_height(avatar_style.outer_width)
|
||||
.aligned()
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_connection_status(
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
status: &client::Status,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Option<ElementBox> {
|
||||
enum ConnectionStatusButton {}
|
||||
|
||||
let theme = &cx.global::<Settings>().theme.clone();
|
||||
match &*workspace.read(cx).client().status().borrow() {
|
||||
match status {
|
||||
client::Status::ConnectionError
|
||||
| client::Status::ConnectionLost
|
||||
| client::Status::Reauthenticating { .. }
|
||||
|
@ -1,8 +1,10 @@
|
||||
mod collab_titlebar_item;
|
||||
mod collaborator_list_popover;
|
||||
mod contact_finder;
|
||||
mod contact_list;
|
||||
mod contact_notification;
|
||||
mod contacts_popover;
|
||||
mod face_pile;
|
||||
mod incoming_call_notification;
|
||||
mod notifications;
|
||||
mod project_shared_notification;
|
||||
@ -10,7 +12,7 @@ mod sharing_status_indicator;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use call::ActiveCall;
|
||||
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
|
||||
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
|
||||
use gpui::{actions, MutableAppContext, Task};
|
||||
use std::sync::Arc;
|
||||
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
|
||||
@ -116,7 +118,7 @@ fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut Mutable
|
||||
});
|
||||
|
||||
if let Some(follow_peer_id) = follow_peer_id {
|
||||
if !workspace.is_following(follow_peer_id) {
|
||||
if !workspace.is_being_followed(follow_peer_id) {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
|
||||
.map(|follow| follow.detach_and_log_err(cx));
|
||||
|
165
crates/collab_ui/src/collaborator_list_popover.rs
Normal file
@ -0,0 +1,165 @@
|
||||
use call::ActiveCall;
|
||||
use client::UserStore;
|
||||
use gpui::Action;
|
||||
use gpui::{
|
||||
actions, elements::*, Entity, ModelHandle, MouseButton, RenderContext, View, ViewContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
|
||||
use crate::collab_titlebar_item::ToggleCollaboratorList;
|
||||
|
||||
pub(crate) enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
enum Collaborator {
|
||||
SelfUser { username: String },
|
||||
RemoteUser { username: String },
|
||||
}
|
||||
|
||||
actions!(collaborator_list_popover, [NoOp]);
|
||||
|
||||
pub(crate) struct CollaboratorListPopover {
|
||||
list_state: ListState,
|
||||
}
|
||||
|
||||
impl Entity for CollaboratorListPopover {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for CollaboratorListPopover {
|
||||
fn ui_name() -> &'static str {
|
||||
"CollaboratorListPopover"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
MouseEventHandler::<Self>::new(0, cx, |_, _| {
|
||||
List::new(self.list_state.clone())
|
||||
.contained()
|
||||
.with_style(theme.contacts_popover.container) //TODO: Change the name of this theme key
|
||||
.constrained()
|
||||
.with_width(theme.contacts_popover.width)
|
||||
.with_height(theme.contacts_popover.height)
|
||||
.boxed()
|
||||
})
|
||||
.on_down_out(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleCollaboratorList);
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
}
|
||||
|
||||
impl CollaboratorListPopover {
|
||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
|
||||
let mut collaborators = user_store
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.map(|u| Collaborator::SelfUser {
|
||||
username: u.github_login.clone(),
|
||||
})
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
//TODO: What should the canonical sort here look like, consult contacts list implementation
|
||||
if let Some(room) = active_call.read(cx).room() {
|
||||
for participant in room.read(cx).remote_participants() {
|
||||
collaborators.push(Collaborator::RemoteUser {
|
||||
username: participant.1.user.github_login.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
list_state: ListState::new(
|
||||
collaborators.len(),
|
||||
Orientation::Top,
|
||||
0.,
|
||||
cx,
|
||||
move |_, index, cx| match &collaborators[index] {
|
||||
Collaborator::SelfUser { username } => render_collaborator_list_entry(
|
||||
index,
|
||||
username,
|
||||
None::<NoOp>,
|
||||
None,
|
||||
Svg::new("icons/chevron_right_12.svg"),
|
||||
NoOp,
|
||||
"Leave call".to_owned(),
|
||||
cx,
|
||||
),
|
||||
|
||||
Collaborator::RemoteUser { username } => render_collaborator_list_entry(
|
||||
index,
|
||||
username,
|
||||
Some(NoOp),
|
||||
Some(format!("Follow {username}")),
|
||||
Svg::new("icons/x_mark_12.svg"),
|
||||
NoOp,
|
||||
format!("Remove {username} from call"),
|
||||
cx,
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_collaborator_list_entry<UA: Action + Clone, IA: Action + Clone>(
|
||||
index: usize,
|
||||
username: &str,
|
||||
username_action: Option<UA>,
|
||||
username_tooltip: Option<String>,
|
||||
icon: Svg,
|
||||
icon_action: IA,
|
||||
icon_tooltip: String,
|
||||
cx: &mut RenderContext<CollaboratorListPopover>,
|
||||
) -> ElementBox {
|
||||
enum Username {}
|
||||
enum UsernameTooltip {}
|
||||
enum Icon {}
|
||||
enum IconTooltip {}
|
||||
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
let username_theme = theme.contact_list.contact_username.text.clone();
|
||||
let tooltip_theme = theme.tooltip.clone();
|
||||
|
||||
let username = MouseEventHandler::<Username>::new(index, cx, |_, _| {
|
||||
Label::new(username.to_owned(), username_theme.clone()).boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
if let Some(username_action) = username_action.clone() {
|
||||
cx.dispatch_action(username_action);
|
||||
}
|
||||
});
|
||||
|
||||
Flex::row()
|
||||
.with_child(if let Some(username_tooltip) = username_tooltip {
|
||||
username
|
||||
.with_tooltip::<UsernameTooltip, _>(
|
||||
index,
|
||||
username_tooltip,
|
||||
None,
|
||||
tooltip_theme.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed()
|
||||
} else {
|
||||
username.boxed()
|
||||
})
|
||||
.with_child(
|
||||
MouseEventHandler::<Icon>::new(index, cx, |_, _| icon.boxed())
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(icon_action.clone())
|
||||
})
|
||||
.with_tooltip::<IconTooltip, _>(index, icon_tooltip, None, tooltip_theme, cx)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use super::collab_titlebar_item::LeaveCall;
|
||||
use crate::contacts_popover;
|
||||
use call::ActiveCall;
|
||||
use client::{proto::PeerId, Contact, User, UserStore};
|
||||
@ -18,22 +19,20 @@ use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{mem, sync::Arc};
|
||||
use theme::IconButton;
|
||||
use util::ResultExt;
|
||||
use workspace::{JoinProject, OpenSharedScreen};
|
||||
|
||||
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
|
||||
impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
|
||||
impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ContactList::remove_contact);
|
||||
cx.add_action(ContactList::respond_to_contact_request);
|
||||
cx.add_action(ContactList::clear_filter);
|
||||
cx.add_action(ContactList::cancel);
|
||||
cx.add_action(ContactList::select_next);
|
||||
cx.add_action(ContactList::select_prev);
|
||||
cx.add_action(ContactList::confirm);
|
||||
cx.add_action(ContactList::toggle_expanded);
|
||||
cx.add_action(ContactList::call);
|
||||
cx.add_action(ContactList::leave_call);
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
@ -45,9 +44,6 @@ struct Call {
|
||||
initial_project: Option<ModelHandle<Project>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
struct LeaveCall;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
|
||||
enum Section {
|
||||
ActiveCall,
|
||||
@ -326,7 +322,7 @@ impl ContactList {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
let did_clear = self.filter_editor.update(cx, |editor, cx| {
|
||||
if editor.buffer().read(cx).len(cx) > 0 {
|
||||
editor.set_text("", cx);
|
||||
@ -335,6 +331,7 @@ impl ContactList {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if !did_clear {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
@ -980,6 +977,7 @@ impl ContactList {
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
enum Header {}
|
||||
enum LeaveCallContactList {}
|
||||
|
||||
let header_style = theme
|
||||
.header_row
|
||||
@ -992,9 +990,9 @@ impl ContactList {
|
||||
};
|
||||
let leave_call = if section == Section::ActiveCall {
|
||||
Some(
|
||||
MouseEventHandler::<LeaveCall>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<LeaveCallContactList>::new(0, cx, |state, _| {
|
||||
let style = theme.leave_call.style_for(state, false);
|
||||
Label::new("Leave Session", style.text.clone())
|
||||
Label::new("Leave Call", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
@ -1283,12 +1281,6 @@ impl ContactList {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContactList {
|
||||
@ -1302,7 +1294,7 @@ impl View for ContactList {
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx.add_identifier("menu");
|
||||
cx
|
||||
}
|
||||
|
||||
@ -1334,7 +1326,7 @@ impl View for ContactList {
|
||||
})
|
||||
.with_tooltip::<AddContact, _>(
|
||||
0,
|
||||
"Add contact".into(),
|
||||
"Search for new contact".into(),
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu};
|
||||
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
|
||||
use client::UserStore;
|
||||
use gpui::{
|
||||
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
|
||||
@ -155,7 +155,7 @@ impl View for ContactsPopover {
|
||||
.boxed()
|
||||
})
|
||||
.on_down_out(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleCollaborationMenu);
|
||||
cx.dispatch_action(ToggleContactsMenu);
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
101
crates/collab_ui/src/face_pile.rs
Normal file
@ -0,0 +1,101 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::ToJson,
|
||||
serde_json::{self, json},
|
||||
Axis, DebugContext, Element, ElementBox, MeasurementContext, PaintContext,
|
||||
};
|
||||
|
||||
pub(crate) struct FacePile {
|
||||
overlap: f32,
|
||||
faces: Vec<ElementBox>,
|
||||
}
|
||||
|
||||
impl FacePile {
|
||||
pub fn new(overlap: f32) -> FacePile {
|
||||
FacePile {
|
||||
overlap,
|
||||
faces: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for FacePile {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
cx: &mut gpui::LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
|
||||
|
||||
let mut width = 0.;
|
||||
for face in &mut self.faces {
|
||||
width += face.layout(constraint, cx).x();
|
||||
}
|
||||
width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||
|
||||
(Vector2F::new(width, constraint.max.y()), ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_layout: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
let origin_y = bounds.upper_right().y();
|
||||
let mut origin_x = bounds.upper_right().x();
|
||||
|
||||
for face in self.faces.iter_mut().rev() {
|
||||
let size = face.size();
|
||||
origin_x -= size.x();
|
||||
cx.paint_layer(None, |cx| {
|
||||
face.paint(vec2f(origin_x, origin_y), visible_bounds, cx);
|
||||
});
|
||||
origin_x += self.overlap;
|
||||
}
|
||||
|
||||
()
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &MeasurementContext,
|
||||
) -> Option<RectF> {
|
||||
None
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
bounds: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &DebugContext,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "FacePile",
|
||||
"bounds": bounds.to_json()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<ElementBox> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
|
||||
self.faces.extend(children);
|
||||
}
|
||||
}
|
@ -5,7 +5,9 @@ use gpui::{
|
||||
};
|
||||
use menu::*;
|
||||
use settings::Settings;
|
||||
use std::{any::TypeId, time::Duration};
|
||||
use std::{any::TypeId, borrow::Cow, time::Duration};
|
||||
|
||||
pub type StaticItem = Box<dyn Fn(&mut MutableAppContext) -> ElementBox>;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
struct Clicked;
|
||||
@ -24,16 +26,17 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
|
||||
pub enum ContextMenuItem {
|
||||
Item {
|
||||
label: String,
|
||||
label: Cow<'static, str>,
|
||||
action: Box<dyn Action>,
|
||||
},
|
||||
Static(StaticItem),
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl ContextMenuItem {
|
||||
pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
|
||||
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
|
||||
Self::Item {
|
||||
label: label.to_string(),
|
||||
label: label.into(),
|
||||
action: Box::new(action),
|
||||
}
|
||||
}
|
||||
@ -42,14 +45,14 @@ impl ContextMenuItem {
|
||||
Self::Separator
|
||||
}
|
||||
|
||||
fn is_separator(&self) -> bool {
|
||||
matches!(self, Self::Separator)
|
||||
fn is_action(&self) -> bool {
|
||||
matches!(self, Self::Item { .. })
|
||||
}
|
||||
|
||||
fn action_id(&self) -> Option<TypeId> {
|
||||
match self {
|
||||
ContextMenuItem::Item { action, .. } => Some(action.id()),
|
||||
ContextMenuItem::Separator => None,
|
||||
ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -58,6 +61,7 @@ pub struct ContextMenu {
|
||||
show_count: usize,
|
||||
anchor_position: Vector2F,
|
||||
anchor_corner: AnchorCorner,
|
||||
position_mode: OverlayPositionMode,
|
||||
items: Vec<ContextMenuItem>,
|
||||
selected_index: Option<usize>,
|
||||
visible: bool,
|
||||
@ -78,7 +82,7 @@ impl View for ContextMenu {
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx.add_identifier("menu");
|
||||
cx
|
||||
}
|
||||
|
||||
@ -105,6 +109,7 @@ impl View for ContextMenu {
|
||||
.with_fit_mode(OverlayFitMode::SnapToWindow)
|
||||
.with_anchor_position(self.anchor_position)
|
||||
.with_anchor_corner(self.anchor_corner)
|
||||
.with_position_mode(self.position_mode)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@ -121,6 +126,7 @@ impl ContextMenu {
|
||||
show_count: 0,
|
||||
anchor_position: Default::default(),
|
||||
anchor_corner: AnchorCorner::TopLeft,
|
||||
position_mode: OverlayPositionMode::Window,
|
||||
items: Default::default(),
|
||||
selected_index: Default::default(),
|
||||
visible: Default::default(),
|
||||
@ -188,13 +194,13 @@ impl ContextMenu {
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
|
||||
self.selected_index = self.items.iter().position(|item| !item.is_separator());
|
||||
self.selected_index = self.items.iter().position(|item| item.is_action());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
|
||||
for (ix, item) in self.items.iter().enumerate().rev() {
|
||||
if !item.is_separator() {
|
||||
if item.is_action() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
@ -205,7 +211,7 @@ impl ContextMenu {
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
|
||||
if !item.is_separator() {
|
||||
if item.is_action() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
@ -219,7 +225,7 @@ impl ContextMenu {
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
|
||||
if !item.is_separator() {
|
||||
if item.is_action() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
@ -234,7 +240,7 @@ impl ContextMenu {
|
||||
&mut self,
|
||||
anchor_position: Vector2F,
|
||||
anchor_corner: AnchorCorner,
|
||||
items: impl IntoIterator<Item = ContextMenuItem>,
|
||||
items: Vec<ContextMenuItem>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let mut items = items.into_iter().peekable();
|
||||
@ -254,6 +260,10 @@ impl ContextMenu {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_position_mode(&mut self, mode: OverlayPositionMode) {
|
||||
self.position_mode = mode;
|
||||
}
|
||||
|
||||
fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
|
||||
let window_id = cx.window_id();
|
||||
let style = cx.global::<Settings>().theme.context_menu.clone();
|
||||
@ -273,6 +283,9 @@ impl ContextMenu {
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
@ -302,6 +315,9 @@ impl ContextMenu {
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(_) => Empty::new().boxed(),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.collapsed()
|
||||
.constrained()
|
||||
@ -339,7 +355,7 @@ impl ContextMenu {
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(label.to_string(), style.label.clone())
|
||||
Label::new(label.clone(), style.label.clone())
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
@ -366,6 +382,9 @@ impl ContextMenu {
|
||||
.on_drag(MouseButton::Left, |_, _| {})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.constrained()
|
||||
.with_height(1.)
|
||||
|
@ -5071,7 +5071,7 @@ impl Editor {
|
||||
GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
|
||||
});
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
cx.spawn_labeled("Fetching Definition...", |workspace, mut cx| async move {
|
||||
let definitions = definitions.await?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
|
||||
@ -5151,31 +5151,36 @@ impl Editor {
|
||||
|
||||
let project = workspace.project().clone();
|
||||
let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
|
||||
Some(cx.spawn(|workspace, mut cx| async move {
|
||||
let locations = references.await?;
|
||||
if locations.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
Some(cx.spawn_labeled(
|
||||
"Finding All References...",
|
||||
|workspace, mut cx| async move {
|
||||
let locations = references.await?;
|
||||
if locations.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let title = locations
|
||||
.first()
|
||||
.as_ref()
|
||||
.map(|location| {
|
||||
let buffer = location.buffer.read(cx);
|
||||
format!(
|
||||
"References to `{}`",
|
||||
buffer
|
||||
.text_for_range(location.range.clone())
|
||||
.collect::<String>()
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx);
|
||||
});
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let title = locations
|
||||
.first()
|
||||
.as_ref()
|
||||
.map(|location| {
|
||||
let buffer = location.buffer.read(cx);
|
||||
format!(
|
||||
"References to `{}`",
|
||||
buffer
|
||||
.text_for_range(location.range.clone())
|
||||
.collect::<String>()
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
Self::open_locations_in_multibuffer(
|
||||
workspace, locations, replica_id, title, cx,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}))
|
||||
Ok(())
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Opens a multibuffer with the given project locations in it
|
||||
@ -5454,21 +5459,20 @@ impl Editor {
|
||||
None => return None,
|
||||
};
|
||||
|
||||
Some(self.perform_format(project, cx))
|
||||
Some(self.perform_format(project, FormatTrigger::Manual, cx))
|
||||
}
|
||||
|
||||
fn perform_format(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
trigger: FormatTrigger,
|
||||
cx: &mut ViewContext<'_, Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = self.buffer().clone();
|
||||
let buffers = buffer.read(cx).all_buffers();
|
||||
|
||||
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
||||
let format = project.update(cx, |project, cx| {
|
||||
project.format(buffers, true, FormatTrigger::Manual, cx)
|
||||
});
|
||||
let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let transaction = futures::select_biased! {
|
||||
@ -6428,17 +6432,13 @@ impl View for Editor {
|
||||
EditorMode::AutoHeight { .. } => "auto_height",
|
||||
EditorMode::Full => "full",
|
||||
};
|
||||
context.map.insert("mode".into(), mode.into());
|
||||
context.add_key("mode", mode);
|
||||
if self.pending_rename.is_some() {
|
||||
context.set.insert("renaming".into());
|
||||
context.add_identifier("renaming");
|
||||
}
|
||||
match self.context_menu.as_ref() {
|
||||
Some(ContextMenu::Completions(_)) => {
|
||||
context.set.insert("showing_completions".into());
|
||||
}
|
||||
Some(ContextMenu::CodeActions(_)) => {
|
||||
context.set.insert("showing_code_actions".into());
|
||||
}
|
||||
Some(ContextMenu::Completions(_)) => context.add_identifier("showing_completions"),
|
||||
Some(ContextMenu::CodeActions(_)) => context.add_identifier("showing_code_actions"),
|
||||
None => {}
|
||||
}
|
||||
|
||||
|
@ -4193,7 +4193,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
|
||||
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
|
||||
|
||||
let format = editor.update(cx, |editor, cx| editor.perform_format(project.clone(), cx));
|
||||
let format = editor.update(cx, |editor, cx| {
|
||||
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
|
||||
});
|
||||
fake_server
|
||||
.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
|
||||
assert_eq!(
|
||||
@ -4225,7 +4227,9 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
|
||||
futures::future::pending::<()>().await;
|
||||
unreachable!()
|
||||
});
|
||||
let format = editor.update(cx, |editor, cx| editor.perform_format(project, cx));
|
||||
let format = editor.update(cx, |editor, cx| {
|
||||
editor.perform_format(project, FormatTrigger::Manual, cx)
|
||||
});
|
||||
cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
|
||||
cx.foreground().start_waiting();
|
||||
format.await.unwrap();
|
||||
|
@ -14,7 +14,7 @@ use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
|
||||
SelectionGoal,
|
||||
};
|
||||
use project::{Item as _, Project, ProjectPath};
|
||||
use project::{FormatTrigger, Item as _, Project, ProjectPath};
|
||||
use rpc::proto::{self, update_view};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
@ -608,7 +608,7 @@ impl Item for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.report_event("save editor", cx);
|
||||
let format = self.perform_format(project.clone(), cx);
|
||||
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
|
||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||
cx.as_mut().spawn(|mut cx| async move {
|
||||
format.await?;
|
||||
|
@ -39,7 +39,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
pane::init(cx);
|
||||
});
|
||||
|
||||
let params = cx.update(AppState::test);
|
||||
let app_state = cx.update(AppState::test);
|
||||
|
||||
let file_name = format!(
|
||||
"file.{}",
|
||||
@ -56,10 +56,10 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
}))
|
||||
.await;
|
||||
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
|
||||
params
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
|
||||
|
@ -13,7 +13,6 @@ use gpui::{
|
||||
elements::{ChildView, Flex, Label, ParentElement},
|
||||
serde_json, AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle,
|
||||
MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use isahc::Request;
|
||||
use language::Buffer;
|
||||
@ -24,7 +23,6 @@ use serde::Serialize;
|
||||
use workspace::{
|
||||
item::{Item, ItemHandle},
|
||||
searchable::{SearchableItem, SearchableItemHandle},
|
||||
smallvec::SmallVec,
|
||||
AppState, Workspace,
|
||||
};
|
||||
|
||||
@ -259,16 +257,10 @@ impl Item for FeedbackEditor {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn to_item_events(_: &Self::Event) -> SmallVec<[workspace::item::ItemEvent; 2]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn can_save(&self, _: &AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
@ -295,7 +287,7 @@ impl Item for FeedbackEditor {
|
||||
_: ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
unreachable!("reload should not have been called")
|
||||
Task::Ready(Some(Ok(())))
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
@ -322,20 +314,6 @@ impl Item for FeedbackEditor {
|
||||
))
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
_: ModelHandle<Project>,
|
||||
_: WeakViewHandle<Workspace>,
|
||||
_: workspace::WorkspaceId,
|
||||
_: workspace::ItemId,
|
||||
_: &mut ViewContext<workspace::Pane>,
|
||||
) -> Task<anyhow::Result<ViewHandle<Self>>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(handle.clone()))
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ pub struct FileFinder {
|
||||
latest_search_id: usize,
|
||||
latest_search_did_cancel: bool,
|
||||
latest_search_query: String,
|
||||
relative_to: Option<Arc<Path>>,
|
||||
matches: Vec<PathMatch>,
|
||||
selected: Option<(usize, Arc<Path>)>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
@ -90,7 +91,11 @@ impl FileFinder {
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let finder = cx.add_view(|cx| Self::new(project, cx));
|
||||
let relative_to = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.project_path(cx))
|
||||
.map(|project_path| project_path.path.clone());
|
||||
let finder = cx.add_view(|cx| Self::new(project, relative_to, cx));
|
||||
cx.subscribe(&finder, Self::on_event).detach();
|
||||
finder
|
||||
});
|
||||
@ -115,7 +120,11 @@ impl FileFinder {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(
|
||||
project: ModelHandle<Project>,
|
||||
relative_to: Option<Arc<Path>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let handle = cx.weak_handle();
|
||||
cx.observe(&project, Self::project_updated).detach();
|
||||
Self {
|
||||
@ -125,6 +134,7 @@ impl FileFinder {
|
||||
latest_search_id: 0,
|
||||
latest_search_did_cancel: false,
|
||||
latest_search_query: String::new(),
|
||||
relative_to,
|
||||
matches: Vec::new(),
|
||||
selected: None,
|
||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
@ -137,6 +147,7 @@ impl FileFinder {
|
||||
}
|
||||
|
||||
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
|
||||
let relative_to = self.relative_to.clone();
|
||||
let worktrees = self
|
||||
.project
|
||||
.read(cx)
|
||||
@ -165,6 +176,7 @@ impl FileFinder {
|
||||
let matches = fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
&query,
|
||||
relative_to,
|
||||
false,
|
||||
100,
|
||||
&cancel_flag,
|
||||
@ -377,7 +389,7 @@ mod tests {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
|
||||
let query = "hi".to_string();
|
||||
finder
|
||||
@ -453,7 +465,7 @@ mod tests {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
finder
|
||||
.update(cx, |f, cx| f.spawn_search("hi".into(), cx))
|
||||
.await;
|
||||
@ -479,7 +491,7 @@ mod tests {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
|
||||
// Even though there is only one worktree, that worktree's filename
|
||||
// is included in the matching, because the worktree is a single file.
|
||||
@ -532,8 +544,9 @@ mod tests {
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
|
||||
// Run a search that matches two files with the same relative path.
|
||||
finder
|
||||
@ -551,6 +564,48 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_path_distance_ordering(cx: &mut gpui::TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let app_state = cx.update(AppState::test);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"dir1": { "a.txt": "" },
|
||||
"dir2": {
|
||||
"a.txt": "",
|
||||
"b.txt": ""
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
|
||||
// When workspace has an active item, sort items which are closer to that item
|
||||
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
|
||||
// so that one should be sorted earlier
|
||||
let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt")));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), b_path, cx));
|
||||
|
||||
finder
|
||||
.update(cx, |f, cx| f.spawn_search("a.txt".into(), cx))
|
||||
.await;
|
||||
|
||||
finder.read_with(cx, |f, _| {
|
||||
assert_eq!(f.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
|
||||
assert_eq!(f.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = cx.update(AppState::test);
|
||||
@ -573,7 +628,7 @@ mod tests {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
finder
|
||||
.update(cx, |f, cx| f.spawn_search("dir".into(), cx))
|
||||
.await;
|
||||
|
@ -443,6 +443,7 @@ mod tests {
|
||||
positions: Vec::new(),
|
||||
path: candidate.path.clone(),
|
||||
path_prefix: "".into(),
|
||||
distance_to_relative_ancestor: usize::MAX,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -25,6 +25,9 @@ pub struct PathMatch {
|
||||
pub worktree_id: usize,
|
||||
pub path: Arc<Path>,
|
||||
pub path_prefix: Arc<str>,
|
||||
/// Number of steps removed from a shared parent with the relative path
|
||||
/// Used to order closer paths first in the search list
|
||||
pub distance_to_relative_ancestor: usize,
|
||||
}
|
||||
|
||||
pub trait PathMatchCandidateSet<'a>: Send + Sync {
|
||||
@ -78,6 +81,11 @@ impl Ord for PathMatch {
|
||||
.partial_cmp(&other.score)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
|
||||
.then_with(|| {
|
||||
other
|
||||
.distance_to_relative_ancestor
|
||||
.cmp(&self.distance_to_relative_ancestor)
|
||||
})
|
||||
.then_with(|| self.path.cmp(&other.path))
|
||||
}
|
||||
}
|
||||
@ -85,6 +93,7 @@ impl Ord for PathMatch {
|
||||
pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||
candidate_sets: &'a [Set],
|
||||
query: &str,
|
||||
relative_to: Option<Arc<Path>>,
|
||||
smart_case: bool,
|
||||
max_results: usize,
|
||||
cancel_flag: &AtomicBool,
|
||||
@ -111,6 +120,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||
background
|
||||
.scoped(|scope| {
|
||||
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
|
||||
let relative_to = relative_to.clone();
|
||||
scope.spawn(async move {
|
||||
let segment_start = segment_idx * segment_size;
|
||||
let segment_end = segment_start + segment_size;
|
||||
@ -149,6 +159,15 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||
positions: Vec::new(),
|
||||
path: candidate.path.clone(),
|
||||
path_prefix: candidate_set.prefix(),
|
||||
distance_to_relative_ancestor: relative_to.as_ref().map_or(
|
||||
usize::MAX,
|
||||
|relative_to| {
|
||||
distance_between_paths(
|
||||
candidate.path.as_ref(),
|
||||
relative_to.as_ref(),
|
||||
)
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -172,3 +191,13 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
/// Compute the distance from a given path to some other path
|
||||
/// If there is no shared path, returns usize::MAX
|
||||
fn distance_between_paths(path: &Path, relative_to: &Path) -> usize {
|
||||
let mut path_components = path.components();
|
||||
let mut relative_components = relative_to.components();
|
||||
|
||||
while path_components.next() == relative_components.next() {}
|
||||
path_components.count() + relative_components.count() + 1
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ use uuid::Uuid;
|
||||
|
||||
pub use action::*;
|
||||
use callback_collection::CallbackCollection;
|
||||
use collections::{hash_map::Entry, HashMap, HashSet, VecDeque};
|
||||
use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
|
||||
pub use menu::*;
|
||||
use platform::Event;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@ -86,7 +86,7 @@ pub trait View: Entity + Sized {
|
||||
}
|
||||
fn default_keymap_context() -> keymap_matcher::KeymapContext {
|
||||
let mut cx = keymap_matcher::KeymapContext::default();
|
||||
cx.set.insert(Self::ui_name().into());
|
||||
cx.add_identifier(Self::ui_name());
|
||||
cx
|
||||
}
|
||||
fn debug_json(&self, _: &AppContext) -> serde_json::Value {
|
||||
@ -474,6 +474,7 @@ type WindowBoundsCallback = Box<dyn FnMut(WindowBounds, Uuid, &mut MutableAppCon
|
||||
type KeystrokeCallback = Box<
|
||||
dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut MutableAppContext) -> bool,
|
||||
>;
|
||||
type ActiveLabeledTasksCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
||||
type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
|
||||
type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
|
||||
|
||||
@ -503,6 +504,7 @@ pub struct MutableAppContext {
|
||||
window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
|
||||
window_bounds_observations: CallbackCollection<usize, WindowBoundsCallback>,
|
||||
keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
|
||||
active_labeled_task_observations: CallbackCollection<(), ActiveLabeledTasksCallback>,
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
presenters_and_platform_windows:
|
||||
@ -514,6 +516,8 @@ pub struct MutableAppContext {
|
||||
pending_flushes: usize,
|
||||
flushing_effects: bool,
|
||||
halt_action_dispatch: bool,
|
||||
next_labeled_task_id: usize,
|
||||
active_labeled_tasks: BTreeMap<usize, &'static str>,
|
||||
}
|
||||
|
||||
impl MutableAppContext {
|
||||
@ -562,6 +566,7 @@ impl MutableAppContext {
|
||||
window_bounds_observations: Default::default(),
|
||||
keystroke_observations: Default::default(),
|
||||
action_dispatch_observations: Default::default(),
|
||||
active_labeled_task_observations: Default::default(),
|
||||
presenters_and_platform_windows: Default::default(),
|
||||
foreground,
|
||||
pending_effects: VecDeque::new(),
|
||||
@ -570,6 +575,8 @@ impl MutableAppContext {
|
||||
pending_flushes: 0,
|
||||
flushing_effects: false,
|
||||
halt_action_dispatch: false,
|
||||
next_labeled_task_id: 0,
|
||||
active_labeled_tasks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -794,6 +801,12 @@ impl MutableAppContext {
|
||||
window.screen().display_uuid()
|
||||
}
|
||||
|
||||
pub fn active_labeled_tasks<'a>(
|
||||
&'a self,
|
||||
) -> impl DoubleEndedIterator<Item = &'static str> + 'a {
|
||||
self.active_labeled_tasks.values().cloned()
|
||||
}
|
||||
|
||||
pub fn render_view(&mut self, params: RenderParams) -> Result<ElementBox> {
|
||||
let window_id = params.window_id;
|
||||
let view_id = params.view_id;
|
||||
@ -1160,6 +1173,19 @@ impl MutableAppContext {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn observe_active_labeled_tasks<F>(&mut self, callback: F) -> Subscription
|
||||
where
|
||||
F: 'static + FnMut(&mut MutableAppContext) -> bool,
|
||||
{
|
||||
let subscription_id = post_inc(&mut self.next_subscription_id);
|
||||
self.active_labeled_task_observations
|
||||
.add_callback((), subscription_id, Box::new(callback));
|
||||
Subscription::ActiveLabeledTasksObservation(
|
||||
self.active_labeled_task_observations
|
||||
.subscribe((), subscription_id),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
|
||||
self.pending_effects.push_back(Effect::Deferred {
|
||||
callback: Box::new(callback),
|
||||
@ -2042,6 +2068,17 @@ impl MutableAppContext {
|
||||
handled_by,
|
||||
result,
|
||||
} => self.handle_keystroke_effect(window_id, keystroke, handled_by, result),
|
||||
Effect::ActiveLabeledTasksChanged => {
|
||||
self.handle_active_labeled_tasks_changed_effect()
|
||||
}
|
||||
Effect::ActiveLabeledTasksObservation {
|
||||
subscription_id,
|
||||
callback,
|
||||
} => self.active_labeled_task_observations.add_callback(
|
||||
(),
|
||||
subscription_id,
|
||||
callback,
|
||||
),
|
||||
}
|
||||
self.pending_notifications.clear();
|
||||
self.remove_dropped_entities();
|
||||
@ -2449,26 +2486,68 @@ impl MutableAppContext {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_active_labeled_tasks_changed_effect(&mut self) {
|
||||
self.active_labeled_task_observations
|
||||
.clone()
|
||||
.emit((), self, move |callback, this| {
|
||||
callback(this);
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
|
||||
self.pending_effects
|
||||
.push_back(Effect::Focus { window_id, view_id });
|
||||
}
|
||||
|
||||
pub fn spawn<F, Fut, T>(&self, f: F) -> Task<T>
|
||||
fn spawn_internal<F, Fut, T>(&mut self, task_name: Option<&'static str>, f: F) -> Task<T>
|
||||
where
|
||||
F: FnOnce(AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = T>,
|
||||
T: 'static,
|
||||
{
|
||||
let label_id = task_name.map(|task_name| {
|
||||
let id = post_inc(&mut self.next_labeled_task_id);
|
||||
self.active_labeled_tasks.insert(id, task_name);
|
||||
self.pending_effects
|
||||
.push_back(Effect::ActiveLabeledTasksChanged);
|
||||
id
|
||||
});
|
||||
|
||||
let future = f(self.to_async());
|
||||
let cx = self.to_async();
|
||||
self.foreground.spawn(async move {
|
||||
let result = future.await;
|
||||
cx.0.borrow_mut().flush_effects();
|
||||
let mut cx = cx.0.borrow_mut();
|
||||
|
||||
if let Some(completed_label_id) = label_id {
|
||||
cx.active_labeled_tasks.remove(&completed_label_id);
|
||||
cx.pending_effects
|
||||
.push_back(Effect::ActiveLabeledTasksChanged);
|
||||
}
|
||||
cx.flush_effects();
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub fn spawn_labeled<F, Fut, T>(&mut self, task_name: &'static str, f: F) -> Task<T>
|
||||
where
|
||||
F: FnOnce(AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = T>,
|
||||
T: 'static,
|
||||
{
|
||||
self.spawn_internal(Some(task_name), f)
|
||||
}
|
||||
|
||||
pub fn spawn<F, Fut, T>(&mut self, f: F) -> Task<T>
|
||||
where
|
||||
F: FnOnce(AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = T>,
|
||||
T: 'static,
|
||||
{
|
||||
self.spawn_internal(None, f)
|
||||
}
|
||||
|
||||
pub fn to_async(&self) -> AsyncAppContext {
|
||||
AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap())
|
||||
}
|
||||
@ -2907,6 +2986,11 @@ pub enum Effect {
|
||||
window_id: usize,
|
||||
callback: WindowShouldCloseSubscriptionCallback,
|
||||
},
|
||||
ActiveLabeledTasksChanged,
|
||||
ActiveLabeledTasksObservation {
|
||||
subscription_id: usize,
|
||||
callback: ActiveLabeledTasksCallback,
|
||||
},
|
||||
}
|
||||
|
||||
impl Debug for Effect {
|
||||
@ -3066,6 +3150,16 @@ impl Debug for Effect {
|
||||
)
|
||||
.field("result", result)
|
||||
.finish(),
|
||||
Effect::ActiveLabeledTasksChanged => {
|
||||
f.debug_struct("Effect::ActiveLabeledTasksChanged").finish()
|
||||
}
|
||||
Effect::ActiveLabeledTasksObservation {
|
||||
subscription_id,
|
||||
callback: _,
|
||||
} => f
|
||||
.debug_struct("Effect::ActiveLabeledTasksObservation")
|
||||
.field("subscription_id", subscription_id)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3480,7 +3574,7 @@ impl<'a, T: Entity> ModelContext<'a, T> {
|
||||
WeakModelHandle::new(self.model_id)
|
||||
}
|
||||
|
||||
pub fn spawn<F, Fut, S>(&self, f: F) -> Task<S>
|
||||
pub fn spawn<F, Fut, S>(&mut self, f: F) -> Task<S>
|
||||
where
|
||||
F: FnOnce(ModelHandle<T>, AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = S>,
|
||||
@ -3490,7 +3584,7 @@ impl<'a, T: Entity> ModelContext<'a, T> {
|
||||
self.app.spawn(|cx| f(handle, cx))
|
||||
}
|
||||
|
||||
pub fn spawn_weak<F, Fut, S>(&self, f: F) -> Task<S>
|
||||
pub fn spawn_weak<F, Fut, S>(&mut self, f: F) -> Task<S>
|
||||
where
|
||||
F: FnOnce(WeakModelHandle<T>, AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = S>,
|
||||
@ -3947,6 +4041,23 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn observe_active_labeled_tasks<F>(&mut self, mut callback: F) -> Subscription
|
||||
where
|
||||
F: 'static + FnMut(&mut T, &mut ViewContext<T>),
|
||||
{
|
||||
let observer = self.weak_handle();
|
||||
self.app.observe_active_labeled_tasks(move |cx| {
|
||||
if let Some(observer) = observer.upgrade(cx) {
|
||||
observer.update(cx, |observer, cx| {
|
||||
callback(observer, cx);
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn emit(&mut self, payload: T::Event) {
|
||||
self.app.pending_effects.push_back(Effect::Event {
|
||||
entity_id: self.view_id,
|
||||
@ -3993,7 +4104,17 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
self.app.halt_action_dispatch = false;
|
||||
}
|
||||
|
||||
pub fn spawn<F, Fut, S>(&self, f: F) -> Task<S>
|
||||
pub fn spawn_labeled<F, Fut, S>(&mut self, task_label: &'static str, f: F) -> Task<S>
|
||||
where
|
||||
F: FnOnce(ViewHandle<T>, AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = S>,
|
||||
S: 'static,
|
||||
{
|
||||
let handle = self.handle();
|
||||
self.app.spawn_labeled(task_label, |cx| f(handle, cx))
|
||||
}
|
||||
|
||||
pub fn spawn<F, Fut, S>(&mut self, f: F) -> Task<S>
|
||||
where
|
||||
F: FnOnce(ViewHandle<T>, AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = S>,
|
||||
@ -4003,7 +4124,7 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
self.app.spawn(|cx| f(handle, cx))
|
||||
}
|
||||
|
||||
pub fn spawn_weak<F, Fut, S>(&self, f: F) -> Task<S>
|
||||
pub fn spawn_weak<F, Fut, S>(&mut self, f: F) -> Task<S>
|
||||
where
|
||||
F: FnOnce(WeakViewHandle<T>, AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Future<Output = S>,
|
||||
@ -5121,6 +5242,9 @@ pub enum Subscription {
|
||||
KeystrokeObservation(callback_collection::Subscription<usize, KeystrokeCallback>),
|
||||
ReleaseObservation(callback_collection::Subscription<usize, ReleaseObservationCallback>),
|
||||
ActionObservation(callback_collection::Subscription<(), ActionObservationCallback>),
|
||||
ActiveLabeledTasksObservation(
|
||||
callback_collection::Subscription<(), ActiveLabeledTasksCallback>,
|
||||
),
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
@ -5137,6 +5261,7 @@ impl Subscription {
|
||||
Subscription::KeystrokeObservation(subscription) => subscription.id(),
|
||||
Subscription::ReleaseObservation(subscription) => subscription.id(),
|
||||
Subscription::ActionObservation(subscription) => subscription.id(),
|
||||
Subscription::ActiveLabeledTasksObservation(subscription) => subscription.id(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -5153,6 +5278,7 @@ impl Subscription {
|
||||
Subscription::WindowBoundsObservation(subscription) => subscription.detach(),
|
||||
Subscription::ReleaseObservation(subscription) => subscription.detach(),
|
||||
Subscription::ActionObservation(subscription) => subscription.detach(),
|
||||
Subscription::ActiveLabeledTasksObservation(subscription) => subscription.detach(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5161,6 +5287,7 @@ impl Subscription {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{actions, elements::*, impl_actions, MouseButton, MouseButtonEvent};
|
||||
use postage::{sink::Sink, stream::Stream};
|
||||
use serde::Deserialize;
|
||||
use smol::future::poll_once;
|
||||
use std::{
|
||||
@ -6512,12 +6639,12 @@ mod tests {
|
||||
let mut view_1 = View::new(1);
|
||||
let mut view_2 = View::new(2);
|
||||
let mut view_3 = View::new(3);
|
||||
view_1.keymap_context.set.insert("a".into());
|
||||
view_2.keymap_context.set.insert("a".into());
|
||||
view_2.keymap_context.set.insert("b".into());
|
||||
view_3.keymap_context.set.insert("a".into());
|
||||
view_3.keymap_context.set.insert("b".into());
|
||||
view_3.keymap_context.set.insert("c".into());
|
||||
view_1.keymap_context.add_identifier("a");
|
||||
view_2.keymap_context.add_identifier("a");
|
||||
view_2.keymap_context.add_identifier("b");
|
||||
view_3.keymap_context.add_identifier("a");
|
||||
view_3.keymap_context.add_identifier("b");
|
||||
view_3.keymap_context.add_identifier("c");
|
||||
|
||||
let (window_id, view_1) = cx.add_window(Default::default(), |_| view_1);
|
||||
let view_2 = cx.add_view(&view_1, |_| view_2);
|
||||
@ -6776,6 +6903,26 @@ mod tests {
|
||||
assert_eq!(presenter.borrow().rendered_views.len(), 1);
|
||||
}
|
||||
|
||||
#[crate::test(self)]
|
||||
async fn test_labeled_tasks(cx: &mut TestAppContext) {
|
||||
assert_eq!(None, cx.update(|cx| cx.active_labeled_tasks().next()));
|
||||
let (mut sender, mut reciever) = postage::oneshot::channel::<()>();
|
||||
let task = cx
|
||||
.update(|cx| cx.spawn_labeled("Test Label", |_| async move { reciever.recv().await }));
|
||||
|
||||
assert_eq!(
|
||||
Some("Test Label"),
|
||||
cx.update(|cx| cx.active_labeled_tasks().next())
|
||||
);
|
||||
sender
|
||||
.send(())
|
||||
.await
|
||||
.expect("Could not send message to complete task");
|
||||
task.await;
|
||||
|
||||
assert_eq!(None, cx.update(|cx| cx.active_labeled_tasks().next()));
|
||||
}
|
||||
|
||||
#[crate::test(self)]
|
||||
async fn test_window_activation(cx: &mut TestAppContext) {
|
||||
struct View(&'static str);
|
||||
|
@ -16,6 +16,14 @@ pub trait Action: 'static {
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn Action {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("dyn Action")
|
||||
.field("namespace", &self.namespace())
|
||||
.field("name", &self.name())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
/// Define a set of unit struct types that all implement the `Action` trait.
|
||||
///
|
||||
/// The first argument is a namespace that will be associated with each of
|
||||
|
@ -11,9 +11,46 @@ pub enum MenuItem<'a> {
|
||||
Action {
|
||||
name: &'a str,
|
||||
action: Box<dyn Action>,
|
||||
os_action: Option<OsAction>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> MenuItem<'a> {
|
||||
pub fn separator() -> Self {
|
||||
Self::Separator
|
||||
}
|
||||
|
||||
pub fn submenu(menu: Menu<'a>) -> Self {
|
||||
Self::Submenu(menu)
|
||||
}
|
||||
|
||||
pub fn action(name: &'a str, action: impl Action) -> Self {
|
||||
Self::Action {
|
||||
name,
|
||||
action: Box::new(action),
|
||||
os_action: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self {
|
||||
Self::Action {
|
||||
name,
|
||||
action: Box::new(action),
|
||||
os_action: Some(os_action),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum OsAction {
|
||||
Cut,
|
||||
Copy,
|
||||
Paste,
|
||||
SelectAll,
|
||||
Undo,
|
||||
Redo,
|
||||
}
|
||||
|
||||
impl MutableAppContext {
|
||||
pub fn set_menus(&mut self, menus: Vec<Menu>) {
|
||||
self.foreground_platform
|
||||
|
@ -363,6 +363,7 @@ impl<T: Element> AnyElement for Lifecycle<T> {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
_ => panic!("invalid element lifecycle state"),
|
||||
}
|
||||
}
|
||||
|
@ -308,7 +308,9 @@ impl Element for Flex {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
child.paint(child_origin, visible_bounds, cx);
|
||||
|
||||
match self.axis {
|
||||
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
|
||||
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
|
||||
|
@ -5,7 +5,7 @@ mod keystroke;
|
||||
|
||||
use std::{any::TypeId, fmt::Debug};
|
||||
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use collections::HashMap;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::Action;
|
||||
@ -68,8 +68,8 @@ impl KeymapMatcher {
|
||||
/// There exist bindings which are still waiting for more keys.
|
||||
/// MatchResult::Complete(matches) =>
|
||||
/// 1 or more bindings have recieved the necessary key presses.
|
||||
/// The order of the matched actions is by order in the keymap file first and
|
||||
/// position of the matching view second.
|
||||
/// The order of the matched actions is by position of the matching first,
|
||||
// and order in the keymap second.
|
||||
pub fn push_keystroke(
|
||||
&mut self,
|
||||
keystroke: Keystroke,
|
||||
@ -80,8 +80,7 @@ impl KeymapMatcher {
|
||||
// and then the order the binding matched in the view tree second.
|
||||
// The key is the reverse position of the binding in the bindings list so that later bindings
|
||||
// match before earlier ones in the user's config
|
||||
let mut matched_bindings: BTreeMap<usize, Vec<(usize, Box<dyn Action>)>> =
|
||||
Default::default();
|
||||
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Default::default();
|
||||
|
||||
let first_keystroke = self.pending_keystrokes.is_empty();
|
||||
self.pending_keystrokes.push(keystroke.clone());
|
||||
@ -105,14 +104,11 @@ impl KeymapMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
for (order, binding) in self.keymap.bindings().iter().rev().enumerate() {
|
||||
for binding in self.keymap.bindings().iter().rev() {
|
||||
match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
|
||||
{
|
||||
BindingMatchResult::Complete(action) => {
|
||||
matched_bindings
|
||||
.entry(order)
|
||||
.or_default()
|
||||
.push((*view_id, action));
|
||||
matched_bindings.push((*view_id, action));
|
||||
}
|
||||
BindingMatchResult::Partial => {
|
||||
self.pending_views
|
||||
@ -131,7 +127,7 @@ impl KeymapMatcher {
|
||||
if !matched_bindings.is_empty() {
|
||||
// Collect the sorted matched bindings into the final vec for ease of use
|
||||
// Matched bindings are in order by precedence
|
||||
MatchResult::Matches(matched_bindings.into_values().flatten().collect())
|
||||
MatchResult::Matches(matched_bindings)
|
||||
} else if any_pending {
|
||||
MatchResult::Pending
|
||||
} else {
|
||||
@ -225,15 +221,47 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_keymap_and_view_ordering() -> Result<()> {
|
||||
actions!(test, [EditorAction, ProjectPanelAction]);
|
||||
|
||||
let mut editor = KeymapContext::default();
|
||||
editor.add_identifier("Editor");
|
||||
|
||||
let mut project_panel = KeymapContext::default();
|
||||
project_panel.add_identifier("ProjectPanel");
|
||||
|
||||
// Editor 'deeper' in than project panel
|
||||
let dispatch_path = vec![(2, editor), (1, project_panel)];
|
||||
|
||||
// But editor actions 'higher' up in keymap
|
||||
let keymap = Keymap::new(vec![
|
||||
Binding::new("left", EditorAction, Some("Editor")),
|
||||
Binding::new("left", ProjectPanelAction, Some("ProjectPanel")),
|
||||
]);
|
||||
|
||||
let mut matcher = KeymapMatcher::new(keymap);
|
||||
|
||||
assert_eq!(
|
||||
matcher.push_keystroke(Keystroke::parse("left")?, dispatch_path.clone()),
|
||||
MatchResult::Matches(vec![
|
||||
(2, Box::new(EditorAction)),
|
||||
(1, Box::new(ProjectPanelAction)),
|
||||
]),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_push_keystroke() -> Result<()> {
|
||||
actions!(test, [B, AB, C, D, DA]);
|
||||
actions!(test, [B, AB, C, D, DA, E, EF]);
|
||||
|
||||
let mut context1 = KeymapContext::default();
|
||||
context1.set.insert("1".into());
|
||||
context1.add_identifier("1");
|
||||
|
||||
let mut context2 = KeymapContext::default();
|
||||
context2.set.insert("2".into());
|
||||
context2.add_identifier("2");
|
||||
|
||||
let dispatch_path = vec![(2, context2), (1, context1)];
|
||||
|
||||
@ -286,6 +314,7 @@ mod tests {
|
||||
matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
|
||||
MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
|
||||
);
|
||||
|
||||
// If none of the d action handlers consume the binding, a pending
|
||||
// binding may then be used
|
||||
assert_eq!(
|
||||
@ -366,22 +395,22 @@ mod tests {
|
||||
let predicate = KeymapContextPredicate::parse("a && b || c == d").unwrap();
|
||||
|
||||
let mut context = KeymapContext::default();
|
||||
context.set.insert("a".into());
|
||||
context.add_identifier("a");
|
||||
assert!(!predicate.eval(&[context]));
|
||||
|
||||
let mut context = KeymapContext::default();
|
||||
context.set.insert("a".into());
|
||||
context.set.insert("b".into());
|
||||
context.add_identifier("a");
|
||||
context.add_identifier("b");
|
||||
assert!(predicate.eval(&[context]));
|
||||
|
||||
let mut context = KeymapContext::default();
|
||||
context.set.insert("a".into());
|
||||
context.map.insert("c".into(), "x".into());
|
||||
context.add_identifier("a");
|
||||
context.add_key("c", "x");
|
||||
assert!(!predicate.eval(&[context]));
|
||||
|
||||
let mut context = KeymapContext::default();
|
||||
context.set.insert("a".into());
|
||||
context.map.insert("c".into(), "d".into());
|
||||
context.add_identifier("a");
|
||||
context.add_key("c", "d");
|
||||
assert!(predicate.eval(&[context]));
|
||||
|
||||
let predicate = KeymapContextPredicate::parse("!a").unwrap();
|
||||
@ -421,10 +450,11 @@ mod tests {
|
||||
assert!(!predicate.eval(&contexts[6..]));
|
||||
|
||||
fn context_set(names: &[&str]) -> KeymapContext {
|
||||
KeymapContext {
|
||||
set: names.iter().copied().map(str::to_string).collect(),
|
||||
..Default::default()
|
||||
}
|
||||
let mut keymap = KeymapContext::new();
|
||||
names
|
||||
.iter()
|
||||
.for_each(|name| keymap.add_identifier(name.to_string()));
|
||||
keymap
|
||||
}
|
||||
}
|
||||
|
||||
@ -447,10 +477,10 @@ mod tests {
|
||||
]);
|
||||
|
||||
let mut context_a = KeymapContext::default();
|
||||
context_a.set.insert("a".into());
|
||||
context_a.add_identifier("a");
|
||||
|
||||
let mut context_b = KeymapContext::default();
|
||||
context_b.set.insert("b".into());
|
||||
context_b.add_identifier("b");
|
||||
|
||||
let mut matcher = KeymapMatcher::new(keymap);
|
||||
|
||||
@ -495,7 +525,7 @@ mod tests {
|
||||
matcher.clear_pending();
|
||||
|
||||
let mut context_c = KeymapContext::default();
|
||||
context_c.set.insert("c".into());
|
||||
context_c.add_identifier("c");
|
||||
|
||||
// Pending keystrokes are maintained per-view
|
||||
assert_eq!(
|
||||
|
@ -1,13 +1,22 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct KeymapContext {
|
||||
pub set: HashSet<String>,
|
||||
pub map: HashMap<String, String>,
|
||||
set: HashSet<Cow<'static, str>>,
|
||||
map: HashMap<Cow<'static, str>, Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl KeymapContext {
|
||||
pub fn new() -> Self {
|
||||
KeymapContext {
|
||||
set: HashSet::default(),
|
||||
map: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, other: &Self) {
|
||||
for v in &other.set {
|
||||
self.set.insert(v.clone());
|
||||
@ -16,6 +25,18 @@ impl KeymapContext {
|
||||
self.map.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_identifier<I: Into<Cow<'static, str>>>(&mut self, identifier: I) {
|
||||
self.set.insert(identifier.into());
|
||||
}
|
||||
|
||||
pub fn add_key<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
|
||||
&mut self,
|
||||
key: S1,
|
||||
value: S2,
|
||||
) {
|
||||
self.map.insert(key.into(), value.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
@ -46,12 +67,12 @@ impl KeymapContextPredicate {
|
||||
Self::Identifier(name) => (&context.set).contains(name.as_str()),
|
||||
Self::Equal(left, right) => context
|
||||
.map
|
||||
.get(left)
|
||||
.get(left.as_str())
|
||||
.map(|value| value == right)
|
||||
.unwrap_or(false),
|
||||
Self::NotEqual(left, right) => context
|
||||
.map
|
||||
.get(left)
|
||||
.get(left.as_str())
|
||||
.map(|value| value != right)
|
||||
.unwrap_or(true),
|
||||
Self::Not(pred) => !pred.eval(contexts),
|
||||
|
@ -98,6 +98,31 @@ unsafe fn build_classes() {
|
||||
sel!(handleGPUIMenuItem:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
// Add menu item handlers so that OS save panels have the correct key commands
|
||||
decl.add_method(
|
||||
sel!(cut:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(copy:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(paste:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(selectAll:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(undo:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(redo:),
|
||||
handle_menu_item as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(validateMenuItem:),
|
||||
validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool,
|
||||
@ -193,11 +218,25 @@ impl MacForegroundPlatform {
|
||||
) -> id {
|
||||
match item {
|
||||
MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
||||
MenuItem::Action { name, action } => {
|
||||
MenuItem::Action {
|
||||
name,
|
||||
action,
|
||||
os_action,
|
||||
} => {
|
||||
// TODO
|
||||
let keystrokes = keystroke_matcher
|
||||
.bindings_for_action_type(action.as_any().type_id())
|
||||
.find(|binding| binding.action().eq(action.as_ref()))
|
||||
.map(|binding| binding.keystrokes());
|
||||
let selector = match os_action {
|
||||
Some(crate::OsAction::Cut) => selector("cut:"),
|
||||
Some(crate::OsAction::Copy) => selector("copy:"),
|
||||
Some(crate::OsAction::Paste) => selector("paste:"),
|
||||
Some(crate::OsAction::SelectAll) => selector("selectAll:"),
|
||||
Some(crate::OsAction::Undo) => selector("undo:"),
|
||||
Some(crate::OsAction::Redo) => selector("redo:"),
|
||||
None => selector("handleGPUIMenuItem:"),
|
||||
};
|
||||
|
||||
let item;
|
||||
if let Some(keystrokes) = keystrokes {
|
||||
@ -218,7 +257,7 @@ impl MacForegroundPlatform {
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector("handleGPUIMenuItem:"),
|
||||
selector,
|
||||
ns_string(key_to_native(&keystroke.key).as_ref()),
|
||||
)
|
||||
.autorelease();
|
||||
@ -240,7 +279,7 @@ impl MacForegroundPlatform {
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(&name),
|
||||
selector("handleGPUIMenuItem:"),
|
||||
selector,
|
||||
ns_string(""),
|
||||
)
|
||||
.autorelease();
|
||||
@ -249,7 +288,7 @@ impl MacForegroundPlatform {
|
||||
item = NSMenuItem::alloc(nil)
|
||||
.initWithTitle_action_keyEquivalent_(
|
||||
ns_string(name),
|
||||
selector("handleGPUIMenuItem:"),
|
||||
selector,
|
||||
ns_string(""),
|
||||
)
|
||||
.autorelease();
|
||||
|
@ -737,6 +737,7 @@ impl platform::Window for Window {
|
||||
let title = ns_string(title);
|
||||
let _: () = msg_send![app, changeWindowsItem:window title:title filename:false];
|
||||
let _: () = msg_send![window, setTitle: title];
|
||||
self.0.borrow().move_traffic_light();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ fn main() {
|
||||
items: vec![MenuItem::Action {
|
||||
name: "Quit",
|
||||
action: Box::new(Quit),
|
||||
os_action: None,
|
||||
}],
|
||||
}]);
|
||||
|
||||
|
@ -126,7 +126,7 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx.add_identifier("menu");
|
||||
cx
|
||||
}
|
||||
|
||||
|
@ -1314,7 +1314,7 @@ impl View for ProjectPanel {
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx.add_identifier("menu");
|
||||
cx
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ publish = false
|
||||
path = "src/rope.rs"
|
||||
|
||||
[dependencies]
|
||||
bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "dac565a90e8f9245f48ff46225c915dc50f76920" }
|
||||
bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" }
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
arrayvec = "0.7.1"
|
||||
|
@ -16,7 +16,7 @@ message Envelope {
|
||||
Error error = 6;
|
||||
Ping ping = 7;
|
||||
Test test = 8;
|
||||
|
||||
|
||||
CreateRoom create_room = 9;
|
||||
CreateRoomResponse create_room_response = 10;
|
||||
JoinRoom join_room = 11;
|
||||
@ -206,7 +206,8 @@ message Room {
|
||||
uint64 id = 1;
|
||||
repeated Participant participants = 2;
|
||||
repeated PendingParticipant pending_participants = 3;
|
||||
string live_kit_room = 4;
|
||||
repeated Follower followers = 4;
|
||||
string live_kit_room = 5;
|
||||
}
|
||||
|
||||
message Participant {
|
||||
@ -227,6 +228,12 @@ message ParticipantProject {
|
||||
repeated string worktree_root_names = 2;
|
||||
}
|
||||
|
||||
message Follower {
|
||||
PeerId leader_id = 1;
|
||||
PeerId follower_id = 2;
|
||||
uint64 project_id = 3;
|
||||
}
|
||||
|
||||
message ParticipantLocation {
|
||||
oneof variant {
|
||||
SharedProject shared_project = 1;
|
||||
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
||||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 46;
|
||||
pub const PROTOCOL_VERSION: u32 = 49;
|
||||
|
@ -248,15 +248,15 @@ impl Item for ProjectSearchView {
|
||||
tab_theme: &theme::Tab,
|
||||
cx: &gpui::AppContext,
|
||||
) -> ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
let search_theme = &settings.theme.search;
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new("icons/magnifying_glass_12.svg")
|
||||
.with_color(tab_theme.label.text.color)
|
||||
.constrained()
|
||||
.with_width(search_theme.tab_icon_width)
|
||||
.with_width(tab_theme.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(tab_theme.spacing)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(self.model.read(cx).active_query.as_ref().map(|query| {
|
||||
@ -264,8 +264,6 @@ impl Item for ProjectSearchView {
|
||||
|
||||
Label::new(query_text, tab_theme.label.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(search_theme.tab_icon_spacing)
|
||||
.boxed()
|
||||
}))
|
||||
.boxed()
|
||||
|
@ -21,7 +21,7 @@ use gpui::{
|
||||
use project::{LocalWorktree, Project};
|
||||
use serde::Deserialize;
|
||||
use settings::{Settings, TerminalBlink, WorkingDirectory};
|
||||
use smallvec::SmallVec;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::Timer;
|
||||
use terminal::{
|
||||
alacritty_terminal::{
|
||||
@ -469,53 +469,50 @@ impl View for TerminalView {
|
||||
let mut context = Self::default_keymap_context();
|
||||
|
||||
let mode = self.terminal.read(cx).last_content.mode;
|
||||
context.map.insert(
|
||||
"screen".to_string(),
|
||||
(if mode.contains(TermMode::ALT_SCREEN) {
|
||||
context.add_key(
|
||||
"screen",
|
||||
if mode.contains(TermMode::ALT_SCREEN) {
|
||||
"alt"
|
||||
} else {
|
||||
"normal"
|
||||
})
|
||||
.to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
if mode.contains(TermMode::APP_CURSOR) {
|
||||
context.set.insert("DECCKM".to_string());
|
||||
context.add_identifier("DECCKM");
|
||||
}
|
||||
if mode.contains(TermMode::APP_KEYPAD) {
|
||||
context.set.insert("DECPAM".to_string());
|
||||
}
|
||||
//Note the ! here
|
||||
if !mode.contains(TermMode::APP_KEYPAD) {
|
||||
context.set.insert("DECPNM".to_string());
|
||||
context.add_identifier("DECPAM");
|
||||
} else {
|
||||
context.add_identifier("DECPNM");
|
||||
}
|
||||
if mode.contains(TermMode::SHOW_CURSOR) {
|
||||
context.set.insert("DECTCEM".to_string());
|
||||
context.add_identifier("DECTCEM");
|
||||
}
|
||||
if mode.contains(TermMode::LINE_WRAP) {
|
||||
context.set.insert("DECAWM".to_string());
|
||||
context.add_identifier("DECAWM");
|
||||
}
|
||||
if mode.contains(TermMode::ORIGIN) {
|
||||
context.set.insert("DECOM".to_string());
|
||||
context.add_identifier("DECOM");
|
||||
}
|
||||
if mode.contains(TermMode::INSERT) {
|
||||
context.set.insert("IRM".to_string());
|
||||
context.add_identifier("IRM");
|
||||
}
|
||||
//LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
|
||||
if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
|
||||
context.set.insert("LNM".to_string());
|
||||
context.add_identifier("LNM");
|
||||
}
|
||||
if mode.contains(TermMode::FOCUS_IN_OUT) {
|
||||
context.set.insert("report_focus".to_string());
|
||||
context.add_identifier("report_focus");
|
||||
}
|
||||
if mode.contains(TermMode::ALTERNATE_SCROLL) {
|
||||
context.set.insert("alternate_scroll".to_string());
|
||||
context.add_identifier("alternate_scroll");
|
||||
}
|
||||
if mode.contains(TermMode::BRACKETED_PASTE) {
|
||||
context.set.insert("bracketed_paste".to_string());
|
||||
context.add_identifier("bracketed_paste");
|
||||
}
|
||||
if mode.intersects(TermMode::MOUSE_MODE) {
|
||||
context.set.insert("any_mouse_reporting".to_string());
|
||||
context.add_identifier("any_mouse_reporting");
|
||||
}
|
||||
{
|
||||
let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
|
||||
@ -527,9 +524,7 @@ impl View for TerminalView {
|
||||
} else {
|
||||
"off"
|
||||
};
|
||||
context
|
||||
.map
|
||||
.insert("mouse_reporting".to_string(), mouse_reporting.to_string());
|
||||
context.add_key("mouse_reporting", mouse_reporting);
|
||||
}
|
||||
{
|
||||
let format = if mode.contains(TermMode::SGR_MOUSE) {
|
||||
@ -539,9 +534,7 @@ impl View for TerminalView {
|
||||
} else {
|
||||
"normal"
|
||||
};
|
||||
context
|
||||
.map
|
||||
.insert("mouse_format".to_string(), format.to_string());
|
||||
context.add_key("mouse_format", format);
|
||||
}
|
||||
context
|
||||
}
|
||||
@ -589,11 +582,16 @@ impl Item for TerminalView {
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(title, tab_theme.label.clone())
|
||||
gpui::elements::Svg::new("icons/terminal_12.svg")
|
||||
.with_color(tab_theme.label.text.color)
|
||||
.constrained()
|
||||
.with_width(tab_theme.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(tab_theme.spacing)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Label::new(title, tab_theme.label.clone()).aligned().boxed())
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@ -616,43 +614,6 @@ impl Item for TerminalView {
|
||||
None
|
||||
}
|
||||
|
||||
fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
|
||||
|
||||
fn is_singleton(&self, _cx: &gpui::AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn can_save(&self, _cx: &gpui::AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
_project: gpui::ModelHandle<Project>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||
unreachable!("save should not have been called");
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_project: gpui::ModelHandle<Project>,
|
||||
_abs_path: std::path::PathBuf,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||
unreachable!("save_as should not have been called");
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
_project: gpui::ModelHandle<Project>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||
gpui::Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, _cx: &gpui::AppContext) -> bool {
|
||||
self.has_bell()
|
||||
}
|
||||
@ -667,10 +628,10 @@ impl Item for TerminalView {
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
match event {
|
||||
Event::BreadcrumbsChanged => smallvec::smallvec![ItemEvent::UpdateBreadcrumbs],
|
||||
Event::TitleChanged | Event::Wakeup => smallvec::smallvec![ItemEvent::UpdateTab],
|
||||
Event::CloseTerminal => smallvec::smallvec![ItemEvent::CloseItem],
|
||||
_ => smallvec::smallvec![],
|
||||
Event::BreadcrumbsChanged => smallvec![ItemEvent::UpdateBreadcrumbs],
|
||||
Event::TitleChanged | Event::Wakeup => smallvec![ItemEvent::UpdateTab],
|
||||
Event::CloseTerminal => smallvec![ItemEvent::CloseItem],
|
||||
_ => smallvec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,20 +74,32 @@ pub struct Titlebar {
|
||||
pub container: ContainerStyle,
|
||||
pub height: f32,
|
||||
pub title: TextStyle,
|
||||
pub avatar_width: f32,
|
||||
pub avatar_margin: f32,
|
||||
pub item_spacing: f32,
|
||||
pub face_pile_spacing: f32,
|
||||
pub avatar_ribbon: AvatarRibbon,
|
||||
pub follower_avatar_overlap: f32,
|
||||
pub leader_selection: ContainerStyle,
|
||||
pub offline_icon: OfflineIcon,
|
||||
pub avatar: ImageStyle,
|
||||
pub inactive_avatar: ImageStyle,
|
||||
pub leader_avatar: AvatarStyle,
|
||||
pub follower_avatar: AvatarStyle,
|
||||
pub inactive_avatar_grayscale: bool,
|
||||
pub sign_in_prompt: Interactive<ContainedText>,
|
||||
pub outdated_warning: ContainedText,
|
||||
pub share_button: Interactive<ContainedText>,
|
||||
pub call_control: Interactive<IconButton>,
|
||||
pub toggle_contacts_button: Interactive<IconButton>,
|
||||
pub user_menu_button: Interactive<IconButton>,
|
||||
pub toggle_contacts_badge: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Deserialize, Default)]
|
||||
pub struct AvatarStyle {
|
||||
#[serde(flatten)]
|
||||
pub image: ImageStyle,
|
||||
pub outer_width: f32,
|
||||
pub outer_corner_radius: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct ContactsPopover {
|
||||
#[serde(flatten)]
|
||||
@ -246,8 +258,6 @@ pub struct Search {
|
||||
pub match_background: Color,
|
||||
pub match_index: ContainedText,
|
||||
pub results_status: TextStyle,
|
||||
pub tab_icon_width: f32,
|
||||
pub tab_icon_spacing: f32,
|
||||
pub dismiss_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
@ -381,7 +391,7 @@ pub struct InviteLink {
|
||||
pub icon: Icon,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Clone, Copy, Default)]
|
||||
pub struct Icon {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -11,12 +11,8 @@ use gpui::{
|
||||
};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use theme::{ColorScheme, Layer, Style, StyleSet};
|
||||
use workspace::{
|
||||
item::{Item, ItemEvent},
|
||||
register_deserializable_item, Pane, Workspace,
|
||||
};
|
||||
use workspace::{item::Item, register_deserializable_item, Pane, Workspace};
|
||||
|
||||
actions!(theme, [DeployThemeTestbench]);
|
||||
|
||||
@ -314,47 +310,6 @@ impl Item for ThemeTestbench {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
|
||||
|
||||
fn is_singleton(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn can_save(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
_: gpui::ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||
unreachable!("save should not have been called");
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: gpui::ModelHandle<Project>,
|
||||
_: std::path::PathBuf,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||
unreachable!("save_as should not have been called");
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
_: gpui::ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<gpui::anyhow::Result<()>> {
|
||||
gpui::Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn to_item_events(_: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some("ThemeTestBench")
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ pub enum Motion {
|
||||
Matching,
|
||||
FindForward { before: bool, text: Arc<str> },
|
||||
FindBackward { after: bool, text: Arc<str> },
|
||||
NextLineStart,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
@ -74,6 +75,7 @@ actions!(
|
||||
StartOfDocument,
|
||||
EndOfDocument,
|
||||
Matching,
|
||||
NextLineStart,
|
||||
]
|
||||
);
|
||||
impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
|
||||
@ -111,6 +113,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
|
||||
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
|
||||
);
|
||||
cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx))
|
||||
}
|
||||
|
||||
pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||
@ -138,15 +141,43 @@ pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||
impl Motion {
|
||||
pub fn linewise(&self) -> bool {
|
||||
use Motion::*;
|
||||
matches!(
|
||||
self,
|
||||
Down | Up | StartOfDocument | EndOfDocument | CurrentLine
|
||||
)
|
||||
match self {
|
||||
Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true,
|
||||
EndOfLine
|
||||
| NextWordEnd { .. }
|
||||
| Matching
|
||||
| FindForward { .. }
|
||||
| Left
|
||||
| Backspace
|
||||
| Right
|
||||
| StartOfLine
|
||||
| NextWordStart { .. }
|
||||
| PreviousWordStart { .. }
|
||||
| FirstNonWhitespace
|
||||
| FindBackward { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn infallible(&self) -> bool {
|
||||
use Motion::*;
|
||||
matches!(self, StartOfDocument | CurrentLine | EndOfDocument)
|
||||
match self {
|
||||
StartOfDocument | EndOfDocument | CurrentLine => true,
|
||||
Down
|
||||
| Up
|
||||
| EndOfLine
|
||||
| NextWordEnd { .. }
|
||||
| Matching
|
||||
| FindForward { .. }
|
||||
| Left
|
||||
| Backspace
|
||||
| Right
|
||||
| StartOfLine
|
||||
| NextWordStart { .. }
|
||||
| PreviousWordStart { .. }
|
||||
| FirstNonWhitespace
|
||||
| FindBackward { .. }
|
||||
| NextLineStart => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inclusive(&self) -> bool {
|
||||
@ -160,7 +191,8 @@ impl Motion {
|
||||
| EndOfLine
|
||||
| NextWordEnd { .. }
|
||||
| Matching
|
||||
| FindForward { .. } => true,
|
||||
| FindForward { .. }
|
||||
| NextLineStart => true,
|
||||
Left
|
||||
| Backspace
|
||||
| Right
|
||||
@ -214,6 +246,7 @@ impl Motion {
|
||||
find_backward(map, point, *after, text.clone(), times),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
|
||||
};
|
||||
|
||||
(new_point != point || infallible).then_some((new_point, goal))
|
||||
@ -543,3 +576,8 @@ fn find_backward(
|
||||
})
|
||||
.unwrap_or(from)
|
||||
}
|
||||
|
||||
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
let new_row = (point.row() + times as u32).min(map.max_buffer_row());
|
||||
map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
|
||||
}
|
||||
|
@ -473,6 +473,7 @@ pub(crate) fn normal_replace(text: Arc<str>, cx: &mut MutableAppContext) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
|
||||
use crate::{
|
||||
@ -515,15 +516,15 @@ mod test {
|
||||
.await;
|
||||
}
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_enter(cx: &mut gpui::TestAppContext) {
|
||||
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
|
||||
// cx.assert_all(indoc! {"
|
||||
// ˇThe qˇuick broˇwn
|
||||
// ˇfox jumps"
|
||||
// })
|
||||
// .await;
|
||||
// }
|
||||
#[gpui::test]
|
||||
async fn test_enter(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
|
||||
cx.assert_all(indoc! {"
|
||||
ˇThe qˇuick broˇwn
|
||||
ˇfox jumps"
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_k(cx: &mut gpui::TestAppContext) {
|
||||
@ -1030,7 +1031,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_percent(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_percent(cx: &mut TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["%"]);
|
||||
cx.assert_all("ˇconsole.logˇ(ˇvaˇrˇ)ˇ;").await;
|
||||
cx.assert_all("ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;")
|
||||
|
@ -73,34 +73,30 @@ impl VimState {
|
||||
|
||||
pub fn keymap_context_layer(&self) -> KeymapContext {
|
||||
let mut context = KeymapContext::default();
|
||||
context.map.insert(
|
||||
"vim_mode".to_string(),
|
||||
context.add_key(
|
||||
"vim_mode",
|
||||
match self.mode {
|
||||
Mode::Normal => "normal",
|
||||
Mode::Visual { .. } => "visual",
|
||||
Mode::Insert => "insert",
|
||||
}
|
||||
.to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
if self.vim_controlled() {
|
||||
context.set.insert("VimControl".to_string());
|
||||
context.add_identifier("VimControl");
|
||||
}
|
||||
|
||||
let active_operator = self.operator_stack.last();
|
||||
|
||||
if let Some(active_operator) = active_operator {
|
||||
for context_flag in active_operator.context_flags().into_iter() {
|
||||
context.set.insert(context_flag.to_string());
|
||||
context.add_identifier(*context_flag);
|
||||
}
|
||||
}
|
||||
|
||||
context.map.insert(
|
||||
"vim_operator".to_string(),
|
||||
active_operator
|
||||
.map(|op| op.id())
|
||||
.unwrap_or_else(|| "none")
|
||||
.to_string(),
|
||||
context.add_key(
|
||||
"vim_operator",
|
||||
active_operator.map(|op| op.id()).unwrap_or_else(|| "none"),
|
||||
);
|
||||
|
||||
context
|
||||
|
1
crates/vim/test_data/test_enter.json
Normal file
@ -0,0 +1 @@
|
||||
[{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"},{"Text":"The quick brown\nfox jumps"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
@ -49,9 +49,11 @@ pub trait Item: View {
|
||||
}
|
||||
fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
|
||||
-> ElementBox;
|
||||
fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item));
|
||||
fn is_singleton(&self, cx: &AppContext) -> bool;
|
||||
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
|
||||
fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
|
||||
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||
fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
@ -64,23 +66,31 @@ pub trait Item: View {
|
||||
fn has_conflict(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
fn can_save(&self, cx: &AppContext) -> bool;
|
||||
fn can_save(&self, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
fn save(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
_project: ModelHandle<Project>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unimplemented!("save() must be implemented if can_save() returns true")
|
||||
}
|
||||
fn save_as(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
abs_path: PathBuf,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
_project: ModelHandle<Project>,
|
||||
_abs_path: PathBuf,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unimplemented!("save_as() must be implemented if can_save() returns true")
|
||||
}
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
_project: ModelHandle<Project>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unimplemented!("reload() must be implemented if can_save() returns true")
|
||||
}
|
||||
fn git_diff_recalc(
|
||||
&mut self,
|
||||
_project: ModelHandle<Project>,
|
||||
@ -88,7 +98,9 @@ pub trait Item: View {
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]>;
|
||||
fn to_item_events(_event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
SmallVec::new()
|
||||
}
|
||||
fn should_close_item_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
@ -124,15 +136,21 @@ pub trait Item: View {
|
||||
|
||||
fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str>;
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: ModelHandle<Project>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
workspace_id: WorkspaceId,
|
||||
item_id: ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<ViewHandle<Self>>>;
|
||||
_project: ModelHandle<Project>,
|
||||
_workspace: WeakViewHandle<Workspace>,
|
||||
_workspace_id: WorkspaceId,
|
||||
_item_id: ItemId,
|
||||
_cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
unimplemented!(
|
||||
"deserialize() must be implemented if serialized_item_kind() returns Some(_)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ItemHandle: 'static + fmt::Debug {
|
||||
|
@ -21,6 +21,7 @@ use gpui::{
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
impl_actions, impl_internal_actions,
|
||||
keymap_matcher::KeymapContext,
|
||||
platform::{CursorStyle, NavigationDirection},
|
||||
Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
|
||||
ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
|
||||
@ -1550,6 +1551,14 @@ impl View for Pane {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut keymap = Self::default_keymap_context();
|
||||
if self.docked.is_some() {
|
||||
keymap.add_identifier("docked");
|
||||
}
|
||||
keymap
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_bar_button<A: Action>(
|
||||
|
@ -1,23 +1,18 @@
|
||||
use crate::{
|
||||
item::ItemEvent, persistence::model::ItemId, Item, ItemNavHistory, Pane, Workspace, WorkspaceId,
|
||||
item::{Item, ItemEvent},
|
||||
ItemNavHistory, WorkspaceId,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use call::participant::{Frame, RemoteVideoTrack};
|
||||
use client::{proto::PeerId, User};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
AppContext, Entity, ModelHandle, MouseButton, RenderContext, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
AppContext, Entity, MouseButton, RenderContext, Task, View, ViewContext,
|
||||
};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
pub enum Event {
|
||||
Close,
|
||||
@ -130,12 +125,6 @@ impl Item for SharedScreen {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn for_each_project_item(&self, _: &AppContext, _: &mut dyn FnMut(usize, &dyn project::Item)) {}
|
||||
|
||||
fn is_singleton(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
|
||||
self.nav_history = Some(history);
|
||||
}
|
||||
@ -149,52 +138,9 @@ impl Item for SharedScreen {
|
||||
Some(Self::new(&track, self.peer_id, self.user.clone(), cx))
|
||||
}
|
||||
|
||||
fn can_save(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
_: ModelHandle<project::Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow!("Item::save called on SharedScreen")))
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: ModelHandle<project::Project>,
|
||||
_: PathBuf,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow!("Item::save_as called on SharedScreen")))
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
_: ModelHandle<project::Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow!("Item::reload called on SharedScreen")))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
match event {
|
||||
Event::Close => smallvec::smallvec!(ItemEvent::CloseItem),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
_project: ModelHandle<Project>,
|
||||
_workspace: WeakViewHandle<Workspace>,
|
||||
_workspace_id: WorkspaceId,
|
||||
_item_id: ItemId,
|
||||
_cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
unreachable!("Shared screen can not be deserialized")
|
||||
}
|
||||
}
|
||||
|
@ -837,7 +837,7 @@ impl Workspace {
|
||||
&self.project
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &Arc<Client> {
|
||||
pub fn client(&self) -> &Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
@ -1589,13 +1589,17 @@ impl Workspace {
|
||||
}
|
||||
|
||||
let item = pane.read(cx).active_item()?;
|
||||
let new_pane = self.add_pane(cx);
|
||||
if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
|
||||
Pane::add_item(self, &new_pane, clone, true, true, None, cx);
|
||||
}
|
||||
self.center.split(&pane, &new_pane, direction).unwrap();
|
||||
let maybe_pane_handle =
|
||||
if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
|
||||
let new_pane = self.add_pane(cx);
|
||||
Pane::add_item(self, &new_pane, clone, true, true, None, cx);
|
||||
self.center.split(&pane, &new_pane, direction).unwrap();
|
||||
Some(new_pane)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
cx.notify();
|
||||
Some(new_pane)
|
||||
maybe_pane_handle
|
||||
}
|
||||
|
||||
pub fn split_pane_with_item(&mut self, action: &SplitWithItem, cx: &mut ViewContext<Self>) {
|
||||
@ -1828,24 +1832,15 @@ impl Workspace {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_following(&self, peer_id: PeerId) -> bool {
|
||||
pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
|
||||
self.follower_states_by_leader.contains_key(&peer_id)
|
||||
}
|
||||
|
||||
pub fn is_followed(&self, peer_id: PeerId) -> bool {
|
||||
pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
|
||||
self.leader_state.followers.contains(&peer_id)
|
||||
}
|
||||
|
||||
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let project = &self.project.read(cx);
|
||||
let mut worktree_root_names = String::new();
|
||||
for (i, name) in project.worktree_root_names(cx).enumerate() {
|
||||
if i > 0 {
|
||||
worktree_root_names.push_str(", ");
|
||||
}
|
||||
worktree_root_names.push_str(name);
|
||||
}
|
||||
|
||||
// TODO: There should be a better system in place for this
|
||||
// (https://github.com/zed-industries/zed/issues/1290)
|
||||
let is_fullscreen = cx.window_is_fullscreen(cx.window_id());
|
||||
@ -1862,16 +1857,10 @@ impl Workspace {
|
||||
MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
|
||||
Container::new(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(
|
||||
self.titlebar_item
|
||||
.as_ref()
|
||||
.map(|item| ChildView::new(item, cx).aligned().right().boxed()),
|
||||
.map(|item| ChildView::new(item, cx).boxed()),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
@ -2727,11 +2716,7 @@ impl View for Workspace {
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut keymap = Self::default_keymap_context();
|
||||
if self.active_pane() == self.dock_pane() {
|
||||
keymap.set.insert("Dock".into());
|
||||
}
|
||||
keymap
|
||||
Self::default_keymap_context()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.75.0"
|
||||
version = "0.76.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 226 KiB |
Before Width: | Height: | Size: 320 KiB After Width: | Height: | Size: 739 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 164 KiB |
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 442 KiB |
@ -1,4 +1,4 @@
|
||||
use gpui::{Menu, MenuItem};
|
||||
use gpui::{Menu, MenuItem, OsAction};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn menus() -> Vec<Menu<'static>> {
|
||||
@ -6,363 +6,159 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||
Menu {
|
||||
name: "Zed",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "About Zed…",
|
||||
action: Box::new(super::About),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Check for Updates",
|
||||
action: Box::new(auto_update::Check),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Submenu(Menu {
|
||||
MenuItem::action("About Zed…", super::About),
|
||||
MenuItem::action("Check for Updates", auto_update::Check),
|
||||
MenuItem::separator(),
|
||||
MenuItem::submenu(Menu {
|
||||
name: "Preferences",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "Open Settings",
|
||||
action: Box::new(super::OpenSettings),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Open Key Bindings",
|
||||
action: Box::new(super::OpenKeymap),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Open Default Settings",
|
||||
action: Box::new(super::OpenDefaultSettings),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Open Default Key Bindings",
|
||||
action: Box::new(super::OpenDefaultKeymap),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Select Theme",
|
||||
action: Box::new(theme_selector::Toggle),
|
||||
},
|
||||
MenuItem::action("Open Settings", super::OpenSettings),
|
||||
MenuItem::action("Open Key Bindings", super::OpenKeymap),
|
||||
MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
|
||||
MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap),
|
||||
MenuItem::action("Select Theme", theme_selector::Toggle),
|
||||
],
|
||||
}),
|
||||
MenuItem::Action {
|
||||
name: "Install CLI",
|
||||
action: Box::new(super::InstallCommandLineInterface),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Hide Zed",
|
||||
action: Box::new(super::Hide),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Hide Others",
|
||||
action: Box::new(super::HideOthers),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Show All",
|
||||
action: Box::new(super::ShowAll),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Quit",
|
||||
action: Box::new(super::Quit),
|
||||
},
|
||||
MenuItem::action("Install CLI", super::InstallCommandLineInterface),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Hide Zed", super::Hide),
|
||||
MenuItem::action("Hide Others", super::HideOthers),
|
||||
MenuItem::action("Show All", super::ShowAll),
|
||||
MenuItem::action("Quit", super::Quit),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "File",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "New",
|
||||
action: Box::new(workspace::NewFile),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "New Window",
|
||||
action: Box::new(workspace::NewWindow),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Open…",
|
||||
action: Box::new(workspace::Open),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Open Recent...",
|
||||
action: Box::new(recent_projects::OpenRecent),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Add Folder to Project…",
|
||||
action: Box::new(workspace::AddFolderToProject),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Save",
|
||||
action: Box::new(workspace::Save),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Save As…",
|
||||
action: Box::new(workspace::SaveAs),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Save All",
|
||||
action: Box::new(workspace::SaveAll),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Close Editor",
|
||||
action: Box::new(workspace::CloseActiveItem),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Close Window",
|
||||
action: Box::new(workspace::CloseWindow),
|
||||
},
|
||||
MenuItem::action("New", workspace::NewFile),
|
||||
MenuItem::action("New Window", workspace::NewWindow),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Open…", workspace::Open),
|
||||
MenuItem::action("Open Recent...", recent_projects::OpenRecent),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject),
|
||||
MenuItem::action("Save", workspace::Save),
|
||||
MenuItem::action("Save As…", workspace::SaveAs),
|
||||
MenuItem::action("Save All", workspace::SaveAll),
|
||||
MenuItem::action("Close Editor", workspace::CloseActiveItem),
|
||||
MenuItem::action("Close Window", workspace::CloseWindow),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Edit",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "Undo",
|
||||
action: Box::new(editor::Undo),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Redo",
|
||||
action: Box::new(editor::Redo),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Cut",
|
||||
action: Box::new(editor::Cut),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Copy",
|
||||
action: Box::new(editor::Copy),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Paste",
|
||||
action: Box::new(editor::Paste),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Find",
|
||||
action: Box::new(search::buffer_search::Deploy { focus: true }),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Find In Project",
|
||||
action: Box::new(workspace::NewSearch),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Toggle Line Comment",
|
||||
action: Box::new(editor::ToggleComments::default()),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Emoji & Symbols",
|
||||
action: Box::new(editor::ShowCharacterPalette),
|
||||
},
|
||||
MenuItem::os_action("Undo", editor::Undo, OsAction::Undo),
|
||||
MenuItem::os_action("Redo", editor::Redo, OsAction::Redo),
|
||||
MenuItem::separator(),
|
||||
MenuItem::os_action("Cut", editor::Cut, OsAction::Cut),
|
||||
MenuItem::os_action("Copy", editor::Copy, OsAction::Copy),
|
||||
MenuItem::os_action("Paste", editor::Paste, OsAction::Paste),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Find", search::buffer_search::Deploy { focus: true }),
|
||||
MenuItem::action("Find In Project", workspace::NewSearch),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Toggle Line Comment", editor::ToggleComments::default()),
|
||||
MenuItem::action("Emoji & Symbols", editor::ShowCharacterPalette),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Selection",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "Select All",
|
||||
action: Box::new(editor::SelectAll),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Expand Selection",
|
||||
action: Box::new(editor::SelectLargerSyntaxNode),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Shrink Selection",
|
||||
action: Box::new(editor::SelectSmallerSyntaxNode),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Add Cursor Above",
|
||||
action: Box::new(editor::AddSelectionAbove),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Add Cursor Below",
|
||||
action: Box::new(editor::AddSelectionBelow),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Select Next Occurrence",
|
||||
action: Box::new(editor::SelectNext {
|
||||
MenuItem::os_action("Select All", editor::SelectAll, OsAction::SelectAll),
|
||||
MenuItem::action("Expand Selection", editor::SelectLargerSyntaxNode),
|
||||
MenuItem::action("Shrink Selection", editor::SelectSmallerSyntaxNode),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Add Cursor Above", editor::AddSelectionAbove),
|
||||
MenuItem::action("Add Cursor Below", editor::AddSelectionBelow),
|
||||
MenuItem::action(
|
||||
"Select Next Occurrence",
|
||||
editor::SelectNext {
|
||||
replace_newest: false,
|
||||
}),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Move Line Up",
|
||||
action: Box::new(editor::MoveLineUp),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Move Line Down",
|
||||
action: Box::new(editor::MoveLineDown),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Duplicate Selection",
|
||||
action: Box::new(editor::DuplicateLine),
|
||||
},
|
||||
},
|
||||
),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Move Line Up", editor::MoveLineUp),
|
||||
MenuItem::action("Move Line Down", editor::MoveLineDown),
|
||||
MenuItem::action("Duplicate Selection", editor::DuplicateLine),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "View",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "Zoom In",
|
||||
action: Box::new(super::IncreaseBufferFontSize),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Zoom Out",
|
||||
action: Box::new(super::DecreaseBufferFontSize),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Reset Zoom",
|
||||
action: Box::new(super::ResetBufferFontSize),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Toggle Left Sidebar",
|
||||
action: Box::new(workspace::ToggleLeftSidebar),
|
||||
},
|
||||
MenuItem::Submenu(Menu {
|
||||
MenuItem::action("Zoom In", super::IncreaseBufferFontSize),
|
||||
MenuItem::action("Zoom Out", super::DecreaseBufferFontSize),
|
||||
MenuItem::action("Reset Zoom", super::ResetBufferFontSize),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Toggle Left Sidebar", workspace::ToggleLeftSidebar),
|
||||
MenuItem::submenu(Menu {
|
||||
name: "Editor Layout",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "Split Up",
|
||||
action: Box::new(workspace::SplitUp),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Split Down",
|
||||
action: Box::new(workspace::SplitDown),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Split Left",
|
||||
action: Box::new(workspace::SplitLeft),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Split Right",
|
||||
action: Box::new(workspace::SplitRight),
|
||||
},
|
||||
MenuItem::action("Split Up", workspace::SplitUp),
|
||||
MenuItem::action("Split Down", workspace::SplitDown),
|
||||
MenuItem::action("Split Left", workspace::SplitLeft),
|
||||
MenuItem::action("Split Right", workspace::SplitRight),
|
||||
],
|
||||
}),
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Project Panel",
|
||||
action: Box::new(project_panel::ToggleFocus),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Command Palette",
|
||||
action: Box::new(command_palette::Toggle),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Diagnostics",
|
||||
action: Box::new(diagnostics::Deploy),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Project Panel", project_panel::ToggleFocus),
|
||||
MenuItem::action("Command Palette", command_palette::Toggle),
|
||||
MenuItem::action("Diagnostics", diagnostics::Deploy),
|
||||
MenuItem::separator(),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Go",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "Back",
|
||||
action: Box::new(workspace::GoBack { pane: None }),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Forward",
|
||||
action: Box::new(workspace::GoForward { pane: None }),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Go to File",
|
||||
action: Box::new(file_finder::Toggle),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Go to Symbol in Project",
|
||||
action: Box::new(project_symbols::Toggle),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Go to Symbol in Editor",
|
||||
action: Box::new(outline::Toggle),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Go to Definition",
|
||||
action: Box::new(editor::GoToDefinition),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Go to Type Definition",
|
||||
action: Box::new(editor::GoToTypeDefinition),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Find All References",
|
||||
action: Box::new(editor::FindAllReferences),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Go to Line/Column",
|
||||
action: Box::new(go_to_line::Toggle),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Next Problem",
|
||||
action: Box::new(editor::GoToDiagnostic),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Previous Problem",
|
||||
action: Box::new(editor::GoToPrevDiagnostic),
|
||||
},
|
||||
MenuItem::action("Back", workspace::GoBack { pane: None }),
|
||||
MenuItem::action("Forward", workspace::GoForward { pane: None }),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Go to File", file_finder::Toggle),
|
||||
MenuItem::action("Go to Symbol in Project", project_symbols::Toggle),
|
||||
MenuItem::action("Go to Symbol in Editor", outline::Toggle),
|
||||
MenuItem::action("Go to Definition", editor::GoToDefinition),
|
||||
MenuItem::action("Go to Type Definition", editor::GoToTypeDefinition),
|
||||
MenuItem::action("Find All References", editor::FindAllReferences),
|
||||
MenuItem::action("Go to Line/Column", go_to_line::Toggle),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Next Problem", editor::GoToDiagnostic),
|
||||
MenuItem::action("Previous Problem", editor::GoToPrevDiagnostic),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Window",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "Minimize",
|
||||
action: Box::new(super::Minimize),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Zoom",
|
||||
action: Box::new(super::Zoom),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::action("Minimize", super::Minimize),
|
||||
MenuItem::action("Zoom", super::Zoom),
|
||||
MenuItem::separator(),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Help",
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "Command Palette",
|
||||
action: Box::new(command_palette::Toggle),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "View Telemetry Log",
|
||||
action: Box::new(crate::OpenTelemetryLog),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "View Dependency Licenses",
|
||||
action: Box::new(crate::OpenLicenses),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Copy System Specs Into Clipboard",
|
||||
action: Box::new(feedback::CopySystemSpecsIntoClipboard),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "File Bug Report",
|
||||
action: Box::new(feedback::FileBugReport),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Request Feature",
|
||||
action: Box::new(feedback::RequestFeature),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Documentation",
|
||||
action: Box::new(crate::OpenBrowser {
|
||||
MenuItem::action("Command Palette", command_palette::Toggle),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("View Telemetry Log", crate::OpenTelemetryLog),
|
||||
MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action(
|
||||
"Copy System Specs Into Clipboard",
|
||||
feedback::CopySystemSpecsIntoClipboard,
|
||||
),
|
||||
MenuItem::action("File Bug Report", feedback::FileBugReport),
|
||||
MenuItem::action("Request Feature", feedback::RequestFeature),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action(
|
||||
"Documentation",
|
||||
crate::OpenBrowser {
|
||||
url: "https://zed.dev/docs".into(),
|
||||
}),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Zed Twitter",
|
||||
action: Box::new(crate::OpenBrowser {
|
||||
},
|
||||
),
|
||||
MenuItem::action(
|
||||
"Zed Twitter",
|
||||
crate::OpenBrowser {
|
||||
url: "https://twitter.com/zeddotdev".into(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
@ -6,7 +6,7 @@ use anyhow::{anyhow, Context, Result};
|
||||
use assets::Assets;
|
||||
use breadcrumbs::Breadcrumbs;
|
||||
pub use client;
|
||||
use collab_ui::{CollabTitlebarItem, ToggleCollaborationMenu};
|
||||
use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
|
||||
use collections::VecDeque;
|
||||
pub use editor;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
@ -99,9 +99,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace,
|
||||
_: &ToggleCollaborationMenu,
|
||||
cx: &mut ViewContext<Workspace>| {
|
||||
|workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext<Workspace>| {
|
||||
if let Some(item) = workspace
|
||||
.titlebar_item()
|
||||
.and_then(|item| item.downcast::<CollabTitlebarItem>())
|
||||
|
2
styles/.prettierignore
Normal file
@ -0,0 +1,2 @@
|
||||
package-lock.json
|
||||
package.json
|
229
styles/package-lock.json
generated
@ -9,67 +9,83 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/chroma-js": "^2.1.3",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/chroma-js": "^2.4.0",
|
||||
"@types/node": "^18.14.1",
|
||||
"bezier-easing": "^2.1.0",
|
||||
"case-anything": "^2.1.10",
|
||||
"chroma-js": "^2.4.2",
|
||||
"deepmerge": "^4.3.0",
|
||||
"toml": "^3.0.0",
|
||||
"ts-node": "^10.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-consumer": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
|
||||
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
|
||||
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-consumer": "0.8.0"
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
|
||||
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
|
||||
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
|
||||
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
|
||||
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
|
||||
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ=="
|
||||
},
|
||||
"node_modules/@types/chroma-js": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
|
||||
"integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz",
|
||||
"integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "17.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
|
||||
"integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
|
||||
"version": "18.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
|
||||
"integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
||||
"version": "8.8.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
|
||||
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -90,6 +106,11 @@
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||
},
|
||||
"node_modules/bezier-easing": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
|
||||
"integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
|
||||
},
|
||||
"node_modules/case-anything": {
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
|
||||
@ -111,6 +132,14 @@
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
|
||||
"integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@ -130,11 +159,11 @@
|
||||
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.7.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
|
||||
"integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "0.7.0",
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
@ -145,7 +174,7 @@
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.0",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
@ -172,9 +201,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
|
||||
"integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@ -185,9 +214,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
|
||||
"integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
@ -199,53 +228,67 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-consumer": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
|
||||
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg=="
|
||||
},
|
||||
"@cspotcode/source-map-support": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
|
||||
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"requires": {
|
||||
"@cspotcode/source-map-consumer": "0.8.0"
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
}
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
||||
},
|
||||
"@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"requires": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"@tsconfig/node10": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
|
||||
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
|
||||
},
|
||||
"@tsconfig/node12": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
|
||||
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
|
||||
},
|
||||
"@tsconfig/node14": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
|
||||
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
|
||||
},
|
||||
"@tsconfig/node16": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
|
||||
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
|
||||
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ=="
|
||||
},
|
||||
"@types/chroma-js": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
|
||||
"integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz",
|
||||
"integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "17.0.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
|
||||
"integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
|
||||
"version": "18.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
|
||||
"integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ=="
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
|
||||
"version": "8.8.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
|
||||
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw=="
|
||||
},
|
||||
"acorn-walk": {
|
||||
"version": "8.2.0",
|
||||
@ -257,6 +300,11 @@
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||
},
|
||||
"bezier-easing": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
|
||||
"integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
|
||||
},
|
||||
"case-anything": {
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
|
||||
@ -272,6 +320,11 @@
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||
},
|
||||
"deepmerge": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz",
|
||||
"integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og=="
|
||||
},
|
||||
"diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@ -288,11 +341,11 @@
|
||||
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.7.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
|
||||
"integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
|
||||
"requires": {
|
||||
"@cspotcode/source-map-support": "0.7.0",
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
@ -303,20 +356,20 @@
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.0",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
|
||||
"integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"peer": true
|
||||
},
|
||||
"v8-compile-cache-lib": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
|
||||
"integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
|
||||
},
|
||||
"yn": {
|
||||
"version": "3.1.1",
|
||||
|
@ -10,11 +10,19 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/chroma-js": "^2.1.3",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/chroma-js": "^2.4.0",
|
||||
"@types/node": "^18.14.1",
|
||||
"bezier-easing": "^2.1.0",
|
||||
"case-anything": "^2.1.10",
|
||||
"chroma-js": "^2.4.2",
|
||||
"deepmerge": "^4.3.0",
|
||||
"toml": "^3.0.0",
|
||||
"ts-node": "^10.7.0"
|
||||
"ts-node": "^10.9.1"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"printWidth": 80,
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
|
@ -1,73 +1,87 @@
|
||||
import * as fs from "fs";
|
||||
import toml from "toml";
|
||||
import {
|
||||
schemeMeta
|
||||
} from "./colorSchemes";
|
||||
import { Meta } from "./themes/common/colorScheme";
|
||||
import https from "https";
|
||||
import crypto from "crypto";
|
||||
import * as fs from "fs"
|
||||
import toml from "toml"
|
||||
import { schemeMeta } from "./colorSchemes"
|
||||
import { Meta } from "./themes/common/colorScheme"
|
||||
import https from "https"
|
||||
import crypto from "crypto"
|
||||
|
||||
const accepted_licenses_file = `${__dirname}/../../script/licenses/zed-licenses.toml`
|
||||
|
||||
// Use the cargo-about configuration file as the source of truth for supported licenses.
|
||||
function parseAcceptedToml(file: string): string[] {
|
||||
let buffer = fs.readFileSync(file).toString();
|
||||
let buffer = fs.readFileSync(file).toString()
|
||||
|
||||
let obj = toml.parse(buffer);
|
||||
let obj = toml.parse(buffer)
|
||||
|
||||
if (!Array.isArray(obj.accepted)) {
|
||||
throw Error("Accepted license source is malformed")
|
||||
}
|
||||
if (!Array.isArray(obj.accepted)) {
|
||||
throw Error("Accepted license source is malformed")
|
||||
}
|
||||
|
||||
return obj.accepted
|
||||
return obj.accepted
|
||||
}
|
||||
|
||||
|
||||
function checkLicenses(schemeMeta: Meta[], licenses: string[]) {
|
||||
for (let meta of schemeMeta) {
|
||||
// FIXME: Add support for conjuctions and conditions
|
||||
if (licenses.indexOf(meta.license.SPDX) < 0) {
|
||||
throw Error(`License for theme ${meta.name} (${meta.license.SPDX}) is not supported`)
|
||||
for (let meta of schemeMeta) {
|
||||
// FIXME: Add support for conjuctions and conditions
|
||||
if (licenses.indexOf(meta.license.SPDX) < 0) {
|
||||
throw Error(
|
||||
`License for theme ${meta.name} (${meta.license.SPDX}) is not supported`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLicenseText(
|
||||
schemeMeta: Meta[],
|
||||
callback: (meta: Meta, license_text: string) => void
|
||||
) {
|
||||
for (let meta of schemeMeta) {
|
||||
// The following copied from the example code on nodejs.org:
|
||||
// https://nodejs.org/api/http.html#httpgetoptions-callback
|
||||
https
|
||||
.get(meta.license.https_url, (res) => {
|
||||
const { statusCode } = res
|
||||
|
||||
function getLicenseText(schemeMeta: Meta[], callback: (meta: Meta, license_text: string) => void) {
|
||||
for (let meta of schemeMeta) {
|
||||
// The following copied from the example code on nodejs.org:
|
||||
// https://nodejs.org/api/http.html#httpgetoptions-callback
|
||||
https.get(meta.license.https_url, (res) => {
|
||||
const { statusCode } = res;
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
throw new Error(
|
||||
`Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}`
|
||||
)
|
||||
}
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
throw new Error(`Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}`);
|
||||
}
|
||||
|
||||
res.setEncoding('utf8');
|
||||
let rawData = '';
|
||||
res.on('data', (chunk) => { rawData += chunk; });
|
||||
res.on('end', () => {
|
||||
const hash = crypto.createHash('sha256').update(rawData).digest('hex');
|
||||
if (meta.license.license_checksum == hash) {
|
||||
callback(meta, rawData)
|
||||
} else {
|
||||
throw Error(`Checksum for ${meta.name} did not match file downloaded from ${meta.license.https_url}`)
|
||||
}
|
||||
});
|
||||
}).on('error', (e) => {
|
||||
throw e
|
||||
});
|
||||
}
|
||||
res.setEncoding("utf8")
|
||||
let rawData = ""
|
||||
res.on("data", (chunk) => {
|
||||
rawData += chunk
|
||||
})
|
||||
res.on("end", () => {
|
||||
const hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(rawData)
|
||||
.digest("hex")
|
||||
if (meta.license.license_checksum == hash) {
|
||||
callback(meta, rawData)
|
||||
} else {
|
||||
throw Error(
|
||||
`Checksum for ${meta.name} did not match file downloaded from ${meta.license.https_url}`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
.on("error", (e) => {
|
||||
throw e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function writeLicense(schemeMeta: Meta, text: String) {
|
||||
process.stdout.write(`## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n`)
|
||||
process.stdout.write(
|
||||
`## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n`
|
||||
)
|
||||
}
|
||||
|
||||
const accepted_licenses = parseAcceptedToml(accepted_licenses_file);
|
||||
const accepted_licenses = parseAcceptedToml(accepted_licenses_file)
|
||||
checkLicenses(schemeMeta, accepted_licenses)
|
||||
|
||||
getLicenseText(schemeMeta, (meta, text) => {
|
||||
writeLicense(meta, text)
|
||||
});
|
||||
writeLicense(meta, text)
|
||||
})
|
||||
|
@ -1,50 +1,52 @@
|
||||
import * as fs from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import * as path from "path";
|
||||
import colorSchemes, {
|
||||
staffColorSchemes,
|
||||
} from "./colorSchemes";
|
||||
import app from "./styleTree/app";
|
||||
import { ColorScheme } from "./themes/common/colorScheme";
|
||||
import snakeCase from "./utils/snakeCase";
|
||||
import * as fs from "fs"
|
||||
import { tmpdir } from "os"
|
||||
import * as path from "path"
|
||||
import colorSchemes, { staffColorSchemes } from "./colorSchemes"
|
||||
import app from "./styleTree/app"
|
||||
import { ColorScheme } from "./themes/common/colorScheme"
|
||||
import snakeCase from "./utils/snakeCase"
|
||||
|
||||
const assetsDirectory = `${__dirname}/../../assets`
|
||||
const themeDirectory = `${assetsDirectory}/themes`;
|
||||
const staffDirectory = `${themeDirectory}/staff`;
|
||||
const themeDirectory = `${assetsDirectory}/themes`
|
||||
const staffDirectory = `${themeDirectory}/staff`
|
||||
|
||||
const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"));
|
||||
const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
|
||||
|
||||
// Clear existing themes
|
||||
function clearThemes(themeDirectory: string) {
|
||||
if (!fs.existsSync(themeDirectory)) {
|
||||
fs.mkdirSync(themeDirectory, { recursive: true });
|
||||
} else {
|
||||
for (const file of fs.readdirSync(themeDirectory)) {
|
||||
if (file.endsWith(".json")) {
|
||||
const name = file.replace(/\.json$/, "");
|
||||
if (!colorSchemes.find((colorScheme) => colorScheme.name === name)) {
|
||||
fs.unlinkSync(path.join(themeDirectory, file));
|
||||
if (!fs.existsSync(themeDirectory)) {
|
||||
fs.mkdirSync(themeDirectory, { recursive: true })
|
||||
} else {
|
||||
for (const file of fs.readdirSync(themeDirectory)) {
|
||||
if (file.endsWith(".json")) {
|
||||
const name = file.replace(/\.json$/, "")
|
||||
if (
|
||||
!colorSchemes.find(
|
||||
(colorScheme) => colorScheme.name === name
|
||||
)
|
||||
) {
|
||||
fs.unlinkSync(path.join(themeDirectory, file))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearThemes(themeDirectory);
|
||||
clearThemes(staffDirectory);
|
||||
clearThemes(themeDirectory)
|
||||
clearThemes(staffDirectory)
|
||||
|
||||
function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) {
|
||||
for (let colorScheme of colorSchemes) {
|
||||
let styleTree = snakeCase(app(colorScheme));
|
||||
let styleTreeJSON = JSON.stringify(styleTree, null, 2);
|
||||
let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`);
|
||||
let outPath = path.join(outputDirectory, `${colorScheme.name}.json`);
|
||||
fs.writeFileSync(tempPath, styleTreeJSON);
|
||||
fs.renameSync(tempPath, outPath);
|
||||
console.log(`- ${outPath} created`);
|
||||
}
|
||||
for (let colorScheme of colorSchemes) {
|
||||
let styleTree = snakeCase(app(colorScheme))
|
||||
let styleTreeJSON = JSON.stringify(styleTree, null, 2)
|
||||
let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`)
|
||||
let outPath = path.join(outputDirectory, `${colorScheme.name}.json`)
|
||||
fs.writeFileSync(tempPath, styleTreeJSON)
|
||||
fs.renameSync(tempPath, outPath)
|
||||
console.log(`- ${outPath} created`)
|
||||
}
|
||||
}
|
||||
|
||||
// Write new themes to theme directory
|
||||
writeThemes(colorSchemes, themeDirectory);
|
||||
writeThemes(staffColorSchemes, staffDirectory);
|
||||
writeThemes(colorSchemes, themeDirectory)
|
||||
writeThemes(staffColorSchemes, staffDirectory)
|
||||
|
@ -1,54 +1,54 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { ColorScheme, Meta } from "./themes/common/colorScheme";
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { ColorScheme, Meta } from "./themes/common/colorScheme"
|
||||
|
||||
const colorSchemes: ColorScheme[] = [];
|
||||
export default colorSchemes;
|
||||
const colorSchemes: ColorScheme[] = []
|
||||
export default colorSchemes
|
||||
|
||||
const schemeMeta: Meta[] = [];
|
||||
export { schemeMeta };
|
||||
const schemeMeta: Meta[] = []
|
||||
export { schemeMeta }
|
||||
|
||||
const staffColorSchemes: ColorScheme[] = [];
|
||||
export { staffColorSchemes };
|
||||
const staffColorSchemes: ColorScheme[] = []
|
||||
export { staffColorSchemes }
|
||||
|
||||
const experimentalColorSchemes: ColorScheme[] = [];
|
||||
export { experimentalColorSchemes };
|
||||
const experimentalColorSchemes: ColorScheme[] = []
|
||||
export { experimentalColorSchemes }
|
||||
|
||||
const themes_directory = path.resolve(`${__dirname}/themes`);
|
||||
const themes_directory = path.resolve(`${__dirname}/themes`)
|
||||
|
||||
function for_all_color_schemes_in(themesPath: string, callback: (module: any, path: string) => void) {
|
||||
for (const fileName of fs.readdirSync(themesPath)) {
|
||||
if (fileName == "template.ts") continue;
|
||||
const filePath = path.join(themesPath, fileName);
|
||||
function for_all_color_schemes_in(
|
||||
themesPath: string,
|
||||
callback: (module: any, path: string) => void
|
||||
) {
|
||||
for (const fileName of fs.readdirSync(themesPath)) {
|
||||
if (fileName == "template.ts") continue
|
||||
const filePath = path.join(themesPath, fileName)
|
||||
|
||||
if (fs.statSync(filePath).isFile()) {
|
||||
const colorScheme = require(filePath);
|
||||
callback(colorScheme, path.basename(filePath));
|
||||
if (fs.statSync(filePath).isFile()) {
|
||||
const colorScheme = require(filePath)
|
||||
callback(colorScheme, path.basename(filePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) {
|
||||
for_all_color_schemes_in(themesPath, (colorScheme, _path) => {
|
||||
if (colorScheme.dark) colorSchemes.push(colorScheme.dark);
|
||||
if (colorScheme.light) colorSchemes.push(colorScheme.light);
|
||||
})
|
||||
for_all_color_schemes_in(themesPath, (colorScheme, _path) => {
|
||||
if (colorScheme.dark) colorSchemes.push(colorScheme.dark)
|
||||
if (colorScheme.light) colorSchemes.push(colorScheme.light)
|
||||
})
|
||||
}
|
||||
|
||||
fillColorSchemes(themes_directory, colorSchemes);
|
||||
fillColorSchemes(
|
||||
path.resolve(`${themes_directory}/staff`),
|
||||
staffColorSchemes
|
||||
);
|
||||
fillColorSchemes(themes_directory, colorSchemes)
|
||||
fillColorSchemes(path.resolve(`${themes_directory}/staff`), staffColorSchemes)
|
||||
|
||||
function fillMeta(themesPath: string, meta: Meta[]) {
|
||||
for_all_color_schemes_in(themesPath, (colorScheme, path) => {
|
||||
if (colorScheme.meta) {
|
||||
meta.push(colorScheme.meta)
|
||||
} else {
|
||||
throw Error(`Public theme ${path} must have a meta field`)
|
||||
}
|
||||
})
|
||||
for_all_color_schemes_in(themesPath, (colorScheme, path) => {
|
||||
if (colorScheme.meta) {
|
||||
meta.push(colorScheme.meta)
|
||||
} else {
|
||||
throw Error(`Public theme ${path} must have a meta field`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fillMeta(themes_directory, schemeMeta);
|
||||
fillMeta(themes_directory, schemeMeta)
|
||||
|
@ -1,66 +1,45 @@
|
||||
export const fontFamilies = {
|
||||
sans: "Zed Sans",
|
||||
mono: "Zed Mono",
|
||||
};
|
||||
sans: "Zed Sans",
|
||||
mono: "Zed Mono",
|
||||
}
|
||||
|
||||
export const fontSizes = {
|
||||
"3xs": 8,
|
||||
"2xs": 10,
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 18,
|
||||
xl: 20,
|
||||
};
|
||||
"3xs": 8,
|
||||
"2xs": 10,
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 18,
|
||||
xl: 20,
|
||||
}
|
||||
|
||||
export type FontWeight =
|
||||
| "thin"
|
||||
| "extra_light"
|
||||
| "light"
|
||||
| "normal"
|
||||
| "medium"
|
||||
| "semibold"
|
||||
| "bold"
|
||||
| "extra_bold"
|
||||
| "black";
|
||||
| "thin"
|
||||
| "extra_light"
|
||||
| "light"
|
||||
| "normal"
|
||||
| "medium"
|
||||
| "semibold"
|
||||
| "bold"
|
||||
| "extra_bold"
|
||||
| "black"
|
||||
export const fontWeights: { [key: string]: FontWeight } = {
|
||||
thin: "thin",
|
||||
extra_light: "extra_light",
|
||||
light: "light",
|
||||
normal: "normal",
|
||||
medium: "medium",
|
||||
semibold: "semibold",
|
||||
bold: "bold",
|
||||
extra_bold: "extra_bold",
|
||||
black: "black",
|
||||
};
|
||||
thin: "thin",
|
||||
extra_light: "extra_light",
|
||||
light: "light",
|
||||
normal: "normal",
|
||||
medium: "medium",
|
||||
semibold: "semibold",
|
||||
bold: "bold",
|
||||
extra_bold: "extra_bold",
|
||||
black: "black",
|
||||
}
|
||||
|
||||
export const sizes = {
|
||||
px: 1,
|
||||
xs: 2,
|
||||
sm: 4,
|
||||
md: 6,
|
||||
lg: 8,
|
||||
xl: 12,
|
||||
};
|
||||
|
||||
// export const colors = {
|
||||
// neutral: colorRamp(["white", "black"], { steps: 37, increment: 25 }), // (900/25) + 1
|
||||
// rose: colorRamp("#F43F5EFF"),
|
||||
// red: colorRamp("#EF4444FF"),
|
||||
// orange: colorRamp("#F97316FF"),
|
||||
// amber: colorRamp("#F59E0BFF"),
|
||||
// yellow: colorRamp("#EAB308FF"),
|
||||
// lime: colorRamp("#84CC16FF"),
|
||||
// green: colorRamp("#22C55EFF"),
|
||||
// emerald: colorRamp("#10B981FF"),
|
||||
// teal: colorRamp("#14B8A6FF"),
|
||||
// cyan: colorRamp("#06BBD4FF"),
|
||||
// sky: colorRamp("#0EA5E9FF"),
|
||||
// blue: colorRamp("#3B82F6FF"),
|
||||
// indigo: colorRamp("#6366F1FF"),
|
||||
// violet: colorRamp("#8B5CF6FF"),
|
||||
// purple: colorRamp("#A855F7FF"),
|
||||
// fuschia: colorRamp("#D946E4FF"),
|
||||
// pink: colorRamp("#EC4899FF"),
|
||||
// }
|
||||
px: 1,
|
||||
xs: 2,
|
||||
sm: 4,
|
||||
md: 6,
|
||||
lg: 8,
|
||||
xl: 12,
|
||||
}
|
||||
|
@ -1,72 +1,72 @@
|
||||
import { text } from "./components";
|
||||
import contactFinder from "./contactFinder";
|
||||
import contactsPopover from "./contactsPopover";
|
||||
import commandPalette from "./commandPalette";
|
||||
import editor from "./editor";
|
||||
import projectPanel from "./projectPanel";
|
||||
import search from "./search";
|
||||
import picker from "./picker";
|
||||
import workspace from "./workspace";
|
||||
import contextMenu from "./contextMenu";
|
||||
import sharedScreen from "./sharedScreen";
|
||||
import projectDiagnostics from "./projectDiagnostics";
|
||||
import contactNotification from "./contactNotification";
|
||||
import updateNotification from "./updateNotification";
|
||||
import simpleMessageNotification from "./simpleMessageNotification";
|
||||
import projectSharedNotification from "./projectSharedNotification";
|
||||
import tooltip from "./tooltip";
|
||||
import terminal from "./terminal";
|
||||
import contactList from "./contactList";
|
||||
import incomingCallNotification from "./incomingCallNotification";
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import feedback from "./feedback";
|
||||
import { text } from "./components"
|
||||
import contactFinder from "./contactFinder"
|
||||
import contactsPopover from "./contactsPopover"
|
||||
import commandPalette from "./commandPalette"
|
||||
import editor from "./editor"
|
||||
import projectPanel from "./projectPanel"
|
||||
import search from "./search"
|
||||
import picker from "./picker"
|
||||
import workspace from "./workspace"
|
||||
import contextMenu from "./contextMenu"
|
||||
import sharedScreen from "./sharedScreen"
|
||||
import projectDiagnostics from "./projectDiagnostics"
|
||||
import contactNotification from "./contactNotification"
|
||||
import updateNotification from "./updateNotification"
|
||||
import simpleMessageNotification from "./simpleMessageNotification"
|
||||
import projectSharedNotification from "./projectSharedNotification"
|
||||
import tooltip from "./tooltip"
|
||||
import terminal from "./terminal"
|
||||
import contactList from "./contactList"
|
||||
import incomingCallNotification from "./incomingCallNotification"
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import feedback from "./feedback"
|
||||
|
||||
export default function app(colorScheme: ColorScheme): Object {
|
||||
return {
|
||||
meta: {
|
||||
name: colorScheme.name,
|
||||
isLight: colorScheme.isLight,
|
||||
},
|
||||
commandPalette: commandPalette(colorScheme),
|
||||
contactNotification: contactNotification(colorScheme),
|
||||
projectSharedNotification: projectSharedNotification(colorScheme),
|
||||
incomingCallNotification: incomingCallNotification(colorScheme),
|
||||
picker: picker(colorScheme),
|
||||
workspace: workspace(colorScheme),
|
||||
contextMenu: contextMenu(colorScheme),
|
||||
editor: editor(colorScheme),
|
||||
projectDiagnostics: projectDiagnostics(colorScheme),
|
||||
projectPanel: projectPanel(colorScheme),
|
||||
contactsPopover: contactsPopover(colorScheme),
|
||||
contactFinder: contactFinder(colorScheme),
|
||||
contactList: contactList(colorScheme),
|
||||
search: search(colorScheme),
|
||||
sharedScreen: sharedScreen(colorScheme),
|
||||
breadcrumbs: {
|
||||
...text(colorScheme.highest, "sans", "variant"),
|
||||
padding: {
|
||||
left: 6,
|
||||
},
|
||||
},
|
||||
updateNotification: updateNotification(colorScheme),
|
||||
simpleMessageNotification: simpleMessageNotification(colorScheme),
|
||||
tooltip: tooltip(colorScheme),
|
||||
terminal: terminal(colorScheme),
|
||||
feedback: feedback(colorScheme),
|
||||
colorScheme: {
|
||||
...colorScheme,
|
||||
players: Object.values(colorScheme.players),
|
||||
ramps: {
|
||||
neutral: colorScheme.ramps.neutral.colors(100, "hex"),
|
||||
red: colorScheme.ramps.red.colors(100, "hex"),
|
||||
orange: colorScheme.ramps.orange.colors(100, "hex"),
|
||||
yellow: colorScheme.ramps.yellow.colors(100, "hex"),
|
||||
green: colorScheme.ramps.green.colors(100, "hex"),
|
||||
cyan: colorScheme.ramps.cyan.colors(100, "hex"),
|
||||
blue: colorScheme.ramps.blue.colors(100, "hex"),
|
||||
violet: colorScheme.ramps.violet.colors(100, "hex"),
|
||||
magenta: colorScheme.ramps.magenta.colors(100, "hex"),
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
meta: {
|
||||
name: colorScheme.name,
|
||||
isLight: colorScheme.isLight,
|
||||
},
|
||||
commandPalette: commandPalette(colorScheme),
|
||||
contactNotification: contactNotification(colorScheme),
|
||||
projectSharedNotification: projectSharedNotification(colorScheme),
|
||||
incomingCallNotification: incomingCallNotification(colorScheme),
|
||||
picker: picker(colorScheme),
|
||||
workspace: workspace(colorScheme),
|
||||
contextMenu: contextMenu(colorScheme),
|
||||
editor: editor(colorScheme),
|
||||
projectDiagnostics: projectDiagnostics(colorScheme),
|
||||
projectPanel: projectPanel(colorScheme),
|
||||
contactsPopover: contactsPopover(colorScheme),
|
||||
contactFinder: contactFinder(colorScheme),
|
||||
contactList: contactList(colorScheme),
|
||||
search: search(colorScheme),
|
||||
sharedScreen: sharedScreen(colorScheme),
|
||||
breadcrumbs: {
|
||||
...text(colorScheme.highest, "sans", "variant"),
|
||||
padding: {
|
||||
left: 6,
|
||||
},
|
||||
},
|
||||
updateNotification: updateNotification(colorScheme),
|
||||
simpleMessageNotification: simpleMessageNotification(colorScheme),
|
||||
tooltip: tooltip(colorScheme),
|
||||
terminal: terminal(colorScheme),
|
||||
feedback: feedback(colorScheme),
|
||||
colorScheme: {
|
||||
...colorScheme,
|
||||
players: Object.values(colorScheme.players),
|
||||
ramps: {
|
||||
neutral: colorScheme.ramps.neutral.colors(100, "hex"),
|
||||
red: colorScheme.ramps.red.colors(100, "hex"),
|
||||
orange: colorScheme.ramps.orange.colors(100, "hex"),
|
||||
yellow: colorScheme.ramps.yellow.colors(100, "hex"),
|
||||
green: colorScheme.ramps.green.colors(100, "hex"),
|
||||
cyan: colorScheme.ramps.cyan.colors(100, "hex"),
|
||||
blue: colorScheme.ramps.blue.colors(100, "hex"),
|
||||
violet: colorScheme.ramps.violet.colors(100, "hex"),
|
||||
magenta: colorScheme.ramps.magenta.colors(100, "hex"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,30 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { withOpacity } from "../utils/color";
|
||||
import { text, background } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { withOpacity } from "../utils/color"
|
||||
import { text, background } from "./components"
|
||||
|
||||
export default function commandPalette(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest;
|
||||
return {
|
||||
keystrokeSpacing: 8,
|
||||
key: {
|
||||
text: text(layer, "mono", "variant", "default", { size: "xs" }),
|
||||
cornerRadius: 2,
|
||||
background: background(layer, "on"),
|
||||
padding: {
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
left: 6,
|
||||
right: 6,
|
||||
},
|
||||
margin: {
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
left: 2,
|
||||
},
|
||||
active: {
|
||||
text: text(layer, "mono", "on", "default", { size: "xs" }),
|
||||
background: withOpacity(background(layer, "on"), 0.2),
|
||||
},
|
||||
},
|
||||
};
|
||||
let layer = colorScheme.highest
|
||||
return {
|
||||
keystrokeSpacing: 8,
|
||||
key: {
|
||||
text: text(layer, "mono", "variant", "default", { size: "xs" }),
|
||||
cornerRadius: 2,
|
||||
background: background(layer, "on"),
|
||||
padding: {
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
left: 6,
|
||||
right: 6,
|
||||
},
|
||||
margin: {
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
left: 2,
|
||||
},
|
||||
active: {
|
||||
text: text(layer, "mono", "on", "default", { size: "xs" }),
|
||||
background: withOpacity(background(layer, "on"), 0.2),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,210 +1,210 @@
|
||||
import { fontFamilies, fontSizes, FontWeight } from "../common";
|
||||
import { Layer, Styles, StyleSets, Style } from "../themes/common/colorScheme";
|
||||
import { fontFamilies, fontSizes, FontWeight } from "../common"
|
||||
import { Layer, Styles, StyleSets, Style } from "../themes/common/colorScheme"
|
||||
|
||||
function isStyleSet(key: any): key is StyleSets {
|
||||
return [
|
||||
"base",
|
||||
"variant",
|
||||
"on",
|
||||
"accent",
|
||||
"positive",
|
||||
"warning",
|
||||
"negative",
|
||||
].includes(key);
|
||||
return [
|
||||
"base",
|
||||
"variant",
|
||||
"on",
|
||||
"accent",
|
||||
"positive",
|
||||
"warning",
|
||||
"negative",
|
||||
].includes(key)
|
||||
}
|
||||
|
||||
function isStyle(key: any): key is Styles {
|
||||
return [
|
||||
"default",
|
||||
"active",
|
||||
"disabled",
|
||||
"hovered",
|
||||
"pressed",
|
||||
"inverted",
|
||||
].includes(key);
|
||||
return [
|
||||
"default",
|
||||
"active",
|
||||
"disabled",
|
||||
"hovered",
|
||||
"pressed",
|
||||
"inverted",
|
||||
].includes(key)
|
||||
}
|
||||
function getStyle(
|
||||
layer: Layer,
|
||||
possibleStyleSetOrStyle?: any,
|
||||
possibleStyle?: any
|
||||
layer: Layer,
|
||||
possibleStyleSetOrStyle?: any,
|
||||
possibleStyle?: any
|
||||
): Style {
|
||||
let styleSet: StyleSets = "base";
|
||||
let style: Styles = "default";
|
||||
if (isStyleSet(possibleStyleSetOrStyle)) {
|
||||
styleSet = possibleStyleSetOrStyle;
|
||||
} else if (isStyle(possibleStyleSetOrStyle)) {
|
||||
style = possibleStyleSetOrStyle;
|
||||
}
|
||||
let styleSet: StyleSets = "base"
|
||||
let style: Styles = "default"
|
||||
if (isStyleSet(possibleStyleSetOrStyle)) {
|
||||
styleSet = possibleStyleSetOrStyle
|
||||
} else if (isStyle(possibleStyleSetOrStyle)) {
|
||||
style = possibleStyleSetOrStyle
|
||||
}
|
||||
|
||||
if (isStyle(possibleStyle)) {
|
||||
style = possibleStyle;
|
||||
}
|
||||
if (isStyle(possibleStyle)) {
|
||||
style = possibleStyle
|
||||
}
|
||||
|
||||
return layer[styleSet][style];
|
||||
return layer[styleSet][style]
|
||||
}
|
||||
|
||||
export function background(layer: Layer, style?: Styles): string;
|
||||
export function background(layer: Layer, style?: Styles): string
|
||||
export function background(
|
||||
layer: Layer,
|
||||
styleSet?: StyleSets,
|
||||
style?: Styles
|
||||
): string;
|
||||
layer: Layer,
|
||||
styleSet?: StyleSets,
|
||||
style?: Styles
|
||||
): string
|
||||
export function background(
|
||||
layer: Layer,
|
||||
styleSetOrStyles?: StyleSets | Styles,
|
||||
style?: Styles
|
||||
layer: Layer,
|
||||
styleSetOrStyles?: StyleSets | Styles,
|
||||
style?: Styles
|
||||
): string {
|
||||
return getStyle(layer, styleSetOrStyles, style).background;
|
||||
return getStyle(layer, styleSetOrStyles, style).background
|
||||
}
|
||||
|
||||
export function borderColor(layer: Layer, style?: Styles): string;
|
||||
export function borderColor(layer: Layer, style?: Styles): string
|
||||
export function borderColor(
|
||||
layer: Layer,
|
||||
styleSet?: StyleSets,
|
||||
style?: Styles
|
||||
): string;
|
||||
layer: Layer,
|
||||
styleSet?: StyleSets,
|
||||
style?: Styles
|
||||
): string
|
||||
export function borderColor(
|
||||
layer: Layer,
|
||||
styleSetOrStyles?: StyleSets | Styles,
|
||||
style?: Styles
|
||||
layer: Layer,
|
||||
styleSetOrStyles?: StyleSets | Styles,
|
||||
style?: Styles
|
||||
): string {
|
||||
return getStyle(layer, styleSetOrStyles, style).border;
|
||||
return getStyle(layer, styleSetOrStyles, style).border
|
||||
}
|
||||
|
||||
export function foreground(layer: Layer, style?: Styles): string;
|
||||
export function foreground(layer: Layer, style?: Styles): string
|
||||
export function foreground(
|
||||
layer: Layer,
|
||||
styleSet?: StyleSets,
|
||||
style?: Styles
|
||||
): string;
|
||||
layer: Layer,
|
||||
styleSet?: StyleSets,
|
||||
style?: Styles
|
||||
): string
|
||||
export function foreground(
|
||||
layer: Layer,
|
||||
styleSetOrStyles?: StyleSets | Styles,
|
||||
style?: Styles
|
||||
layer: Layer,
|
||||
styleSetOrStyles?: StyleSets | Styles,
|
||||
style?: Styles
|
||||
): string {
|
||||
return getStyle(layer, styleSetOrStyles, style).foreground;
|
||||
return getStyle(layer, styleSetOrStyles, style).foreground
|
||||
}
|
||||
|
||||
interface Text {
|
||||
family: keyof typeof fontFamilies;
|
||||
color: string;
|
||||
size: number;
|
||||
weight?: FontWeight;
|
||||
underline?: boolean;
|
||||
family: keyof typeof fontFamilies
|
||||
color: string
|
||||
size: number
|
||||
weight?: FontWeight
|
||||
underline?: boolean
|
||||
}
|
||||
|
||||
interface TextProperties {
|
||||
size?: keyof typeof fontSizes;
|
||||
weight?: FontWeight;
|
||||
underline?: boolean;
|
||||
color?: string;
|
||||
size?: keyof typeof fontSizes
|
||||
weight?: FontWeight
|
||||
underline?: boolean
|
||||
color?: string
|
||||
}
|
||||
|
||||
export function text(
|
||||
layer: Layer,
|
||||
fontFamily: keyof typeof fontFamilies,
|
||||
styleSet: StyleSets,
|
||||
style: Styles,
|
||||
properties?: TextProperties
|
||||
): Text;
|
||||
layer: Layer,
|
||||
fontFamily: keyof typeof fontFamilies,
|
||||
styleSet: StyleSets,
|
||||
style: Styles,
|
||||
properties?: TextProperties
|
||||
): Text
|
||||
export function text(
|
||||
layer: Layer,
|
||||
fontFamily: keyof typeof fontFamilies,
|
||||
styleSet: StyleSets,
|
||||
properties?: TextProperties
|
||||
): Text;
|
||||
layer: Layer,
|
||||
fontFamily: keyof typeof fontFamilies,
|
||||
styleSet: StyleSets,
|
||||
properties?: TextProperties
|
||||
): Text
|
||||
export function text(
|
||||
layer: Layer,
|
||||
fontFamily: keyof typeof fontFamilies,
|
||||
style: Styles,
|
||||
properties?: TextProperties
|
||||
): Text;
|
||||
layer: Layer,
|
||||
fontFamily: keyof typeof fontFamilies,
|
||||
style: Styles,
|
||||
properties?: TextProperties
|
||||
): Text
|
||||
export function text(
|
||||
layer: Layer,
|
||||
fontFamily: keyof typeof fontFamilies,
|
||||
properties?: TextProperties
|
||||
): Text;
|
||||
layer: Layer,
|
||||
fontFamily: keyof typeof fontFamilies,
|
||||
properties?: TextProperties
|
||||
): Text
|
||||
export function text(
|
||||
layer: Layer,
|
||||
fontFamily: keyof typeof fontFamilies,
|
||||
styleSetStyleOrProperties?: StyleSets | Styles | TextProperties,
|
||||
styleOrProperties?: Styles | TextProperties,
|
||||
properties?: TextProperties
|
||||
layer: Layer,
|
||||
fontFamily: keyof typeof fontFamilies,
|
||||
styleSetStyleOrProperties?: StyleSets | Styles | TextProperties,
|
||||
styleOrProperties?: Styles | TextProperties,
|
||||
properties?: TextProperties
|
||||
) {
|
||||
let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties);
|
||||
let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
|
||||
|
||||
if (typeof styleSetStyleOrProperties === "object") {
|
||||
properties = styleSetStyleOrProperties;
|
||||
}
|
||||
if (typeof styleOrProperties === "object") {
|
||||
properties = styleOrProperties;
|
||||
}
|
||||
if (typeof styleSetStyleOrProperties === "object") {
|
||||
properties = styleSetStyleOrProperties
|
||||
}
|
||||
if (typeof styleOrProperties === "object") {
|
||||
properties = styleOrProperties
|
||||
}
|
||||
|
||||
let size = fontSizes[properties?.size || "sm"];
|
||||
let color = properties?.color || style.foreground;
|
||||
let size = fontSizes[properties?.size || "sm"]
|
||||
let color = properties?.color || style.foreground
|
||||
|
||||
return {
|
||||
family: fontFamilies[fontFamily],
|
||||
...properties,
|
||||
color,
|
||||
size,
|
||||
};
|
||||
return {
|
||||
family: fontFamilies[fontFamily],
|
||||
...properties,
|
||||
color,
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
export interface Border {
|
||||
color: string;
|
||||
width: number;
|
||||
top?: boolean;
|
||||
bottom?: boolean;
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
overlay?: boolean;
|
||||
color: string
|
||||
width: number
|
||||
top?: boolean
|
||||
bottom?: boolean
|
||||
left?: boolean
|
||||
right?: boolean
|
||||
overlay?: boolean
|
||||
}
|
||||
|
||||
export interface BorderProperties {
|
||||
width?: number;
|
||||
top?: boolean;
|
||||
bottom?: boolean;
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
overlay?: boolean;
|
||||
width?: number
|
||||
top?: boolean
|
||||
bottom?: boolean
|
||||
left?: boolean
|
||||
right?: boolean
|
||||
overlay?: boolean
|
||||
}
|
||||
|
||||
export function border(
|
||||
layer: Layer,
|
||||
styleSet: StyleSets,
|
||||
style: Styles,
|
||||
properties?: BorderProperties
|
||||
): Border;
|
||||
layer: Layer,
|
||||
styleSet: StyleSets,
|
||||
style: Styles,
|
||||
properties?: BorderProperties
|
||||
): Border
|
||||
export function border(
|
||||
layer: Layer,
|
||||
styleSet: StyleSets,
|
||||
properties?: BorderProperties
|
||||
): Border;
|
||||
layer: Layer,
|
||||
styleSet: StyleSets,
|
||||
properties?: BorderProperties
|
||||
): Border
|
||||
export function border(
|
||||
layer: Layer,
|
||||
style: Styles,
|
||||
properties?: BorderProperties
|
||||
): Border;
|
||||
export function border(layer: Layer, properties?: BorderProperties): Border;
|
||||
layer: Layer,
|
||||
style: Styles,
|
||||
properties?: BorderProperties
|
||||
): Border
|
||||
export function border(layer: Layer, properties?: BorderProperties): Border
|
||||
export function border(
|
||||
layer: Layer,
|
||||
styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties,
|
||||
styleOrProperties?: Styles | BorderProperties,
|
||||
properties?: BorderProperties
|
||||
layer: Layer,
|
||||
styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties,
|
||||
styleOrProperties?: Styles | BorderProperties,
|
||||
properties?: BorderProperties
|
||||
): Border {
|
||||
let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties);
|
||||
let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties)
|
||||
|
||||
if (typeof styleSetStyleOrProperties === "object") {
|
||||
properties = styleSetStyleOrProperties;
|
||||
}
|
||||
if (typeof styleOrProperties === "object") {
|
||||
properties = styleOrProperties;
|
||||
}
|
||||
if (typeof styleSetStyleOrProperties === "object") {
|
||||
properties = styleSetStyleOrProperties
|
||||
}
|
||||
if (typeof styleOrProperties === "object") {
|
||||
properties = styleOrProperties
|
||||
}
|
||||
|
||||
return {
|
||||
color: style.border,
|
||||
width: 1,
|
||||
...properties,
|
||||
};
|
||||
return {
|
||||
color: style.border,
|
||||
width: 1,
|
||||
...properties,
|
||||
}
|
||||
}
|
||||
|
@ -1,70 +1,70 @@
|
||||
import picker from "./picker";
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, border, foreground, text } from "./components";
|
||||
import picker from "./picker"
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, border, foreground, text } from "./components"
|
||||
|
||||
export default function contactFinder(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle;
|
||||
let layer = colorScheme.middle
|
||||
|
||||
const sideMargin = 6;
|
||||
const contactButton = {
|
||||
background: background(layer, "variant"),
|
||||
color: foreground(layer, "variant"),
|
||||
iconWidth: 8,
|
||||
buttonWidth: 16,
|
||||
cornerRadius: 8,
|
||||
};
|
||||
|
||||
const pickerStyle = picker(colorScheme);
|
||||
const pickerInput = {
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 6,
|
||||
text: text(layer, "mono",),
|
||||
placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
|
||||
selection: colorScheme.players[0],
|
||||
border: border(layer),
|
||||
padding: {
|
||||
bottom: 4,
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
},
|
||||
margin: {
|
||||
left: sideMargin,
|
||||
right: sideMargin,
|
||||
const sideMargin = 6
|
||||
const contactButton = {
|
||||
background: background(layer, "variant"),
|
||||
color: foreground(layer, "variant"),
|
||||
iconWidth: 8,
|
||||
buttonWidth: 16,
|
||||
cornerRadius: 8,
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
picker: {
|
||||
emptyContainer: {},
|
||||
item: {
|
||||
...pickerStyle.item,
|
||||
margin: { left: sideMargin, right: sideMargin },
|
||||
},
|
||||
noMatches: pickerStyle.noMatches,
|
||||
inputEditor: pickerInput,
|
||||
emptyInputEditor: pickerInput
|
||||
},
|
||||
rowHeight: 28,
|
||||
contactAvatar: {
|
||||
cornerRadius: 10,
|
||||
width: 18,
|
||||
},
|
||||
contactUsername: {
|
||||
padding: {
|
||||
left: 8,
|
||||
},
|
||||
},
|
||||
contactButton: {
|
||||
...contactButton,
|
||||
hover: {
|
||||
background: background(layer, "variant", "hovered"),
|
||||
},
|
||||
},
|
||||
disabledContactButton: {
|
||||
...contactButton,
|
||||
background: background(layer, "disabled"),
|
||||
color: foreground(layer, "disabled"),
|
||||
},
|
||||
};
|
||||
const pickerStyle = picker(colorScheme)
|
||||
const pickerInput = {
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 6,
|
||||
text: text(layer, "mono"),
|
||||
placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
|
||||
selection: colorScheme.players[0],
|
||||
border: border(layer),
|
||||
padding: {
|
||||
bottom: 4,
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
},
|
||||
margin: {
|
||||
left: sideMargin,
|
||||
right: sideMargin,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
picker: {
|
||||
emptyContainer: {},
|
||||
item: {
|
||||
...pickerStyle.item,
|
||||
margin: { left: sideMargin, right: sideMargin },
|
||||
},
|
||||
noMatches: pickerStyle.noMatches,
|
||||
inputEditor: pickerInput,
|
||||
emptyInputEditor: pickerInput,
|
||||
},
|
||||
rowHeight: 28,
|
||||
contactAvatar: {
|
||||
cornerRadius: 10,
|
||||
width: 18,
|
||||
},
|
||||
contactUsername: {
|
||||
padding: {
|
||||
left: 8,
|
||||
},
|
||||
},
|
||||
contactButton: {
|
||||
...contactButton,
|
||||
hover: {
|
||||
background: background(layer, "variant", "hovered"),
|
||||
},
|
||||
},
|
||||
disabledContactButton: {
|
||||
...contactButton,
|
||||
background: background(layer, "disabled"),
|
||||
color: foreground(layer, "disabled"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,186 +1,182 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import {
|
||||
background,
|
||||
border,
|
||||
borderColor,
|
||||
foreground,
|
||||
text,
|
||||
} from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, border, borderColor, foreground, text } from "./components"
|
||||
|
||||
export default function contactsPanel(colorScheme: ColorScheme) {
|
||||
const nameMargin = 8;
|
||||
const sidePadding = 12;
|
||||
const nameMargin = 8
|
||||
const sidePadding = 12
|
||||
|
||||
let layer = colorScheme.middle;
|
||||
let layer = colorScheme.middle
|
||||
|
||||
const contactButton = {
|
||||
background: background(layer, "on"),
|
||||
color: foreground(layer, "on"),
|
||||
iconWidth: 8,
|
||||
buttonWidth: 16,
|
||||
cornerRadius: 8,
|
||||
};
|
||||
const projectRow = {
|
||||
guestAvatarSpacing: 4,
|
||||
height: 24,
|
||||
guestAvatar: {
|
||||
cornerRadius: 8,
|
||||
width: 14,
|
||||
},
|
||||
name: {
|
||||
...text(layer, "mono", { size: "sm" }),
|
||||
margin: {
|
||||
left: nameMargin,
|
||||
right: 6,
|
||||
},
|
||||
},
|
||||
guests: {
|
||||
margin: {
|
||||
left: nameMargin,
|
||||
right: nameMargin,
|
||||
},
|
||||
},
|
||||
padding: {
|
||||
left: sidePadding,
|
||||
right: sidePadding,
|
||||
},
|
||||
};
|
||||
const contactButton = {
|
||||
background: background(layer, "on"),
|
||||
color: foreground(layer, "on"),
|
||||
iconWidth: 8,
|
||||
buttonWidth: 16,
|
||||
cornerRadius: 8,
|
||||
}
|
||||
const projectRow = {
|
||||
guestAvatarSpacing: 4,
|
||||
height: 24,
|
||||
guestAvatar: {
|
||||
cornerRadius: 8,
|
||||
width: 14,
|
||||
},
|
||||
name: {
|
||||
...text(layer, "mono", { size: "sm" }),
|
||||
margin: {
|
||||
left: nameMargin,
|
||||
right: 6,
|
||||
},
|
||||
},
|
||||
guests: {
|
||||
margin: {
|
||||
left: nameMargin,
|
||||
right: nameMargin,
|
||||
},
|
||||
},
|
||||
padding: {
|
||||
left: sidePadding,
|
||||
right: sidePadding,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
background: background(layer),
|
||||
padding: { top: 12, bottom: 0 },
|
||||
userQueryEditor: {
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 6,
|
||||
text: text(layer, "mono", "on"),
|
||||
placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
|
||||
selection: colorScheme.players[0],
|
||||
border: border(layer, "on"),
|
||||
padding: {
|
||||
bottom: 4,
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
},
|
||||
margin: {
|
||||
left: 6,
|
||||
},
|
||||
},
|
||||
userQueryEditorHeight: 33,
|
||||
addContactButton: {
|
||||
margin: { left: 6, right: 12 },
|
||||
color: foreground(layer, "on"),
|
||||
buttonWidth: 28,
|
||||
iconWidth: 16,
|
||||
},
|
||||
rowHeight: 28,
|
||||
sectionIconSize: 8,
|
||||
headerRow: {
|
||||
...text(layer, "mono", { size: "sm" }),
|
||||
margin: { top: 14 },
|
||||
padding: {
|
||||
left: sidePadding,
|
||||
right: sidePadding,
|
||||
},
|
||||
active: {
|
||||
...text(layer, "mono", "active", { size: "sm" }),
|
||||
background: background(layer, "active"),
|
||||
},
|
||||
},
|
||||
leaveCall: {
|
||||
background: background(layer),
|
||||
border: border(layer),
|
||||
cornerRadius: 6,
|
||||
margin: {
|
||||
top: 1,
|
||||
},
|
||||
padding: {
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
left: 7,
|
||||
right: 7,
|
||||
},
|
||||
...text(layer, "sans", "variant", { size: "xs" }),
|
||||
hover: {
|
||||
...text(layer, "sans", "hovered", { size: "xs" }),
|
||||
background: background(layer, "hovered"),
|
||||
border: border(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
contactRow: {
|
||||
padding: {
|
||||
left: sidePadding,
|
||||
right: sidePadding,
|
||||
},
|
||||
active: {
|
||||
background: background(layer, "active"),
|
||||
},
|
||||
},
|
||||
contactAvatar: {
|
||||
cornerRadius: 10,
|
||||
width: 18,
|
||||
},
|
||||
contactStatusFree: {
|
||||
cornerRadius: 4,
|
||||
padding: 4,
|
||||
margin: { top: 12, left: 12 },
|
||||
background: foreground(layer, "positive"),
|
||||
},
|
||||
contactStatusBusy: {
|
||||
cornerRadius: 4,
|
||||
padding: 4,
|
||||
margin: { top: 12, left: 12 },
|
||||
background: foreground(layer, "negative"),
|
||||
},
|
||||
contactUsername: {
|
||||
...text(layer, "mono", { size: "sm" }),
|
||||
margin: {
|
||||
left: nameMargin,
|
||||
},
|
||||
},
|
||||
contactButtonSpacing: nameMargin,
|
||||
contactButton: {
|
||||
...contactButton,
|
||||
hover: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
disabledButton: {
|
||||
...contactButton,
|
||||
background: background(layer, "on"),
|
||||
color: foreground(layer, "on"),
|
||||
},
|
||||
callingIndicator: {
|
||||
...text(layer, "mono", "variant", { size: "xs" }),
|
||||
},
|
||||
treeBranch: {
|
||||
color: borderColor(layer),
|
||||
width: 1,
|
||||
hover: {
|
||||
color: borderColor(layer),
|
||||
},
|
||||
active: {
|
||||
color: borderColor(layer),
|
||||
},
|
||||
},
|
||||
projectRow: {
|
||||
...projectRow,
|
||||
background: background(layer),
|
||||
icon: {
|
||||
margin: { left: nameMargin },
|
||||
color: foreground(layer, "variant"),
|
||||
width: 12,
|
||||
},
|
||||
name: {
|
||||
...projectRow.name,
|
||||
...text(layer, "mono", { size: "sm" }),
|
||||
},
|
||||
hover: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
active: {
|
||||
background: background(layer, "active"),
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
background: background(layer),
|
||||
padding: { top: 12, bottom: 0 },
|
||||
userQueryEditor: {
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 6,
|
||||
text: text(layer, "mono", "on"),
|
||||
placeholderText: text(layer, "mono", "on", "disabled", {
|
||||
size: "xs",
|
||||
}),
|
||||
selection: colorScheme.players[0],
|
||||
border: border(layer, "on"),
|
||||
padding: {
|
||||
bottom: 4,
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
},
|
||||
margin: {
|
||||
left: 6,
|
||||
},
|
||||
},
|
||||
userQueryEditorHeight: 33,
|
||||
addContactButton: {
|
||||
margin: { left: 6, right: 12 },
|
||||
color: foreground(layer, "on"),
|
||||
buttonWidth: 28,
|
||||
iconWidth: 16,
|
||||
},
|
||||
rowHeight: 28,
|
||||
sectionIconSize: 8,
|
||||
headerRow: {
|
||||
...text(layer, "mono", { size: "sm" }),
|
||||
margin: { top: 14 },
|
||||
padding: {
|
||||
left: sidePadding,
|
||||
right: sidePadding,
|
||||
},
|
||||
active: {
|
||||
...text(layer, "mono", "active", { size: "sm" }),
|
||||
background: background(layer, "active"),
|
||||
},
|
||||
},
|
||||
leaveCall: {
|
||||
background: background(layer),
|
||||
border: border(layer),
|
||||
cornerRadius: 6,
|
||||
margin: {
|
||||
top: 1,
|
||||
},
|
||||
padding: {
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
left: 7,
|
||||
right: 7,
|
||||
},
|
||||
...text(layer, "sans", "variant", { size: "xs" }),
|
||||
hover: {
|
||||
...text(layer, "sans", "hovered", { size: "xs" }),
|
||||
background: background(layer, "hovered"),
|
||||
border: border(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
contactRow: {
|
||||
padding: {
|
||||
left: sidePadding,
|
||||
right: sidePadding,
|
||||
},
|
||||
active: {
|
||||
background: background(layer, "active"),
|
||||
},
|
||||
},
|
||||
contactAvatar: {
|
||||
cornerRadius: 10,
|
||||
width: 18,
|
||||
},
|
||||
contactStatusFree: {
|
||||
cornerRadius: 4,
|
||||
padding: 4,
|
||||
margin: { top: 12, left: 12 },
|
||||
background: foreground(layer, "positive"),
|
||||
},
|
||||
contactStatusBusy: {
|
||||
cornerRadius: 4,
|
||||
padding: 4,
|
||||
margin: { top: 12, left: 12 },
|
||||
background: foreground(layer, "negative"),
|
||||
},
|
||||
contactUsername: {
|
||||
...text(layer, "mono", { size: "sm" }),
|
||||
margin: {
|
||||
left: nameMargin,
|
||||
},
|
||||
},
|
||||
contactButtonSpacing: nameMargin,
|
||||
contactButton: {
|
||||
...contactButton,
|
||||
hover: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
disabledButton: {
|
||||
...contactButton,
|
||||
background: background(layer, "on"),
|
||||
color: foreground(layer, "on"),
|
||||
},
|
||||
callingIndicator: {
|
||||
...text(layer, "mono", "variant", { size: "xs" }),
|
||||
},
|
||||
treeBranch: {
|
||||
color: borderColor(layer),
|
||||
width: 1,
|
||||
hover: {
|
||||
color: borderColor(layer),
|
||||
},
|
||||
active: {
|
||||
color: borderColor(layer),
|
||||
},
|
||||
},
|
||||
projectRow: {
|
||||
...projectRow,
|
||||
background: background(layer),
|
||||
icon: {
|
||||
margin: { left: nameMargin },
|
||||
color: foreground(layer, "variant"),
|
||||
width: 12,
|
||||
},
|
||||
name: {
|
||||
...projectRow.name,
|
||||
...text(layer, "mono", { size: "sm" }),
|
||||
},
|
||||
hover: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
active: {
|
||||
background: background(layer, "active"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +1,45 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, foreground, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, foreground, text } from "./components"
|
||||
|
||||
const avatarSize = 12;
|
||||
const headerPadding = 8;
|
||||
const avatarSize = 12
|
||||
const headerPadding = 8
|
||||
|
||||
export default function contactNotification(colorScheme: ColorScheme): Object {
|
||||
let layer = colorScheme.lowest;
|
||||
return {
|
||||
headerAvatar: {
|
||||
height: avatarSize,
|
||||
width: avatarSize,
|
||||
cornerRadius: 6,
|
||||
},
|
||||
headerMessage: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, right: headerPadding },
|
||||
},
|
||||
headerHeight: 18,
|
||||
bodyMessage: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
|
||||
},
|
||||
button: {
|
||||
...text(layer, "sans", "on", { size: "xs" }),
|
||||
background: background(layer, "on"),
|
||||
padding: 4,
|
||||
cornerRadius: 6,
|
||||
margin: { left: 6 },
|
||||
hover: {
|
||||
background: background(layer, "on", "hovered"),
|
||||
},
|
||||
},
|
||||
dismissButton: {
|
||||
color: foreground(layer, "variant"),
|
||||
iconWidth: 8,
|
||||
iconHeight: 8,
|
||||
buttonWidth: 8,
|
||||
buttonHeight: 8,
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
};
|
||||
let layer = colorScheme.lowest
|
||||
return {
|
||||
headerAvatar: {
|
||||
height: avatarSize,
|
||||
width: avatarSize,
|
||||
cornerRadius: 6,
|
||||
},
|
||||
headerMessage: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, right: headerPadding },
|
||||
},
|
||||
headerHeight: 18,
|
||||
bodyMessage: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
|
||||
},
|
||||
button: {
|
||||
...text(layer, "sans", "on", { size: "xs" }),
|
||||
background: background(layer, "on"),
|
||||
padding: 4,
|
||||
cornerRadius: 6,
|
||||
margin: { left: 6 },
|
||||
hover: {
|
||||
background: background(layer, "on", "hovered"),
|
||||
},
|
||||
},
|
||||
dismissButton: {
|
||||
color: foreground(layer, "variant"),
|
||||
iconWidth: 8,
|
||||
iconHeight: 8,
|
||||
buttonWidth: 8,
|
||||
buttonHeight: 8,
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,29 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, border, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
|
||||
export default function contactsPopover(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle;
|
||||
const sidePadding = 12;
|
||||
return {
|
||||
background: background(layer),
|
||||
cornerRadius: 6,
|
||||
padding: { top: 6 },
|
||||
margin: { top: -6 },
|
||||
shadow: colorScheme.popoverShadow,
|
||||
border: border(layer),
|
||||
width: 300,
|
||||
height: 400,
|
||||
inviteRowHeight: 28,
|
||||
inviteRow: {
|
||||
padding: {
|
||||
left: sidePadding,
|
||||
right: sidePadding,
|
||||
},
|
||||
border: border(layer, { top: true }),
|
||||
text: text(layer, "sans", "variant", { size: "sm" }),
|
||||
hover: {
|
||||
text: text(layer, "sans", "hovered", { size: "sm" }),
|
||||
},
|
||||
},
|
||||
}
|
||||
let layer = colorScheme.middle
|
||||
const sidePadding = 12
|
||||
return {
|
||||
background: background(layer),
|
||||
cornerRadius: 6,
|
||||
padding: { top: 6 },
|
||||
margin: { top: -6 },
|
||||
shadow: colorScheme.popoverShadow,
|
||||
border: border(layer),
|
||||
width: 300,
|
||||
height: 400,
|
||||
inviteRowHeight: 28,
|
||||
inviteRow: {
|
||||
padding: {
|
||||
left: sidePadding,
|
||||
right: sidePadding,
|
||||
},
|
||||
border: border(layer, { top: true }),
|
||||
text: text(layer, "sans", "variant", { size: "sm" }),
|
||||
hover: {
|
||||
text: text(layer, "sans", "hovered", { size: "sm" }),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +1,44 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, border, borderColor, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, border, borderColor, text } from "./components"
|
||||
|
||||
export default function contextMenu(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle;
|
||||
return {
|
||||
background: background(layer),
|
||||
cornerRadius: 10,
|
||||
padding: 4,
|
||||
shadow: colorScheme.popoverShadow,
|
||||
border: border(layer),
|
||||
keystrokeMargin: 30,
|
||||
item: {
|
||||
iconSpacing: 8,
|
||||
iconWidth: 14,
|
||||
padding: { left: 6, right: 6, top: 2, bottom: 2 },
|
||||
cornerRadius: 6,
|
||||
label: text(layer, "sans", { size: "sm" }),
|
||||
keystroke: {
|
||||
...text(layer, "sans", "variant", { size: "sm", weight: "bold" }),
|
||||
padding: { left: 3, right: 3 },
|
||||
},
|
||||
hover: {
|
||||
background: background(layer, "hovered"),
|
||||
label: text(layer, "sans", "hovered", { size: "sm" }),
|
||||
},
|
||||
active: {
|
||||
background: background(layer, "active"),
|
||||
label: text(layer, "sans", "active", { size: "sm" }),
|
||||
},
|
||||
activeHover: {
|
||||
background: background(layer, "active"),
|
||||
label: text(layer, "sans", "active", { size: "sm" }),
|
||||
},
|
||||
},
|
||||
separator: {
|
||||
background: borderColor(layer),
|
||||
margin: { top: 2, bottom: 2 },
|
||||
},
|
||||
};
|
||||
let layer = colorScheme.middle
|
||||
return {
|
||||
background: background(layer),
|
||||
cornerRadius: 10,
|
||||
padding: 4,
|
||||
shadow: colorScheme.popoverShadow,
|
||||
border: border(layer),
|
||||
keystrokeMargin: 30,
|
||||
item: {
|
||||
iconSpacing: 8,
|
||||
iconWidth: 14,
|
||||
padding: { left: 6, right: 6, top: 2, bottom: 2 },
|
||||
cornerRadius: 6,
|
||||
label: text(layer, "sans", { size: "sm" }),
|
||||
keystroke: {
|
||||
...text(layer, "sans", "variant", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
padding: { left: 3, right: 3 },
|
||||
},
|
||||
hover: {
|
||||
background: background(layer, "hovered"),
|
||||
label: text(layer, "sans", "hovered", { size: "sm" }),
|
||||
},
|
||||
active: {
|
||||
background: background(layer, "active"),
|
||||
label: text(layer, "sans", "active", { size: "sm" }),
|
||||
},
|
||||
activeHover: {
|
||||
background: background(layer, "active"),
|
||||
label: text(layer, "sans", "active", { size: "sm" }),
|
||||
},
|
||||
},
|
||||
separator: {
|
||||
background: borderColor(layer),
|
||||
margin: { top: 2, bottom: 2 },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,285 +1,317 @@
|
||||
import { fontWeights } from "../common";
|
||||
import { withOpacity } from "../utils/color";
|
||||
import { ColorScheme, Layer, StyleSets } from "../themes/common/colorScheme";
|
||||
import { fontWeights } from "../common"
|
||||
import { withOpacity } from "../utils/color"
|
||||
import {
|
||||
background,
|
||||
border,
|
||||
borderColor,
|
||||
foreground,
|
||||
text,
|
||||
} from "./components";
|
||||
import hoverPopover from "./hoverPopover";
|
||||
ColorScheme,
|
||||
Layer,
|
||||
StyleSets,
|
||||
Syntax,
|
||||
ThemeSyntax,
|
||||
} from "../themes/common/colorScheme"
|
||||
import { background, border, borderColor, foreground, text } from "./components"
|
||||
import hoverPopover from "./hoverPopover"
|
||||
|
||||
import deepmerge from "deepmerge"
|
||||
|
||||
export default function editor(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest;
|
||||
let layer = colorScheme.highest
|
||||
|
||||
const autocompleteItem = {
|
||||
cornerRadius: 6,
|
||||
padding: {
|
||||
bottom: 2,
|
||||
left: 6,
|
||||
right: 6,
|
||||
top: 2,
|
||||
},
|
||||
};
|
||||
const autocompleteItem = {
|
||||
cornerRadius: 6,
|
||||
padding: {
|
||||
bottom: 2,
|
||||
left: 6,
|
||||
right: 6,
|
||||
top: 2,
|
||||
},
|
||||
}
|
||||
|
||||
function diagnostic(layer: Layer, styleSet: StyleSets) {
|
||||
return {
|
||||
textScaleFactor: 0.857,
|
||||
header: {
|
||||
border: border(layer, {
|
||||
top: true,
|
||||
}),
|
||||
},
|
||||
message: {
|
||||
text: text(layer, "sans", styleSet, "default", { size: "sm" }),
|
||||
highlightText: text(layer, "sans", styleSet, "default", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSyntax: Syntax = {
|
||||
primary: {
|
||||
color: colorScheme.ramps.neutral(1).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
"variable.special": {
|
||||
// Highlights for self, this, etc
|
||||
color: colorScheme.ramps.blue(0.7).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
comment: {
|
||||
color: colorScheme.ramps.neutral(0.71).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
punctuation: {
|
||||
color: colorScheme.ramps.neutral(0.86).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
constant: {
|
||||
color: colorScheme.ramps.green(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
keyword: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
function: {
|
||||
color: colorScheme.ramps.yellow(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
type: {
|
||||
color: colorScheme.ramps.cyan(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
constructor: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
variant: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
property: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
enum: {
|
||||
color: colorScheme.ramps.orange(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
operator: {
|
||||
color: colorScheme.ramps.orange(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
string: {
|
||||
color: colorScheme.ramps.orange(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
number: {
|
||||
color: colorScheme.ramps.green(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
boolean: {
|
||||
color: colorScheme.ramps.green(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
predictive: {
|
||||
color: colorScheme.ramps.neutral(0.57).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
title: {
|
||||
color: colorScheme.ramps.yellow(0.5).hex(),
|
||||
weight: fontWeights.bold,
|
||||
},
|
||||
emphasis: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
"emphasis.strong": {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.bold,
|
||||
},
|
||||
linkUri: {
|
||||
color: colorScheme.ramps.green(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
underline: true,
|
||||
},
|
||||
linkText: {
|
||||
color: colorScheme.ramps.orange(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
italic: true,
|
||||
},
|
||||
}
|
||||
|
||||
function createSyntax(colorScheme: ColorScheme): Syntax {
|
||||
if (!colorScheme.syntax) {
|
||||
return defaultSyntax
|
||||
}
|
||||
|
||||
return deepmerge<Syntax, Partial<ThemeSyntax>>(
|
||||
defaultSyntax,
|
||||
colorScheme.syntax,
|
||||
{
|
||||
arrayMerge: (destinationArray, sourceArray) => [
|
||||
...destinationArray,
|
||||
...sourceArray,
|
||||
],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const syntax = createSyntax(colorScheme)
|
||||
|
||||
function diagnostic(layer: Layer, styleSet: StyleSets) {
|
||||
return {
|
||||
textScaleFactor: 0.857,
|
||||
header: {
|
||||
border: border(layer, {
|
||||
top: true,
|
||||
}),
|
||||
},
|
||||
message: {
|
||||
text: text(layer, "sans", styleSet, "default", { size: "sm" }),
|
||||
highlightText: text(layer, "sans", styleSet, "default", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const syntax = {
|
||||
primary: {
|
||||
color: colorScheme.ramps.neutral(1).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
"variable.special": {
|
||||
// Highlights for self, this, etc
|
||||
color: colorScheme.ramps.blue(0.7).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
comment: {
|
||||
color: colorScheme.ramps.neutral(0.71).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
punctuation: {
|
||||
color: colorScheme.ramps.neutral(0.86).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
constant: {
|
||||
color: colorScheme.ramps.green(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
keyword: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
function: {
|
||||
color: colorScheme.ramps.yellow(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
type: {
|
||||
color: colorScheme.ramps.cyan(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
constructor: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
variant: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
property: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
enum: {
|
||||
color: colorScheme.ramps.orange(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
operator: {
|
||||
color: colorScheme.ramps.orange(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
string: {
|
||||
color: colorScheme.ramps.orange(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
number: {
|
||||
color: colorScheme.ramps.green(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
boolean: {
|
||||
color: colorScheme.ramps.green(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
predictive: {
|
||||
color: colorScheme.ramps.neutral(0.57).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
title: {
|
||||
color: colorScheme.ramps.yellow(0.5).hex(),
|
||||
weight: fontWeights.bold,
|
||||
},
|
||||
emphasis: {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
"emphasis.strong": {
|
||||
color: colorScheme.ramps.blue(0.5).hex(),
|
||||
weight: fontWeights.bold,
|
||||
},
|
||||
linkUri: {
|
||||
color: colorScheme.ramps.green(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
underline: true,
|
||||
},
|
||||
linkText: {
|
||||
color: colorScheme.ramps.orange(0.5).hex(),
|
||||
weight: fontWeights.normal,
|
||||
italic: true,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
textColor: syntax.primary.color,
|
||||
background: background(layer),
|
||||
activeLineBackground: withOpacity(background(layer, "on"), 0.75),
|
||||
highlightedLineBackground: background(layer, "on"),
|
||||
codeActions: {
|
||||
indicator: foreground(layer, "variant"),
|
||||
verticalScale: 0.55,
|
||||
},
|
||||
diff: {
|
||||
deleted: foreground(layer, "negative"),
|
||||
modified: foreground(layer, "warning"),
|
||||
inserted: foreground(layer, "positive"),
|
||||
removedWidthEm: 0.275,
|
||||
widthEm: 0.16,
|
||||
cornerRadius: 0.05,
|
||||
},
|
||||
/** Highlights matching occurences of what is under the cursor
|
||||
* as well as matched brackets
|
||||
*/
|
||||
documentHighlightReadBackground: withOpacity(foreground(layer, "accent"), 0.1),
|
||||
documentHighlightWriteBackground: colorScheme.ramps
|
||||
.neutral(0.5)
|
||||
.alpha(0.4)
|
||||
.hex(), // TODO: This was blend * 2
|
||||
errorColor: background(layer, "negative"),
|
||||
gutterBackground: background(layer),
|
||||
gutterPaddingFactor: 3.5,
|
||||
lineNumber: withOpacity(foreground(layer), 0.35),
|
||||
lineNumberActive: foreground(layer),
|
||||
renameFade: 0.6,
|
||||
unnecessaryCodeFade: 0.5,
|
||||
selection: colorScheme.players[0],
|
||||
guestSelections: [
|
||||
colorScheme.players[1],
|
||||
colorScheme.players[2],
|
||||
colorScheme.players[3],
|
||||
colorScheme.players[4],
|
||||
colorScheme.players[5],
|
||||
colorScheme.players[6],
|
||||
colorScheme.players[7],
|
||||
],
|
||||
autocomplete: {
|
||||
background: background(colorScheme.middle),
|
||||
cornerRadius: 8,
|
||||
padding: 4,
|
||||
margin: {
|
||||
left: -14,
|
||||
},
|
||||
border: border(colorScheme.middle),
|
||||
shadow: colorScheme.popoverShadow,
|
||||
matchHighlight: foreground(colorScheme.middle, "accent"),
|
||||
item: autocompleteItem,
|
||||
hoveredItem: {
|
||||
...autocompleteItem,
|
||||
matchHighlight: foreground(colorScheme.middle, "accent", "hovered"),
|
||||
background: background(colorScheme.middle, "hovered"),
|
||||
},
|
||||
selectedItem: {
|
||||
...autocompleteItem,
|
||||
matchHighlight: foreground(colorScheme.middle, "accent", "active"),
|
||||
background: background(colorScheme.middle, "active"),
|
||||
},
|
||||
},
|
||||
diagnosticHeader: {
|
||||
background: background(colorScheme.middle),
|
||||
iconWidthFactor: 1.5,
|
||||
textScaleFactor: 0.857,
|
||||
border: border(colorScheme.middle, {
|
||||
bottom: true,
|
||||
top: true,
|
||||
}),
|
||||
code: {
|
||||
...text(colorScheme.middle, "mono", { size: "sm" }),
|
||||
margin: {
|
||||
left: 10,
|
||||
textColor: syntax.primary.color,
|
||||
background: background(layer),
|
||||
activeLineBackground: withOpacity(background(layer, "on"), 0.75),
|
||||
highlightedLineBackground: background(layer, "on"),
|
||||
codeActions: {
|
||||
indicator: foreground(layer, "variant"),
|
||||
verticalScale: 0.55,
|
||||
},
|
||||
},
|
||||
message: {
|
||||
highlightText: text(colorScheme.middle, "sans", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
text: text(colorScheme.middle, "sans", { size: "sm" }),
|
||||
},
|
||||
},
|
||||
diagnosticPathHeader: {
|
||||
background: background(colorScheme.middle),
|
||||
textScaleFactor: 0.857,
|
||||
filename: text(colorScheme.middle, "mono", { size: "sm" }),
|
||||
path: {
|
||||
...text(colorScheme.middle, "mono", { size: "sm" }),
|
||||
margin: {
|
||||
left: 12,
|
||||
diff: {
|
||||
deleted: foreground(layer, "negative"),
|
||||
modified: foreground(layer, "warning"),
|
||||
inserted: foreground(layer, "positive"),
|
||||
removedWidthEm: 0.275,
|
||||
widthEm: 0.16,
|
||||
cornerRadius: 0.05,
|
||||
},
|
||||
},
|
||||
},
|
||||
errorDiagnostic: diagnostic(colorScheme.middle, "negative"),
|
||||
warningDiagnostic: diagnostic(colorScheme.middle, "warning"),
|
||||
informationDiagnostic: diagnostic(colorScheme.middle, "accent"),
|
||||
hintDiagnostic: diagnostic(colorScheme.middle, "warning"),
|
||||
invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
hoverPopover: hoverPopover(colorScheme),
|
||||
linkDefinition: {
|
||||
color: syntax.linkUri.color,
|
||||
underline: syntax.linkUri.underline,
|
||||
},
|
||||
jumpIcon: {
|
||||
color: foreground(layer, "on"),
|
||||
iconWidth: 20,
|
||||
buttonWidth: 20,
|
||||
cornerRadius: 6,
|
||||
padding: {
|
||||
top: 6,
|
||||
bottom: 6,
|
||||
left: 6,
|
||||
right: 6,
|
||||
},
|
||||
hover: {
|
||||
background: background(layer, "on", "hovered"),
|
||||
},
|
||||
},
|
||||
scrollbar: {
|
||||
width: 12,
|
||||
minHeightFactor: 1.0,
|
||||
track: {
|
||||
border: border(layer, "variant", { left: true }),
|
||||
},
|
||||
thumb: {
|
||||
background: withOpacity(background(layer, "inverted"), 0.4),
|
||||
border: {
|
||||
width: 1,
|
||||
color: borderColor(layer, "variant"),
|
||||
/** Highlights matching occurences of what is under the cursor
|
||||
* as well as matched brackets
|
||||
*/
|
||||
documentHighlightReadBackground: withOpacity(
|
||||
foreground(layer, "accent"),
|
||||
0.1
|
||||
),
|
||||
documentHighlightWriteBackground: colorScheme.ramps
|
||||
.neutral(0.5)
|
||||
.alpha(0.4)
|
||||
.hex(), // TODO: This was blend * 2
|
||||
errorColor: background(layer, "negative"),
|
||||
gutterBackground: background(layer),
|
||||
gutterPaddingFactor: 3.5,
|
||||
lineNumber: withOpacity(foreground(layer), 0.35),
|
||||
lineNumberActive: foreground(layer),
|
||||
renameFade: 0.6,
|
||||
unnecessaryCodeFade: 0.5,
|
||||
selection: colorScheme.players[0],
|
||||
guestSelections: [
|
||||
colorScheme.players[1],
|
||||
colorScheme.players[2],
|
||||
colorScheme.players[3],
|
||||
colorScheme.players[4],
|
||||
colorScheme.players[5],
|
||||
colorScheme.players[6],
|
||||
colorScheme.players[7],
|
||||
],
|
||||
autocomplete: {
|
||||
background: background(colorScheme.middle),
|
||||
cornerRadius: 8,
|
||||
padding: 4,
|
||||
margin: {
|
||||
left: -14,
|
||||
},
|
||||
border: border(colorScheme.middle),
|
||||
shadow: colorScheme.popoverShadow,
|
||||
matchHighlight: foreground(colorScheme.middle, "accent"),
|
||||
item: autocompleteItem,
|
||||
hoveredItem: {
|
||||
...autocompleteItem,
|
||||
matchHighlight: foreground(
|
||||
colorScheme.middle,
|
||||
"accent",
|
||||
"hovered"
|
||||
),
|
||||
background: background(colorScheme.middle, "hovered"),
|
||||
},
|
||||
selectedItem: {
|
||||
...autocompleteItem,
|
||||
matchHighlight: foreground(
|
||||
colorScheme.middle,
|
||||
"accent",
|
||||
"active"
|
||||
),
|
||||
background: background(colorScheme.middle, "active"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
compositionMark: {
|
||||
underline: {
|
||||
thickness: 1.0,
|
||||
color: borderColor(layer),
|
||||
},
|
||||
},
|
||||
syntax,
|
||||
};
|
||||
diagnosticHeader: {
|
||||
background: background(colorScheme.middle),
|
||||
iconWidthFactor: 1.5,
|
||||
textScaleFactor: 0.857,
|
||||
border: border(colorScheme.middle, {
|
||||
bottom: true,
|
||||
top: true,
|
||||
}),
|
||||
code: {
|
||||
...text(colorScheme.middle, "mono", { size: "sm" }),
|
||||
margin: {
|
||||
left: 10,
|
||||
},
|
||||
},
|
||||
message: {
|
||||
highlightText: text(colorScheme.middle, "sans", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
text: text(colorScheme.middle, "sans", { size: "sm" }),
|
||||
},
|
||||
},
|
||||
diagnosticPathHeader: {
|
||||
background: background(colorScheme.middle),
|
||||
textScaleFactor: 0.857,
|
||||
filename: text(colorScheme.middle, "mono", { size: "sm" }),
|
||||
path: {
|
||||
...text(colorScheme.middle, "mono", { size: "sm" }),
|
||||
margin: {
|
||||
left: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
errorDiagnostic: diagnostic(colorScheme.middle, "negative"),
|
||||
warningDiagnostic: diagnostic(colorScheme.middle, "warning"),
|
||||
informationDiagnostic: diagnostic(colorScheme.middle, "accent"),
|
||||
hintDiagnostic: diagnostic(colorScheme.middle, "warning"),
|
||||
invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
hoverPopover: hoverPopover(colorScheme),
|
||||
linkDefinition: {
|
||||
color: syntax.linkUri.color,
|
||||
underline: syntax.linkUri.underline,
|
||||
},
|
||||
jumpIcon: {
|
||||
color: foreground(layer, "on"),
|
||||
iconWidth: 20,
|
||||
buttonWidth: 20,
|
||||
cornerRadius: 6,
|
||||
padding: {
|
||||
top: 6,
|
||||
bottom: 6,
|
||||
left: 6,
|
||||
right: 6,
|
||||
},
|
||||
hover: {
|
||||
background: background(layer, "on", "hovered"),
|
||||
},
|
||||
},
|
||||
scrollbar: {
|
||||
width: 12,
|
||||
minHeightFactor: 1.0,
|
||||
track: {
|
||||
border: border(layer, "variant", { left: true }),
|
||||
},
|
||||
thumb: {
|
||||
background: withOpacity(background(layer, "inverted"), 0.4),
|
||||
border: {
|
||||
width: 1,
|
||||
color: borderColor(layer, "variant"),
|
||||
},
|
||||
},
|
||||
},
|
||||
compositionMark: {
|
||||
underline: {
|
||||
thickness: 1.0,
|
||||
color: borderColor(layer),
|
||||
},
|
||||
},
|
||||
syntax,
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +1,38 @@
|
||||
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, border, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
|
||||
export default function feedback(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest;
|
||||
let layer = colorScheme.highest
|
||||
|
||||
return {
|
||||
submit_button: {
|
||||
...text(layer, "mono", "on"),
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 6,
|
||||
border: border(layer, "on"),
|
||||
margin: {
|
||||
right: 4,
|
||||
},
|
||||
padding: {
|
||||
bottom: 2,
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 2,
|
||||
},
|
||||
clicked: {
|
||||
...text(layer, "mono", "on", "pressed"),
|
||||
background: background(layer, "on", "pressed"),
|
||||
border: border(layer, "on", "pressed"),
|
||||
},
|
||||
hover: {
|
||||
...text(layer, "mono", "on", "hovered"),
|
||||
background: background(layer, "on", "hovered"),
|
||||
border: border(layer, "on", "hovered"),
|
||||
},
|
||||
},
|
||||
button_margin: 8,
|
||||
info_text_default: text(layer, "sans", "default", { size: "xs" }),
|
||||
link_text_default: text(layer, "sans", "default", { size: "xs", underline: true }),
|
||||
link_text_hover: text(layer, "sans", "hovered", { size: "xs", underline: true })
|
||||
};
|
||||
return {
|
||||
submit_button: {
|
||||
...text(layer, "mono", "on"),
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 6,
|
||||
border: border(layer, "on"),
|
||||
margin: {
|
||||
right: 4,
|
||||
},
|
||||
padding: {
|
||||
bottom: 2,
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 2,
|
||||
},
|
||||
clicked: {
|
||||
...text(layer, "mono", "on", "pressed"),
|
||||
background: background(layer, "on", "pressed"),
|
||||
border: border(layer, "on", "pressed"),
|
||||
},
|
||||
hover: {
|
||||
...text(layer, "mono", "on", "hovered"),
|
||||
background: background(layer, "on", "hovered"),
|
||||
border: border(layer, "on", "hovered"),
|
||||
},
|
||||
},
|
||||
button_margin: 8,
|
||||
info_text_default: text(layer, "sans", "default", { size: "xs" }),
|
||||
link_text_default: text(layer, "sans", "default", { size: "xs", underline: true }),
|
||||
link_text_hover: text(layer, "sans", "hovered", { size: "xs", underline: true })
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +1,45 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, border, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
|
||||
export default function HoverPopover(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle;
|
||||
let baseContainer = {
|
||||
background: background(layer),
|
||||
cornerRadius: 8,
|
||||
padding: {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
},
|
||||
shadow: colorScheme.popoverShadow,
|
||||
border: border(layer),
|
||||
margin: {
|
||||
left: -8,
|
||||
},
|
||||
};
|
||||
let layer = colorScheme.middle
|
||||
let baseContainer = {
|
||||
background: background(layer),
|
||||
cornerRadius: 8,
|
||||
padding: {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
},
|
||||
shadow: colorScheme.popoverShadow,
|
||||
border: border(layer),
|
||||
margin: {
|
||||
left: -8,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
container: baseContainer,
|
||||
infoContainer: {
|
||||
...baseContainer,
|
||||
background: background(layer, "accent"),
|
||||
border: border(layer, "accent"),
|
||||
},
|
||||
warningContainer: {
|
||||
...baseContainer,
|
||||
background: background(layer, "warning"),
|
||||
border: border(layer, "warning"),
|
||||
},
|
||||
errorContainer: {
|
||||
...baseContainer,
|
||||
background: background(layer, "negative"),
|
||||
border: border(layer, "negative"),
|
||||
},
|
||||
block_style: {
|
||||
padding: { top: 4 },
|
||||
},
|
||||
prose: text(layer, "sans", { size: "sm" }),
|
||||
highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
|
||||
};
|
||||
return {
|
||||
container: baseContainer,
|
||||
infoContainer: {
|
||||
...baseContainer,
|
||||
background: background(layer, "accent"),
|
||||
border: border(layer, "accent"),
|
||||
},
|
||||
warningContainer: {
|
||||
...baseContainer,
|
||||
background: background(layer, "warning"),
|
||||
border: border(layer, "warning"),
|
||||
},
|
||||
errorContainer: {
|
||||
...baseContainer,
|
||||
background: background(layer, "negative"),
|
||||
border: border(layer, "negative"),
|
||||
},
|
||||
block_style: {
|
||||
padding: { top: 4 },
|
||||
},
|
||||
prose: text(layer, "sans", { size: "sm" }),
|
||||
highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +1,53 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, border, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
|
||||
export default function incomingCallNotification(colorScheme: ColorScheme): Object {
|
||||
let layer = colorScheme.middle;
|
||||
const avatarSize = 48;
|
||||
return {
|
||||
windowHeight: 74,
|
||||
windowWidth: 380,
|
||||
background: background(layer),
|
||||
callerContainer: {
|
||||
padding: 12,
|
||||
},
|
||||
callerAvatar: {
|
||||
height: avatarSize,
|
||||
width: avatarSize,
|
||||
cornerRadius: avatarSize / 2,
|
||||
},
|
||||
callerMetadata: {
|
||||
margin: { left: 10 },
|
||||
},
|
||||
callerUsername: {
|
||||
...text(layer, "sans", { size: "sm", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
callerMessage: {
|
||||
...text(layer, "sans", "variant", { size: "xs" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
worktreeRoots: {
|
||||
...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
buttonWidth: 96,
|
||||
acceptButton: {
|
||||
background: background(layer, "accent"),
|
||||
border: border(layer, { left: true, bottom: true }),
|
||||
...text(layer, "sans", "positive", { size: "xs", weight: "extra_bold" })
|
||||
},
|
||||
declineButton: {
|
||||
border: border(layer, { left: true }),
|
||||
...text(layer, "sans", "negative", { size: "xs", weight: "extra_bold" })
|
||||
},
|
||||
};
|
||||
export default function incomingCallNotification(
|
||||
colorScheme: ColorScheme
|
||||
): Object {
|
||||
let layer = colorScheme.middle
|
||||
const avatarSize = 48
|
||||
return {
|
||||
windowHeight: 74,
|
||||
windowWidth: 380,
|
||||
background: background(layer),
|
||||
callerContainer: {
|
||||
padding: 12,
|
||||
},
|
||||
callerAvatar: {
|
||||
height: avatarSize,
|
||||
width: avatarSize,
|
||||
cornerRadius: avatarSize / 2,
|
||||
},
|
||||
callerMetadata: {
|
||||
margin: { left: 10 },
|
||||
},
|
||||
callerUsername: {
|
||||
...text(layer, "sans", { size: "sm", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
callerMessage: {
|
||||
...text(layer, "sans", "variant", { size: "xs" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
worktreeRoots: {
|
||||
...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
buttonWidth: 96,
|
||||
acceptButton: {
|
||||
background: background(layer, "accent"),
|
||||
border: border(layer, { left: true, bottom: true }),
|
||||
...text(layer, "sans", "positive", {
|
||||
size: "xs",
|
||||
weight: "extra_bold",
|
||||
}),
|
||||
},
|
||||
declineButton: {
|
||||
border: border(layer, { left: true }),
|
||||
...text(layer, "sans", "negative", {
|
||||
size: "xs",
|
||||
weight: "extra_bold",
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,78 +1,78 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, border, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
|
||||
export default function picker(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.lowest;
|
||||
const container = {
|
||||
background: background(layer),
|
||||
border: border(layer),
|
||||
shadow: colorScheme.modalShadow,
|
||||
cornerRadius: 12,
|
||||
padding: {
|
||||
bottom: 4,
|
||||
let layer = colorScheme.lowest
|
||||
const container = {
|
||||
background: background(layer),
|
||||
border: border(layer),
|
||||
shadow: colorScheme.modalShadow,
|
||||
cornerRadius: 12,
|
||||
padding: {
|
||||
bottom: 4,
|
||||
},
|
||||
}
|
||||
};
|
||||
const inputEditor = {
|
||||
placeholderText: text(layer, "sans", "on", "disabled"),
|
||||
selection: colorScheme.players[0],
|
||||
text: text(layer, "mono", "on"),
|
||||
border: border(layer, { bottom: true }),
|
||||
padding: {
|
||||
bottom: 8,
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 8,
|
||||
},
|
||||
margin: {
|
||||
bottom: 4,
|
||||
},
|
||||
};
|
||||
const emptyInputEditor = { ...inputEditor };
|
||||
delete emptyInputEditor.border;
|
||||
delete emptyInputEditor.margin;
|
||||
const inputEditor = {
|
||||
placeholderText: text(layer, "sans", "on", "disabled"),
|
||||
selection: colorScheme.players[0],
|
||||
text: text(layer, "mono", "on"),
|
||||
border: border(layer, { bottom: true }),
|
||||
padding: {
|
||||
bottom: 8,
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 8,
|
||||
},
|
||||
margin: {
|
||||
bottom: 4,
|
||||
},
|
||||
}
|
||||
const emptyInputEditor = { ...inputEditor }
|
||||
delete emptyInputEditor.border
|
||||
delete emptyInputEditor.margin
|
||||
|
||||
return {
|
||||
...container,
|
||||
emptyContainer: {
|
||||
...container,
|
||||
padding: {}
|
||||
},
|
||||
item: {
|
||||
padding: {
|
||||
bottom: 4,
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 4,
|
||||
},
|
||||
margin: {
|
||||
top: 1,
|
||||
left: 4,
|
||||
right: 4,
|
||||
},
|
||||
cornerRadius: 8,
|
||||
text: text(layer, "sans", "variant"),
|
||||
highlightText: text(layer, "sans", "accent", { weight: "bold" }),
|
||||
active: {
|
||||
background: background(layer, "base", "active"),
|
||||
text: text(layer, "sans", "base", "active"),
|
||||
highlightText: text(layer, "sans", "accent", {
|
||||
weight: "bold",
|
||||
}),
|
||||
},
|
||||
hover: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
inputEditor,
|
||||
emptyInputEditor,
|
||||
noMatches: {
|
||||
text: text(layer, "sans", "variant"),
|
||||
padding: {
|
||||
bottom: 8,
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 8,
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
...container,
|
||||
emptyContainer: {
|
||||
...container,
|
||||
padding: {},
|
||||
},
|
||||
item: {
|
||||
padding: {
|
||||
bottom: 4,
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 4,
|
||||
},
|
||||
margin: {
|
||||
top: 1,
|
||||
left: 4,
|
||||
right: 4,
|
||||
},
|
||||
cornerRadius: 8,
|
||||
text: text(layer, "sans", "variant"),
|
||||
highlightText: text(layer, "sans", "accent", { weight: "bold" }),
|
||||
active: {
|
||||
background: background(layer, "base", "active"),
|
||||
text: text(layer, "sans", "base", "active"),
|
||||
highlightText: text(layer, "sans", "accent", {
|
||||
weight: "bold",
|
||||
}),
|
||||
},
|
||||
hover: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
inputEditor,
|
||||
emptyInputEditor,
|
||||
noMatches: {
|
||||
text: text(layer, "sans", "variant"),
|
||||
padding: {
|
||||
bottom: 8,
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 8,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, text } from "./components"
|
||||
|
||||
export default function projectDiagnostics(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest;
|
||||
return {
|
||||
background: background(layer),
|
||||
tabIconSpacing: 4,
|
||||
tabIconWidth: 13,
|
||||
tabSummarySpacing: 10,
|
||||
emptyMessage: text(layer, "sans", "variant", { size: "md" }),
|
||||
};
|
||||
let layer = colorScheme.highest
|
||||
return {
|
||||
background: background(layer),
|
||||
tabIconSpacing: 4,
|
||||
tabIconWidth: 13,
|
||||
tabSummarySpacing: 10,
|
||||
emptyMessage: text(layer, "sans", "variant", { size: "md" }),
|
||||
}
|
||||
}
|
||||
|
@ -1,60 +1,60 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { withOpacity } from "../utils/color";
|
||||
import { background, border, foreground, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { withOpacity } from "../utils/color"
|
||||
import { background, border, foreground, text } from "./components"
|
||||
|
||||
export default function projectPanel(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle;
|
||||
|
||||
let baseEntry = {
|
||||
height: 24,
|
||||
iconColor: foreground(layer, "variant"),
|
||||
iconSize: 8,
|
||||
iconSpacing: 8,
|
||||
}
|
||||
let layer = colorScheme.middle
|
||||
|
||||
let entry = {
|
||||
...baseEntry,
|
||||
text: text(layer, "mono", "variant", { size: "sm" }),
|
||||
hover: {
|
||||
background: background(layer, "variant", "hovered"),
|
||||
},
|
||||
active: {
|
||||
background: background(layer, "active"),
|
||||
text: text(layer, "mono", "active", { size: "sm" }),
|
||||
},
|
||||
activeHover: {
|
||||
background: background(layer, "active"),
|
||||
text: text(layer, "mono", "active", { size: "sm" }),
|
||||
},
|
||||
};
|
||||
let baseEntry = {
|
||||
height: 24,
|
||||
iconColor: foreground(layer, "variant"),
|
||||
iconSize: 8,
|
||||
iconSpacing: 8,
|
||||
}
|
||||
|
||||
return {
|
||||
background: background(layer),
|
||||
padding: { left: 12, right: 12, top: 6, bottom: 6 },
|
||||
indentWidth: 8,
|
||||
entry,
|
||||
draggedEntry: {
|
||||
...baseEntry,
|
||||
text: text(layer, "mono", "on", { size: "sm" }),
|
||||
background: withOpacity(background(layer, "on"), 0.9),
|
||||
border: border(layer),
|
||||
},
|
||||
ignoredEntry: {
|
||||
...entry,
|
||||
text: text(layer, "mono", "disabled"),
|
||||
},
|
||||
cutEntry: {
|
||||
...entry,
|
||||
text: text(layer, "mono", "disabled"),
|
||||
active: {
|
||||
background: background(layer, "active"),
|
||||
text: text(layer, "mono", "disabled", { size: "sm" }),
|
||||
},
|
||||
},
|
||||
filenameEditor: {
|
||||
background: background(layer, "on"),
|
||||
text: text(layer, "mono", "on", { size: "sm" }),
|
||||
selection: colorScheme.players[0],
|
||||
},
|
||||
};
|
||||
let entry = {
|
||||
...baseEntry,
|
||||
text: text(layer, "mono", "variant", { size: "sm" }),
|
||||
hover: {
|
||||
background: background(layer, "variant", "hovered"),
|
||||
},
|
||||
active: {
|
||||
background: background(layer, "active"),
|
||||
text: text(layer, "mono", "active", { size: "sm" }),
|
||||
},
|
||||
activeHover: {
|
||||
background: background(layer, "active"),
|
||||
text: text(layer, "mono", "active", { size: "sm" }),
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
background: background(layer),
|
||||
padding: { left: 12, right: 12, top: 6, bottom: 6 },
|
||||
indentWidth: 8,
|
||||
entry,
|
||||
draggedEntry: {
|
||||
...baseEntry,
|
||||
text: text(layer, "mono", "on", { size: "sm" }),
|
||||
background: withOpacity(background(layer, "on"), 0.9),
|
||||
border: border(layer),
|
||||
},
|
||||
ignoredEntry: {
|
||||
...entry,
|
||||
text: text(layer, "mono", "disabled"),
|
||||
},
|
||||
cutEntry: {
|
||||
...entry,
|
||||
text: text(layer, "mono", "disabled"),
|
||||
active: {
|
||||
background: background(layer, "active"),
|
||||
text: text(layer, "mono", "disabled", { size: "sm" }),
|
||||
},
|
||||
},
|
||||
filenameEditor: {
|
||||
background: background(layer, "on"),
|
||||
text: text(layer, "mono", "on", { size: "sm" }),
|
||||
selection: colorScheme.players[0],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +1,54 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, border, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
|
||||
export default function projectSharedNotification(colorScheme: ColorScheme): Object {
|
||||
let layer = colorScheme.middle;
|
||||
export default function projectSharedNotification(
|
||||
colorScheme: ColorScheme
|
||||
): Object {
|
||||
let layer = colorScheme.middle
|
||||
|
||||
const avatarSize = 48;
|
||||
return {
|
||||
windowHeight: 74,
|
||||
windowWidth: 380,
|
||||
background: background(layer),
|
||||
ownerContainer: {
|
||||
padding: 12,
|
||||
},
|
||||
ownerAvatar: {
|
||||
height: avatarSize,
|
||||
width: avatarSize,
|
||||
cornerRadius: avatarSize / 2,
|
||||
},
|
||||
ownerMetadata: {
|
||||
margin: { left: 10 },
|
||||
},
|
||||
ownerUsername: {
|
||||
...text(layer, "sans", { size: "sm", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
message: {
|
||||
...text(layer, "sans", "variant", { size: "xs" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
worktreeRoots: {
|
||||
...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
buttonWidth: 96,
|
||||
openButton: {
|
||||
background: background(layer, "accent"),
|
||||
border: border(layer, { left: true, bottom: true, }),
|
||||
...text(layer, "sans", "accent", { size: "xs", weight: "extra_bold" })
|
||||
},
|
||||
dismissButton: {
|
||||
border: border(layer, { left: true }),
|
||||
...text(layer, "sans", "variant", { size: "xs", weight: "extra_bold" })
|
||||
},
|
||||
};
|
||||
const avatarSize = 48
|
||||
return {
|
||||
windowHeight: 74,
|
||||
windowWidth: 380,
|
||||
background: background(layer),
|
||||
ownerContainer: {
|
||||
padding: 12,
|
||||
},
|
||||
ownerAvatar: {
|
||||
height: avatarSize,
|
||||
width: avatarSize,
|
||||
cornerRadius: avatarSize / 2,
|
||||
},
|
||||
ownerMetadata: {
|
||||
margin: { left: 10 },
|
||||
},
|
||||
ownerUsername: {
|
||||
...text(layer, "sans", { size: "sm", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
message: {
|
||||
...text(layer, "sans", "variant", { size: "xs" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
worktreeRoots: {
|
||||
...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
buttonWidth: 96,
|
||||
openButton: {
|
||||
background: background(layer, "accent"),
|
||||
border: border(layer, { left: true, bottom: true }),
|
||||
...text(layer, "sans", "accent", {
|
||||
size: "xs",
|
||||
weight: "extra_bold",
|
||||
}),
|
||||
},
|
||||
dismissButton: {
|
||||
border: border(layer, { left: true }),
|
||||
...text(layer, "sans", "variant", {
|
||||
size: "xs",
|
||||
weight: "extra_bold",
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,96 +1,94 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { withOpacity } from "../utils/color";
|
||||
import { background, border, foreground, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { withOpacity } from "../utils/color"
|
||||
import { background, border, foreground, text } from "./components"
|
||||
|
||||
export default function search(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest;
|
||||
let layer = colorScheme.highest
|
||||
|
||||
// Search input
|
||||
const editor = {
|
||||
background: background(layer),
|
||||
cornerRadius: 8,
|
||||
minWidth: 200,
|
||||
maxWidth: 500,
|
||||
placeholderText: text(layer, "mono", "disabled"),
|
||||
selection: colorScheme.players[0],
|
||||
text: text(layer, "mono", "default"),
|
||||
border: border(layer),
|
||||
margin: {
|
||||
right: 12,
|
||||
},
|
||||
padding: {
|
||||
top: 3,
|
||||
bottom: 3,
|
||||
left: 12,
|
||||
right: 8,
|
||||
},
|
||||
};
|
||||
// Search input
|
||||
const editor = {
|
||||
background: background(layer),
|
||||
cornerRadius: 8,
|
||||
minWidth: 200,
|
||||
maxWidth: 500,
|
||||
placeholderText: text(layer, "mono", "disabled"),
|
||||
selection: colorScheme.players[0],
|
||||
text: text(layer, "mono", "default"),
|
||||
border: border(layer),
|
||||
margin: {
|
||||
right: 12,
|
||||
},
|
||||
padding: {
|
||||
top: 3,
|
||||
bottom: 3,
|
||||
left: 12,
|
||||
right: 8,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
// TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive
|
||||
matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
|
||||
tabIconSpacing: 8,
|
||||
tabIconWidth: 14,
|
||||
optionButton: {
|
||||
...text(layer, "mono", "on"),
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 6,
|
||||
border: border(layer, "on"),
|
||||
margin: {
|
||||
right: 4,
|
||||
},
|
||||
padding: {
|
||||
bottom: 2,
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 2,
|
||||
},
|
||||
active: {
|
||||
...text(layer, "mono", "on", "inverted"),
|
||||
background: background(layer, "on", "inverted"),
|
||||
border: border(layer, "on", "inverted"),
|
||||
},
|
||||
clicked: {
|
||||
...text(layer, "mono", "on", "pressed"),
|
||||
background: background(layer, "on", "pressed"),
|
||||
border: border(layer, "on", "pressed"),
|
||||
},
|
||||
hover: {
|
||||
...text(layer, "mono", "on", "hovered"),
|
||||
background: background(layer, "on", "hovered"),
|
||||
border: border(layer, "on", "hovered"),
|
||||
},
|
||||
},
|
||||
editor,
|
||||
invalidEditor: {
|
||||
...editor,
|
||||
border: border(layer, "negative"),
|
||||
},
|
||||
matchIndex: {
|
||||
...text(layer, "mono", "variant"),
|
||||
padding: 6,
|
||||
},
|
||||
optionButtonGroup: {
|
||||
padding: {
|
||||
left: 12,
|
||||
right: 12,
|
||||
},
|
||||
},
|
||||
resultsStatus: {
|
||||
...text(layer, "mono", "on"),
|
||||
size: 18,
|
||||
},
|
||||
dismissButton: {
|
||||
color: foreground(layer, "variant"),
|
||||
iconWidth: 12,
|
||||
buttonWidth: 14,
|
||||
padding: {
|
||||
left: 10,
|
||||
right: 10,
|
||||
},
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
// TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive
|
||||
matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
|
||||
optionButton: {
|
||||
...text(layer, "mono", "on"),
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 6,
|
||||
border: border(layer, "on"),
|
||||
margin: {
|
||||
right: 4,
|
||||
},
|
||||
padding: {
|
||||
bottom: 2,
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 2,
|
||||
},
|
||||
active: {
|
||||
...text(layer, "mono", "on", "inverted"),
|
||||
background: background(layer, "on", "inverted"),
|
||||
border: border(layer, "on", "inverted"),
|
||||
},
|
||||
clicked: {
|
||||
...text(layer, "mono", "on", "pressed"),
|
||||
background: background(layer, "on", "pressed"),
|
||||
border: border(layer, "on", "pressed"),
|
||||
},
|
||||
hover: {
|
||||
...text(layer, "mono", "on", "hovered"),
|
||||
background: background(layer, "on", "hovered"),
|
||||
border: border(layer, "on", "hovered"),
|
||||
},
|
||||
},
|
||||
editor,
|
||||
invalidEditor: {
|
||||
...editor,
|
||||
border: border(layer, "negative"),
|
||||
},
|
||||
matchIndex: {
|
||||
...text(layer, "mono", "variant"),
|
||||
padding: 6,
|
||||
},
|
||||
optionButtonGroup: {
|
||||
padding: {
|
||||
left: 12,
|
||||
right: 12,
|
||||
},
|
||||
},
|
||||
resultsStatus: {
|
||||
...text(layer, "mono", "on"),
|
||||
size: 18,
|
||||
},
|
||||
dismissButton: {
|
||||
color: foreground(layer, "variant"),
|
||||
iconWidth: 12,
|
||||
buttonWidth: 14,
|
||||
padding: {
|
||||
left: 10,
|
||||
right: 10,
|
||||
},
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background } from "./components"
|
||||
|
||||
export default function sharedScreen(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest;
|
||||
return {
|
||||
background: background(layer)
|
||||
}
|
||||
let layer = colorScheme.highest
|
||||
return {
|
||||
background: background(layer),
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,33 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { foreground, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { foreground, text } from "./components"
|
||||
|
||||
const headerPadding = 8;
|
||||
const headerPadding = 8
|
||||
|
||||
export default function simpleMessageNotification(colorScheme: ColorScheme): Object {
|
||||
let layer = colorScheme.middle;
|
||||
return {
|
||||
message: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, right: headerPadding },
|
||||
},
|
||||
actionMessage: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, top: 6, bottom: 6 },
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
dismissButton: {
|
||||
color: foreground(layer),
|
||||
iconWidth: 8,
|
||||
iconHeight: 8,
|
||||
buttonWidth: 8,
|
||||
buttonHeight: 8,
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
};
|
||||
export default function simpleMessageNotification(
|
||||
colorScheme: ColorScheme
|
||||
): Object {
|
||||
let layer = colorScheme.middle
|
||||
return {
|
||||
message: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, right: headerPadding },
|
||||
},
|
||||
actionMessage: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, top: 6, bottom: 6 },
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
dismissButton: {
|
||||
color: foreground(layer),
|
||||
iconWidth: 8,
|
||||
iconHeight: 8,
|
||||
buttonWidth: 8,
|
||||
buttonHeight: 8,
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,118 +1,118 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, border, foreground, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, border, foreground, text } from "./components"
|
||||
|
||||
export default function statusBar(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.lowest;
|
||||
let layer = colorScheme.lowest
|
||||
|
||||
const statusContainer = {
|
||||
cornerRadius: 6,
|
||||
padding: { top: 3, bottom: 3, left: 6, right: 6 },
|
||||
};
|
||||
|
||||
const diagnosticStatusContainer = {
|
||||
cornerRadius: 6,
|
||||
padding: { top: 1, bottom: 1, left: 6, right: 6 },
|
||||
};
|
||||
|
||||
return {
|
||||
height: 30,
|
||||
itemSpacing: 8,
|
||||
padding: {
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
left: 6,
|
||||
right: 6,
|
||||
},
|
||||
border: border(layer, { top: true, overlay: true }),
|
||||
cursorPosition: text(layer, "sans", "variant"),
|
||||
autoUpdateProgressMessage: text(layer, "sans", "variant"),
|
||||
autoUpdateDoneMessage: text(layer, "sans", "variant"),
|
||||
lspStatus: {
|
||||
...diagnosticStatusContainer,
|
||||
iconSpacing: 4,
|
||||
iconWidth: 14,
|
||||
height: 18,
|
||||
message: text(layer, "sans"),
|
||||
iconColor: foreground(layer),
|
||||
hover: {
|
||||
message: text(layer, "sans"),
|
||||
iconColor: foreground(layer),
|
||||
background: background(layer),
|
||||
},
|
||||
},
|
||||
diagnosticMessage: {
|
||||
...text(layer, "sans"),
|
||||
hover: text(layer, "sans", "hovered"),
|
||||
},
|
||||
feedback: {
|
||||
...text(layer, "sans", "variant"),
|
||||
hover: text(layer, "sans", "hovered"),
|
||||
},
|
||||
diagnosticSummary: {
|
||||
height: 20,
|
||||
iconWidth: 16,
|
||||
iconSpacing: 2,
|
||||
summarySpacing: 6,
|
||||
text: text(layer, "sans", { size: "sm" }),
|
||||
iconColorOk: foreground(layer, "variant"),
|
||||
iconColorWarning: foreground(layer, "warning"),
|
||||
iconColorError: foreground(layer, "negative"),
|
||||
containerOk: {
|
||||
const statusContainer = {
|
||||
cornerRadius: 6,
|
||||
padding: { top: 3, bottom: 3, left: 7, right: 7 },
|
||||
},
|
||||
containerWarning: {
|
||||
...diagnosticStatusContainer,
|
||||
background: background(layer, "warning"),
|
||||
border: border(layer, "warning"),
|
||||
},
|
||||
containerError: {
|
||||
...diagnosticStatusContainer,
|
||||
background: background(layer, "negative"),
|
||||
border: border(layer, "negative"),
|
||||
},
|
||||
hover: {
|
||||
iconColorOk: foreground(layer, "on"),
|
||||
containerOk: {
|
||||
cornerRadius: 6,
|
||||
padding: { top: 3, bottom: 3, left: 7, right: 7 },
|
||||
background: background(layer, "on", "hovered"),
|
||||
padding: { top: 3, bottom: 3, left: 6, right: 6 },
|
||||
}
|
||||
|
||||
const diagnosticStatusContainer = {
|
||||
cornerRadius: 6,
|
||||
padding: { top: 1, bottom: 1, left: 6, right: 6 },
|
||||
}
|
||||
|
||||
return {
|
||||
height: 30,
|
||||
itemSpacing: 8,
|
||||
padding: {
|
||||
top: 1,
|
||||
bottom: 1,
|
||||
left: 6,
|
||||
right: 6,
|
||||
},
|
||||
containerWarning: {
|
||||
...diagnosticStatusContainer,
|
||||
background: background(layer, "warning", "hovered"),
|
||||
border: border(layer, "warning", "hovered"),
|
||||
border: border(layer, { top: true, overlay: true }),
|
||||
cursorPosition: text(layer, "sans", "variant"),
|
||||
autoUpdateProgressMessage: text(layer, "sans", "variant"),
|
||||
autoUpdateDoneMessage: text(layer, "sans", "variant"),
|
||||
lspStatus: {
|
||||
...diagnosticStatusContainer,
|
||||
iconSpacing: 4,
|
||||
iconWidth: 14,
|
||||
height: 18,
|
||||
message: text(layer, "sans"),
|
||||
iconColor: foreground(layer),
|
||||
hover: {
|
||||
message: text(layer, "sans"),
|
||||
iconColor: foreground(layer),
|
||||
background: background(layer),
|
||||
},
|
||||
},
|
||||
containerError: {
|
||||
...diagnosticStatusContainer,
|
||||
background: background(layer, "negative", "hovered"),
|
||||
border: border(layer, "negative", "hovered"),
|
||||
diagnosticMessage: {
|
||||
...text(layer, "sans"),
|
||||
hover: text(layer, "sans", "hovered"),
|
||||
},
|
||||
},
|
||||
},
|
||||
sidebarButtons: {
|
||||
groupLeft: {},
|
||||
groupRight: {},
|
||||
item: {
|
||||
...statusContainer,
|
||||
iconSize: 16,
|
||||
iconColor: foreground(layer, "variant"),
|
||||
hover: {
|
||||
iconColor: foreground(layer, "hovered"),
|
||||
background: background(layer, "variant"),
|
||||
feedback: {
|
||||
...text(layer, "sans", "variant"),
|
||||
hover: text(layer, "sans", "hovered"),
|
||||
},
|
||||
active: {
|
||||
iconColor: foreground(layer, "active"),
|
||||
background: background(layer, "active"),
|
||||
diagnosticSummary: {
|
||||
height: 20,
|
||||
iconWidth: 16,
|
||||
iconSpacing: 2,
|
||||
summarySpacing: 6,
|
||||
text: text(layer, "sans", { size: "sm" }),
|
||||
iconColorOk: foreground(layer, "variant"),
|
||||
iconColorWarning: foreground(layer, "warning"),
|
||||
iconColorError: foreground(layer, "negative"),
|
||||
containerOk: {
|
||||
cornerRadius: 6,
|
||||
padding: { top: 3, bottom: 3, left: 7, right: 7 },
|
||||
},
|
||||
containerWarning: {
|
||||
...diagnosticStatusContainer,
|
||||
background: background(layer, "warning"),
|
||||
border: border(layer, "warning"),
|
||||
},
|
||||
containerError: {
|
||||
...diagnosticStatusContainer,
|
||||
background: background(layer, "negative"),
|
||||
border: border(layer, "negative"),
|
||||
},
|
||||
hover: {
|
||||
iconColorOk: foreground(layer, "on"),
|
||||
containerOk: {
|
||||
cornerRadius: 6,
|
||||
padding: { top: 3, bottom: 3, left: 7, right: 7 },
|
||||
background: background(layer, "on", "hovered"),
|
||||
},
|
||||
containerWarning: {
|
||||
...diagnosticStatusContainer,
|
||||
background: background(layer, "warning", "hovered"),
|
||||
border: border(layer, "warning", "hovered"),
|
||||
},
|
||||
containerError: {
|
||||
...diagnosticStatusContainer,
|
||||
background: background(layer, "negative", "hovered"),
|
||||
border: border(layer, "negative", "hovered"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
badge: {
|
||||
cornerRadius: 3,
|
||||
padding: 2,
|
||||
margin: { bottom: -1, right: -1 },
|
||||
border: border(layer),
|
||||
background: background(layer, "accent"),
|
||||
},
|
||||
},
|
||||
};
|
||||
sidebarButtons: {
|
||||
groupLeft: {},
|
||||
groupRight: {},
|
||||
item: {
|
||||
...statusContainer,
|
||||
iconSize: 16,
|
||||
iconColor: foreground(layer, "variant"),
|
||||
hover: {
|
||||
iconColor: foreground(layer, "hovered"),
|
||||
background: background(layer, "variant"),
|
||||
},
|
||||
active: {
|
||||
iconColor: foreground(layer, "active"),
|
||||
background: background(layer, "active"),
|
||||
},
|
||||
},
|
||||
badge: {
|
||||
cornerRadius: 3,
|
||||
padding: 2,
|
||||
margin: { bottom: -1, right: -1 },
|
||||
border: border(layer),
|
||||
background: background(layer, "accent"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,103 +1,103 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { withOpacity } from "../utils/color";
|
||||
import { text, border, background, foreground } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { withOpacity } from "../utils/color"
|
||||
import { text, border, background, foreground } from "./components"
|
||||
|
||||
export default function tabBar(colorScheme: ColorScheme) {
|
||||
const height = 32;
|
||||
const height = 32
|
||||
|
||||
let activeLayer = colorScheme.highest;
|
||||
let layer = colorScheme.middle;
|
||||
let activeLayer = colorScheme.highest
|
||||
let layer = colorScheme.middle
|
||||
|
||||
const tab = {
|
||||
height,
|
||||
text: text(layer, "sans", "variant", { size: "sm" }),
|
||||
background: background(layer),
|
||||
border: border(layer, {
|
||||
right: true,
|
||||
bottom: true,
|
||||
overlay: true,
|
||||
}),
|
||||
padding: {
|
||||
left: 8,
|
||||
right: 12,
|
||||
},
|
||||
spacing: 8,
|
||||
const tab = {
|
||||
height,
|
||||
text: text(layer, "sans", "variant", { size: "sm" }),
|
||||
background: background(layer),
|
||||
border: border(layer, {
|
||||
right: true,
|
||||
bottom: true,
|
||||
overlay: true,
|
||||
}),
|
||||
padding: {
|
||||
left: 8,
|
||||
right: 12,
|
||||
},
|
||||
spacing: 8,
|
||||
|
||||
// Close icons
|
||||
iconWidth: 8,
|
||||
iconClose: foreground(layer, "variant"),
|
||||
iconCloseActive: foreground(layer, "hovered"),
|
||||
// Close icons
|
||||
iconWidth: 14,
|
||||
iconClose: foreground(layer, "variant"),
|
||||
iconCloseActive: foreground(layer, "hovered"),
|
||||
|
||||
// Indicators
|
||||
iconConflict: foreground(layer, "warning"),
|
||||
iconDirty: foreground(layer, "accent"),
|
||||
// Indicators
|
||||
iconConflict: foreground(layer, "warning"),
|
||||
iconDirty: foreground(layer, "accent"),
|
||||
|
||||
// When two tabs of the same name are open, a label appears next to them
|
||||
description: {
|
||||
margin: { left: 8 },
|
||||
...text(layer, "sans", "disabled", { size: "2xs" }),
|
||||
},
|
||||
};
|
||||
// When two tabs of the same name are open, a label appears next to them
|
||||
description: {
|
||||
margin: { left: 8 },
|
||||
...text(layer, "sans", "disabled", { size: "2xs" }),
|
||||
},
|
||||
}
|
||||
|
||||
const activePaneActiveTab = {
|
||||
...tab,
|
||||
background: background(activeLayer),
|
||||
text: text(activeLayer, "sans", "active", { size: "sm" }),
|
||||
border: {
|
||||
...tab.border,
|
||||
bottom: false,
|
||||
},
|
||||
};
|
||||
const activePaneActiveTab = {
|
||||
...tab,
|
||||
background: background(activeLayer),
|
||||
text: text(activeLayer, "sans", "active", { size: "sm" }),
|
||||
border: {
|
||||
...tab.border,
|
||||
bottom: false,
|
||||
},
|
||||
}
|
||||
|
||||
const inactivePaneInactiveTab = {
|
||||
...tab,
|
||||
background: background(layer),
|
||||
text: text(layer, "sans", "variant", { size: "sm" }),
|
||||
};
|
||||
const inactivePaneInactiveTab = {
|
||||
...tab,
|
||||
background: background(layer),
|
||||
text: text(layer, "sans", "variant", { size: "sm" }),
|
||||
}
|
||||
|
||||
const inactivePaneActiveTab = {
|
||||
...tab,
|
||||
background: background(activeLayer),
|
||||
text: text(layer, "sans", "variant", { size: "sm" }),
|
||||
border: {
|
||||
...tab.border,
|
||||
bottom: false,
|
||||
},
|
||||
};
|
||||
const inactivePaneActiveTab = {
|
||||
...tab,
|
||||
background: background(activeLayer),
|
||||
text: text(layer, "sans", "variant", { size: "sm" }),
|
||||
border: {
|
||||
...tab.border,
|
||||
bottom: false,
|
||||
},
|
||||
}
|
||||
|
||||
const draggedTab = {
|
||||
...activePaneActiveTab,
|
||||
background: withOpacity(tab.background, 0.9),
|
||||
border: undefined as any,
|
||||
shadow: colorScheme.popoverShadow,
|
||||
};
|
||||
const draggedTab = {
|
||||
...activePaneActiveTab,
|
||||
background: withOpacity(tab.background, 0.9),
|
||||
border: undefined as any,
|
||||
shadow: colorScheme.popoverShadow,
|
||||
}
|
||||
|
||||
return {
|
||||
height,
|
||||
background: background(layer),
|
||||
activePane: {
|
||||
activeTab: activePaneActiveTab,
|
||||
inactiveTab: tab,
|
||||
},
|
||||
inactivePane: {
|
||||
activeTab: inactivePaneActiveTab,
|
||||
inactiveTab: inactivePaneInactiveTab,
|
||||
},
|
||||
draggedTab,
|
||||
paneButton: {
|
||||
color: foreground(layer, "variant"),
|
||||
iconWidth: 12,
|
||||
buttonWidth: activePaneActiveTab.height,
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
paneButtonContainer: {
|
||||
background: tab.background,
|
||||
border: {
|
||||
...tab.border,
|
||||
right: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
height,
|
||||
background: background(layer),
|
||||
activePane: {
|
||||
activeTab: activePaneActiveTab,
|
||||
inactiveTab: tab,
|
||||
},
|
||||
inactivePane: {
|
||||
activeTab: inactivePaneActiveTab,
|
||||
inactiveTab: inactivePaneInactiveTab,
|
||||
},
|
||||
draggedTab,
|
||||
paneButton: {
|
||||
color: foreground(layer, "variant"),
|
||||
iconWidth: 12,
|
||||
buttonWidth: activePaneActiveTab.height,
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
paneButtonContainer: {
|
||||
background: tab.background,
|
||||
border: {
|
||||
...tab.border,
|
||||
right: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +1,52 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
|
||||
export default function terminal(colorScheme: ColorScheme) {
|
||||
/**
|
||||
* Colors are controlled per-cell in the terminal grid.
|
||||
* Cells can be set to any of these more 'theme-capable' colors
|
||||
* or can be set directly with RGB values.
|
||||
* Here are the common interpretations of these names:
|
||||
* https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
*/
|
||||
return {
|
||||
black: colorScheme.ramps.neutral(0).hex(),
|
||||
red: colorScheme.ramps.red(0.5).hex(),
|
||||
green: colorScheme.ramps.green(0.5).hex(),
|
||||
yellow: colorScheme.ramps.yellow(0.5).hex(),
|
||||
blue: colorScheme.ramps.blue(0.5).hex(),
|
||||
magenta: colorScheme.ramps.magenta(0.5).hex(),
|
||||
cyan: colorScheme.ramps.cyan(0.5).hex(),
|
||||
white: colorScheme.ramps.neutral(1).hex(),
|
||||
brightBlack: colorScheme.ramps.neutral(0.4).hex(),
|
||||
brightRed: colorScheme.ramps.red(0.25).hex(),
|
||||
brightGreen: colorScheme.ramps.green(0.25).hex(),
|
||||
brightYellow: colorScheme.ramps.yellow(0.25).hex(),
|
||||
brightBlue: colorScheme.ramps.blue(0.25).hex(),
|
||||
brightMagenta: colorScheme.ramps.magenta(0.25).hex(),
|
||||
brightCyan: colorScheme.ramps.cyan(0.25).hex(),
|
||||
brightWhite: colorScheme.ramps.neutral(1).hex(),
|
||||
/**
|
||||
* Default color for characters
|
||||
* Colors are controlled per-cell in the terminal grid.
|
||||
* Cells can be set to any of these more 'theme-capable' colors
|
||||
* or can be set directly with RGB values.
|
||||
* Here are the common interpretations of these names:
|
||||
* https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
*/
|
||||
foreground: colorScheme.ramps.neutral(1).hex(),
|
||||
/**
|
||||
* Default color for the rectangle background of a cell
|
||||
*/
|
||||
background: colorScheme.ramps.neutral(0).hex(),
|
||||
modalBackground: colorScheme.ramps.neutral(0.1).hex(),
|
||||
/**
|
||||
* Default color for the cursor
|
||||
*/
|
||||
cursor: colorScheme.players[0].cursor,
|
||||
dimBlack: colorScheme.ramps.neutral(1).hex(),
|
||||
dimRed: colorScheme.ramps.red(0.75).hex(),
|
||||
dimGreen: colorScheme.ramps.green(0.75).hex(),
|
||||
dimYellow: colorScheme.ramps.yellow(0.75).hex(),
|
||||
dimBlue: colorScheme.ramps.blue(0.75).hex(),
|
||||
dimMagenta: colorScheme.ramps.magenta(0.75).hex(),
|
||||
dimCyan: colorScheme.ramps.cyan(0.75).hex(),
|
||||
dimWhite: colorScheme.ramps.neutral(0.6).hex(),
|
||||
brightForeground: colorScheme.ramps.neutral(1).hex(),
|
||||
dimForeground: colorScheme.ramps.neutral(0).hex(),
|
||||
};
|
||||
return {
|
||||
black: colorScheme.ramps.neutral(0).hex(),
|
||||
red: colorScheme.ramps.red(0.5).hex(),
|
||||
green: colorScheme.ramps.green(0.5).hex(),
|
||||
yellow: colorScheme.ramps.yellow(0.5).hex(),
|
||||
blue: colorScheme.ramps.blue(0.5).hex(),
|
||||
magenta: colorScheme.ramps.magenta(0.5).hex(),
|
||||
cyan: colorScheme.ramps.cyan(0.5).hex(),
|
||||
white: colorScheme.ramps.neutral(1).hex(),
|
||||
brightBlack: colorScheme.ramps.neutral(0.4).hex(),
|
||||
brightRed: colorScheme.ramps.red(0.25).hex(),
|
||||
brightGreen: colorScheme.ramps.green(0.25).hex(),
|
||||
brightYellow: colorScheme.ramps.yellow(0.25).hex(),
|
||||
brightBlue: colorScheme.ramps.blue(0.25).hex(),
|
||||
brightMagenta: colorScheme.ramps.magenta(0.25).hex(),
|
||||
brightCyan: colorScheme.ramps.cyan(0.25).hex(),
|
||||
brightWhite: colorScheme.ramps.neutral(1).hex(),
|
||||
/**
|
||||
* Default color for characters
|
||||
*/
|
||||
foreground: colorScheme.ramps.neutral(1).hex(),
|
||||
/**
|
||||
* Default color for the rectangle background of a cell
|
||||
*/
|
||||
background: colorScheme.ramps.neutral(0).hex(),
|
||||
modalBackground: colorScheme.ramps.neutral(0.1).hex(),
|
||||
/**
|
||||
* Default color for the cursor
|
||||
*/
|
||||
cursor: colorScheme.players[0].cursor,
|
||||
dimBlack: colorScheme.ramps.neutral(1).hex(),
|
||||
dimRed: colorScheme.ramps.red(0.75).hex(),
|
||||
dimGreen: colorScheme.ramps.green(0.75).hex(),
|
||||
dimYellow: colorScheme.ramps.yellow(0.75).hex(),
|
||||
dimBlue: colorScheme.ramps.blue(0.75).hex(),
|
||||
dimMagenta: colorScheme.ramps.magenta(0.75).hex(),
|
||||
dimCyan: colorScheme.ramps.cyan(0.75).hex(),
|
||||
dimWhite: colorScheme.ramps.neutral(0.6).hex(),
|
||||
brightForeground: colorScheme.ramps.neutral(1).hex(),
|
||||
dimForeground: colorScheme.ramps.neutral(0).hex(),
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,23 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, border, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
|
||||
export default function tooltip(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle;
|
||||
return {
|
||||
background: background(layer),
|
||||
border: border(layer),
|
||||
padding: { top: 4, bottom: 4, left: 8, right: 8 },
|
||||
margin: { top: 6, left: 6 },
|
||||
shadow: colorScheme.popoverShadow,
|
||||
cornerRadius: 6,
|
||||
text: text(layer, "sans", { size: "xs" }),
|
||||
keystroke: {
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 4,
|
||||
margin: { left: 6 },
|
||||
padding: { left: 4, right: 4 },
|
||||
...text(layer, "mono", "on", { size: "xs", weight: "bold" }),
|
||||
},
|
||||
maxTextWidth: 200,
|
||||
};
|
||||
let layer = colorScheme.middle
|
||||
return {
|
||||
background: background(layer),
|
||||
border: border(layer),
|
||||
padding: { top: 4, bottom: 4, left: 8, right: 8 },
|
||||
margin: { top: 6, left: 6 },
|
||||
shadow: colorScheme.popoverShadow,
|
||||
cornerRadius: 6,
|
||||
text: text(layer, "sans", { size: "xs" }),
|
||||
keystroke: {
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 4,
|
||||
margin: { left: 6 },
|
||||
padding: { left: 4, right: 4 },
|
||||
...text(layer, "mono", "on", { size: "xs", weight: "bold" }),
|
||||
},
|
||||
maxTextWidth: 200,
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,31 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { foreground, text } from "./components";
|
||||
import { ColorScheme } from "../themes/common/colorScheme"
|
||||
import { foreground, text } from "./components"
|
||||
|
||||
const headerPadding = 8;
|
||||
const headerPadding = 8
|
||||
|
||||
export default function updateNotification(colorScheme: ColorScheme): Object {
|
||||
let layer = colorScheme.middle;
|
||||
return {
|
||||
message: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, right: headerPadding },
|
||||
},
|
||||
actionMessage: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, top: 6, bottom: 6 },
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
dismissButton: {
|
||||
color: foreground(layer),
|
||||
iconWidth: 8,
|
||||
iconHeight: 8,
|
||||
buttonWidth: 8,
|
||||
buttonHeight: 8,
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
};
|
||||
let layer = colorScheme.middle
|
||||
return {
|
||||
message: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, right: headerPadding },
|
||||
},
|
||||
actionMessage: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, top: 6, bottom: 6 },
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
dismissButton: {
|
||||
color: foreground(layer),
|
||||
iconWidth: 8,
|
||||
iconHeight: 8,
|
||||
buttonWidth: 8,
|
||||
buttonHeight: 8,
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|