Merge branch 'main' into fix-broken-lsp-installations
This commit is contained in:
commit
48bed2ee03
516 changed files with 12747 additions and 3368 deletions
|
@ -321,7 +321,7 @@ impl View for ActivityIndicator {
|
|||
let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
|
||||
let theme = &theme::current(cx).workspace.status_bar.lsp_status;
|
||||
let style = if state.hovered() && on_click.is_some() {
|
||||
theme.hover.as_ref().unwrap_or(&theme.default)
|
||||
theme.hovered.as_ref().unwrap_or(&theme.default)
|
||||
} else {
|
||||
&theme.default
|
||||
};
|
||||
|
|
|
@ -22,9 +22,10 @@ util = { path = "../util" }
|
|||
workspace = { path = "../workspace" }
|
||||
|
||||
anyhow.workspace = true
|
||||
chrono = "0.4"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
futures.workspace = true
|
||||
isahc.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
@ -33,3 +34,4 @@ tiktoken-rs = "0.4"
|
|||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
pub mod assistant;
|
||||
mod assistant_settings;
|
||||
|
||||
use anyhow::Result;
|
||||
pub use assistant::AssistantPanel;
|
||||
use chrono::{DateTime, Local};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::AppContext;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
fmt::{self, Display},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
// Data types for chat completion requests
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -14,6 +26,84 @@ struct OpenAIRequest {
|
|||
stream: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
struct MessageId(usize);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct MessageMetadata {
|
||||
role: Role,
|
||||
sent_at: DateTime<Local>,
|
||||
status: MessageStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum MessageStatus {
|
||||
Pending,
|
||||
Done,
|
||||
Error(Arc<str>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedMessage {
|
||||
id: MessageId,
|
||||
start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversation {
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
model: String,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
const VERSION: &'static str = "0.1.0";
|
||||
}
|
||||
|
||||
struct SavedConversationMetadata {
|
||||
title: String,
|
||||
path: PathBuf,
|
||||
mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
struct RequestMessage {
|
||||
role: Role,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -49,7 +49,7 @@ impl View for UpdateNotification {
|
|||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
let style = theme.dismiss_button.style_for(state);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
|
@ -74,7 +74,7 @@ impl View for UpdateNotification {
|
|||
),
|
||||
)
|
||||
.with_child({
|
||||
let style = theme.action_message.style_for(state, false);
|
||||
let style = theme.action_message.style_for(state);
|
||||
Text::new("View the release notes", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
|
|
@ -83,7 +83,7 @@ impl View for Breadcrumbs {
|
|||
}
|
||||
|
||||
MouseEventHandler::<Breadcrumbs, Breadcrumbs>::new(0, cx, |state, _| {
|
||||
let style = style.style_for(state, false);
|
||||
let style = style.style_for(state);
|
||||
crumbs.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
|
|
|
@ -3,6 +3,7 @@ use client::{proto, User};
|
|||
use collections::HashMap;
|
||||
use gpui::WeakModelHandle;
|
||||
pub use live_kit_client::Frame;
|
||||
use live_kit_client::RemoteAudioTrack;
|
||||
use project::Project;
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
|
@ -42,7 +43,10 @@ pub struct RemoteParticipant {
|
|||
pub peer_id: proto::PeerId,
|
||||
pub projects: Vec<proto::ParticipantProject>,
|
||||
pub location: ParticipantLocation,
|
||||
pub tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
|
||||
pub muted: bool,
|
||||
pub speaking: bool,
|
||||
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
|
||||
pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -12,7 +12,10 @@ use fs::Fs;
|
|||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
||||
use language::LanguageRegistry;
|
||||
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
|
||||
use live_kit_client::{
|
||||
LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
|
||||
RemoteVideoTrackUpdate,
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
use project::Project;
|
||||
use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
|
||||
|
@ -28,6 +31,9 @@ pub enum Event {
|
|||
RemoteVideoTracksChanged {
|
||||
participant_id: proto::PeerId,
|
||||
},
|
||||
RemoteAudioTracksChanged {
|
||||
participant_id: proto::PeerId,
|
||||
},
|
||||
RemoteProjectShared {
|
||||
owner: Arc<User>,
|
||||
project_id: u64,
|
||||
|
@ -112,9 +118,9 @@ impl Room {
|
|||
}
|
||||
});
|
||||
|
||||
let mut track_changes = room.remote_video_track_updates();
|
||||
let _maintain_tracks = cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some(track_change) = track_changes.next().await {
|
||||
let mut track_video_changes = room.remote_video_track_updates();
|
||||
let _maintain_video_tracks = cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some(track_change) = track_video_changes.next().await {
|
||||
let this = if let Some(this) = this.upgrade(&cx) {
|
||||
this
|
||||
} else {
|
||||
|
@ -127,16 +133,41 @@ impl Room {
|
|||
}
|
||||
});
|
||||
|
||||
cx.foreground()
|
||||
.spawn(room.connect(&connection_info.server_url, &connection_info.token))
|
||||
.detach_and_log_err(cx);
|
||||
let mut track_audio_changes = room.remote_audio_track_updates();
|
||||
let _maintain_audio_tracks = cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some(track_change) = track_audio_changes.next().await {
|
||||
let this = if let Some(this) = this.upgrade(&cx) {
|
||||
this
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.remote_audio_track_updated(track_change, cx).log_err()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let connect = room.connect(&connection_info.server_url, &connection_info.token);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
connect.await?;
|
||||
this.update(&mut cx, |this, cx| this.share_microphone(cx))
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Some(LiveKitRoom {
|
||||
room,
|
||||
screen_track: ScreenTrack::None,
|
||||
screen_track: LocalTrack::None,
|
||||
microphone_track: LocalTrack::None,
|
||||
next_publish_id: 0,
|
||||
muted_by_user: false,
|
||||
deafened: false,
|
||||
speaking: false,
|
||||
_maintain_room,
|
||||
_maintain_tracks,
|
||||
_maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
@ -618,20 +649,32 @@ impl Room {
|
|||
peer_id,
|
||||
projects: participant.projects,
|
||||
location,
|
||||
tracks: Default::default(),
|
||||
muted: false,
|
||||
speaking: false,
|
||||
video_tracks: Default::default(),
|
||||
audio_tracks: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(live_kit) = this.live_kit.as_ref() {
|
||||
let tracks =
|
||||
let video_tracks =
|
||||
live_kit.room.remote_video_tracks(&user.id.to_string());
|
||||
for track in tracks {
|
||||
let audio_tracks =
|
||||
live_kit.room.remote_audio_tracks(&user.id.to_string());
|
||||
for track in video_tracks {
|
||||
this.remote_video_track_updated(
|
||||
RemoteVideoTrackUpdate::Subscribed(track),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
for track in audio_tracks {
|
||||
this.remote_audio_track_updated(
|
||||
RemoteAudioTrackUpdate::Subscribed(track),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -706,7 +749,7 @@ impl Room {
|
|||
.remote_participants
|
||||
.get_mut(&user_id)
|
||||
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
|
||||
participant.tracks.insert(
|
||||
participant.video_tracks.insert(
|
||||
track_id.clone(),
|
||||
Arc::new(RemoteVideoTrack {
|
||||
live_kit_track: track,
|
||||
|
@ -725,7 +768,7 @@ impl Room {
|
|||
.remote_participants
|
||||
.get_mut(&user_id)
|
||||
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
|
||||
participant.tracks.remove(&track_id);
|
||||
participant.video_tracks.remove(&track_id);
|
||||
cx.emit(Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
|
@ -736,6 +779,84 @@ impl Room {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn remote_audio_track_updated(
|
||||
&mut self,
|
||||
change: RemoteAudioTrackUpdate,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
match change {
|
||||
RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } => {
|
||||
let mut speaker_ids = speakers
|
||||
.into_iter()
|
||||
.filter_map(|speaker_sid| speaker_sid.parse().ok())
|
||||
.collect::<Vec<u64>>();
|
||||
speaker_ids.sort_unstable();
|
||||
for (sid, participant) in &mut self.remote_participants {
|
||||
if let Ok(_) = speaker_ids.binary_search(sid) {
|
||||
participant.speaking = true;
|
||||
} else {
|
||||
participant.speaking = false;
|
||||
}
|
||||
}
|
||||
if let Some(id) = self.client.user_id() {
|
||||
if let Some(room) = &mut self.live_kit {
|
||||
if let Ok(_) = speaker_ids.binary_search(&id) {
|
||||
room.speaking = true;
|
||||
} else {
|
||||
room.speaking = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => {
|
||||
for participant in &mut self.remote_participants.values_mut() {
|
||||
let mut found = false;
|
||||
for track in participant.audio_tracks.values() {
|
||||
if track.sid() == track_id {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if found {
|
||||
participant.muted = muted;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
RemoteAudioTrackUpdate::Subscribed(track) => {
|
||||
let user_id = track.publisher_id().parse()?;
|
||||
let track_id = track.sid().to_string();
|
||||
let participant = self
|
||||
.remote_participants
|
||||
.get_mut(&user_id)
|
||||
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
|
||||
participant.audio_tracks.insert(track_id.clone(), track);
|
||||
cx.emit(Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
}
|
||||
RemoteAudioTrackUpdate::Unsubscribed {
|
||||
publisher_id,
|
||||
track_id,
|
||||
} => {
|
||||
let user_id = publisher_id.parse()?;
|
||||
let participant = self
|
||||
.remote_participants
|
||||
.get_mut(&user_id)
|
||||
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
|
||||
participant.audio_tracks.remove(&track_id);
|
||||
cx.emit(Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_invariants(&self) {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
{
|
||||
|
@ -908,7 +1029,116 @@ impl Room {
|
|||
|
||||
pub fn is_screen_sharing(&self) -> bool {
|
||||
self.live_kit.as_ref().map_or(false, |live_kit| {
|
||||
!matches!(live_kit.screen_track, ScreenTrack::None)
|
||||
!matches!(live_kit.screen_track, LocalTrack::None)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_sharing_mic(&self) -> bool {
|
||||
self.live_kit.as_ref().map_or(false, |live_kit| {
|
||||
!matches!(live_kit.microphone_track, LocalTrack::None)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_muted(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.and_then(|live_kit| match &live_kit.microphone_track {
|
||||
LocalTrack::None => None,
|
||||
LocalTrack::Pending { muted, .. } => Some(*muted),
|
||||
LocalTrack::Published { muted, .. } => Some(*muted),
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_speaking(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.map_or(false, |live_kit| live_kit.speaking)
|
||||
}
|
||||
|
||||
pub fn is_deafened(&self) -> Option<bool> {
|
||||
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
|
||||
}
|
||||
|
||||
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
} else if self.is_sharing_mic() {
|
||||
return Task::ready(Err(anyhow!("microphone was already shared")));
|
||||
}
|
||||
|
||||
let publish_id = if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
let publish_id = post_inc(&mut live_kit.next_publish_id);
|
||||
live_kit.microphone_track = LocalTrack::Pending {
|
||||
publish_id,
|
||||
muted: false,
|
||||
};
|
||||
cx.notify();
|
||||
publish_id
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||
};
|
||||
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
let publish_track = async {
|
||||
let track = LocalAudioTrack::create();
|
||||
this.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.read_with(&cx, |this, _| {
|
||||
this.live_kit
|
||||
.as_ref()
|
||||
.map(|live_kit| live_kit.room.publish_audio_track(&track))
|
||||
})
|
||||
.ok_or_else(|| anyhow!("live-kit was not initialized"))?
|
||||
.await
|
||||
};
|
||||
|
||||
let publication = publish_track.await;
|
||||
this.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
let live_kit = this
|
||||
.live_kit
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
|
||||
|
||||
let (canceled, muted) = if let LocalTrack::Pending {
|
||||
publish_id: cur_publish_id,
|
||||
muted,
|
||||
} = &live_kit.microphone_track
|
||||
{
|
||||
(*cur_publish_id != publish_id, *muted)
|
||||
} else {
|
||||
(true, false)
|
||||
};
|
||||
|
||||
match publication {
|
||||
Ok(publication) => {
|
||||
if canceled {
|
||||
live_kit.room.unpublish_track(publication);
|
||||
} else {
|
||||
if muted {
|
||||
cx.background().spawn(publication.set_mute(muted)).detach();
|
||||
}
|
||||
live_kit.microphone_track = LocalTrack::Published {
|
||||
track_publication: publication,
|
||||
muted,
|
||||
};
|
||||
cx.notify();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
if canceled {
|
||||
Ok(())
|
||||
} else {
|
||||
live_kit.microphone_track = LocalTrack::None;
|
||||
cx.notify();
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -921,7 +1151,10 @@ impl Room {
|
|||
|
||||
let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
let publish_id = post_inc(&mut live_kit.next_publish_id);
|
||||
live_kit.screen_track = ScreenTrack::Pending { publish_id };
|
||||
live_kit.screen_track = LocalTrack::Pending {
|
||||
publish_id,
|
||||
muted: false,
|
||||
};
|
||||
cx.notify();
|
||||
(live_kit.room.display_sources(), publish_id)
|
||||
} else {
|
||||
|
@ -955,13 +1188,14 @@ impl Room {
|
|||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
|
||||
|
||||
let canceled = if let ScreenTrack::Pending {
|
||||
let (canceled, muted) = if let LocalTrack::Pending {
|
||||
publish_id: cur_publish_id,
|
||||
muted,
|
||||
} = &live_kit.screen_track
|
||||
{
|
||||
*cur_publish_id != publish_id
|
||||
(*cur_publish_id != publish_id, *muted)
|
||||
} else {
|
||||
true
|
||||
(true, false)
|
||||
};
|
||||
|
||||
match publication {
|
||||
|
@ -969,7 +1203,13 @@ impl Room {
|
|||
if canceled {
|
||||
live_kit.room.unpublish_track(publication);
|
||||
} else {
|
||||
live_kit.screen_track = ScreenTrack::Published(publication);
|
||||
if muted {
|
||||
cx.background().spawn(publication.set_mute(muted)).detach();
|
||||
}
|
||||
live_kit.screen_track = LocalTrack::Published {
|
||||
track_publication: publication,
|
||||
muted,
|
||||
};
|
||||
cx.notify();
|
||||
}
|
||||
Ok(())
|
||||
|
@ -978,7 +1218,7 @@ impl Room {
|
|||
if canceled {
|
||||
Ok(())
|
||||
} else {
|
||||
live_kit.screen_track = ScreenTrack::None;
|
||||
live_kit.screen_track = LocalTrack::None;
|
||||
cx.notify();
|
||||
Err(error)
|
||||
}
|
||||
|
@ -987,6 +1227,77 @@ impl Room {
|
|||
})
|
||||
})
|
||||
}
|
||||
fn set_mute(
|
||||
live_kit: &mut LiveKitRoom,
|
||||
should_mute: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<Task<Result<()>>> {
|
||||
if !should_mute {
|
||||
// clear user muting state.
|
||||
live_kit.muted_by_user = false;
|
||||
}
|
||||
match &mut live_kit.microphone_track {
|
||||
LocalTrack::None => Err(anyhow!("microphone was not shared")),
|
||||
LocalTrack::Pending { muted, .. } => {
|
||||
*muted = should_mute;
|
||||
cx.notify();
|
||||
Ok(Task::Ready(Some(Ok(()))))
|
||||
}
|
||||
LocalTrack::Published {
|
||||
track_publication,
|
||||
muted,
|
||||
} => {
|
||||
*muted = should_mute;
|
||||
cx.notify();
|
||||
Ok(cx.background().spawn(track_publication.set_mute(*muted)))
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
|
||||
let should_mute = !self.is_muted();
|
||||
if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
let ret = Self::set_mute(live_kit, should_mute, cx);
|
||||
live_kit.muted_by_user = should_mute;
|
||||
ret
|
||||
} else {
|
||||
Err(anyhow!("LiveKit not started"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
|
||||
if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
(*live_kit).deafened = !live_kit.deafened;
|
||||
|
||||
let mut tasks = Vec::with_capacity(self.remote_participants.len());
|
||||
// Context notification is sent within set_mute itself.
|
||||
let mut mute_task = None;
|
||||
// When deafening, mute user's mic as well.
|
||||
// When undeafening, unmute user's mic unless it was manually muted prior to deafening.
|
||||
if live_kit.deafened || !live_kit.muted_by_user {
|
||||
mute_task = Some(Self::set_mute(live_kit, live_kit.deafened, cx)?);
|
||||
};
|
||||
for participant in self.remote_participants.values() {
|
||||
for track in live_kit
|
||||
.room
|
||||
.remote_audio_track_publications(&participant.user.id.to_string())
|
||||
{
|
||||
tasks.push(cx.foreground().spawn(track.set_enabled(!live_kit.deafened)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cx.foreground().spawn(async move {
|
||||
if let Some(mute_task) = mute_task {
|
||||
mute_task.await?;
|
||||
}
|
||||
for task in tasks {
|
||||
task.await?;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
} else {
|
||||
Err(anyhow!("LiveKit not started"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
if self.status.is_offline() {
|
||||
|
@ -998,13 +1309,15 @@ impl Room {
|
|||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
|
||||
match mem::take(&mut live_kit.screen_track) {
|
||||
ScreenTrack::None => Err(anyhow!("screen was not shared")),
|
||||
ScreenTrack::Pending { .. } => {
|
||||
LocalTrack::None => Err(anyhow!("screen was not shared")),
|
||||
LocalTrack::Pending { .. } => {
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
ScreenTrack::Published(track) => {
|
||||
live_kit.room.unpublish_track(track);
|
||||
LocalTrack::Published {
|
||||
track_publication, ..
|
||||
} => {
|
||||
live_kit.room.unpublish_track(track_publication);
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1023,19 +1336,30 @@ impl Room {
|
|||
|
||||
struct LiveKitRoom {
|
||||
room: Arc<live_kit_client::Room>,
|
||||
screen_track: ScreenTrack,
|
||||
screen_track: LocalTrack,
|
||||
microphone_track: LocalTrack,
|
||||
/// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.
|
||||
muted_by_user: bool,
|
||||
deafened: bool,
|
||||
speaking: bool,
|
||||
next_publish_id: usize,
|
||||
_maintain_room: Task<()>,
|
||||
_maintain_tracks: Task<()>,
|
||||
_maintain_tracks: [Task<()>; 2],
|
||||
}
|
||||
|
||||
enum ScreenTrack {
|
||||
enum LocalTrack {
|
||||
None,
|
||||
Pending { publish_id: usize },
|
||||
Published(LocalTrackPublication),
|
||||
Pending {
|
||||
publish_id: usize,
|
||||
muted: bool,
|
||||
},
|
||||
Published {
|
||||
track_publication: LocalTrackPublication,
|
||||
muted: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for ScreenTrack {
|
||||
impl Default for LocalTrack {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{executor::Background, serde_json, AppContext, Task};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
|
@ -8,7 +7,6 @@ use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
|
|||
use tempfile::NamedTempFile;
|
||||
use util::http::HttpClient;
|
||||
use util::{channel::ReleaseChannel, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Telemetry {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
|
@ -120,39 +118,15 @@ impl Telemetry {
|
|||
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||
}
|
||||
|
||||
pub fn start(self: &Arc<Self>) {
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let installation_id =
|
||||
if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
|
||||
installation_id
|
||||
} else {
|
||||
let installation_id = Uuid::new_v4().to_string();
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp("device_id".to_string(), installation_id.clone())
|
||||
.await?;
|
||||
installation_id
|
||||
};
|
||||
pub fn start(self: &Arc<Self>, installation_id: Option<String>) {
|
||||
let mut state = self.state.lock();
|
||||
state.installation_id = installation_id.map(|id| id.into());
|
||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
||||
drop(state);
|
||||
|
||||
let installation_id: Arc<str> = installation_id.into();
|
||||
let mut state = this.state.lock();
|
||||
state.installation_id = Some(installation_id.clone());
|
||||
|
||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
||||
|
||||
drop(state);
|
||||
|
||||
if has_clickhouse_events {
|
||||
this.flush_clickhouse_events();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
if has_clickhouse_events {
|
||||
self.flush_clickhouse_events();
|
||||
}
|
||||
}
|
||||
|
||||
/// This method takes the entire TelemetrySettings struct in order to force client code
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
|||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.14.2"
|
||||
version = "0.15.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -74,6 +74,7 @@ CREATE TABLE "worktree_entries" (
|
|||
"mtime_seconds" INTEGER NOT NULL,
|
||||
"mtime_nanos" INTEGER NOT NULL,
|
||||
"is_symlink" BOOL NOT NULL,
|
||||
"is_external" BOOL NOT NULL,
|
||||
"is_ignored" BOOL NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
"git_status" INTEGER,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "worktree_entries"
|
||||
ADD "is_external" BOOL NOT NULL DEFAULT FALSE;
|
|
@ -1539,6 +1539,7 @@ impl Database {
|
|||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
is_external: db_entry.is_external,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
});
|
||||
}
|
||||
|
@ -2349,6 +2350,7 @@ impl Database {
|
|||
mtime_nanos: ActiveValue::set(mtime.nanos as i32),
|
||||
is_symlink: ActiveValue::set(entry.is_symlink),
|
||||
is_ignored: ActiveValue::set(entry.is_ignored),
|
||||
is_external: ActiveValue::set(entry.is_external),
|
||||
git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
|
@ -2705,6 +2707,7 @@ impl Database {
|
|||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
is_external: db_entry.is_external,
|
||||
git_status: db_entry.git_status.map(|status| status as i32),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ pub struct Model {
|
|||
pub git_status: Option<i64>,
|
||||
pub is_symlink: bool,
|
||||
pub is_ignored: bool,
|
||||
pub is_external: bool,
|
||||
pub is_deleted: bool,
|
||||
pub scan_id: i64,
|
||||
}
|
||||
|
|
|
@ -224,6 +224,7 @@ impl Server {
|
|||
.add_request_handler(forward_project_request::<proto::RenameProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::CopyProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::ExpandProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
|
||||
.add_message_handler(create_buffer_for_peer)
|
||||
.add_request_handler(update_buffer)
|
||||
|
|
|
@ -257,7 +257,7 @@ async fn test_basic_calls(
|
|||
room_b.read_with(cx_b, |room, _| {
|
||||
assert_eq!(
|
||||
room.remote_participants()[&client_a.user_id().unwrap()]
|
||||
.tracks
|
||||
.video_tracks
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
@ -274,7 +274,7 @@ async fn test_basic_calls(
|
|||
room_c.read_with(cx_c, |room, _| {
|
||||
assert_eq!(
|
||||
room.remote_participants()[&client_a.user_id().unwrap()]
|
||||
.tracks
|
||||
.video_tracks
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
@ -1266,6 +1266,27 @@ async fn test_share_project(
|
|||
let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
|
||||
assert_eq!(client_b_collaborator.replica_id, replica_id_b);
|
||||
});
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
let worktree = project.worktrees(cx).next().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
|
||||
[
|
||||
Path::new(".gitignore"),
|
||||
Path::new("a.txt"),
|
||||
Path::new("b.txt"),
|
||||
Path::new("ignored-dir"),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
let worktree = project.worktrees(cx).next().unwrap();
|
||||
let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap();
|
||||
project.expand_entry(worktree_id, entry.id, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
let worktree = project.worktrees(cx).next().unwrap().read(cx);
|
||||
assert_eq!(
|
||||
|
@ -6993,7 +7014,7 @@ async fn test_join_call_after_screen_was_shared(
|
|||
room.remote_participants()
|
||||
.get(&client_a.user_id().unwrap())
|
||||
.unwrap()
|
||||
.tracks
|
||||
.video_tracks
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
|
|
@ -37,8 +37,10 @@ picker = { path = "../picker" }
|
|||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
theme_selector = { path = "../theme_selector" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
zed-actions = {path = "../zed-actions"}
|
||||
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
|
||||
toggle_screen_sharing, ToggleScreenSharing,
|
||||
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
|
||||
ToggleScreenSharing,
|
||||
};
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
|
||||
|
@ -17,13 +18,13 @@ use gpui::{
|
|||
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::Project;
|
||||
use project::{Project, RepositoryEntry};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use theme::{AvatarStyle, Theme};
|
||||
use util::ResultExt;
|
||||
use workspace::{FollowNextCollaborator, Workspace};
|
||||
|
||||
const MAX_TITLE_LENGTH: usize = 75;
|
||||
// const MAX_TITLE_LENGTH: usize = 75;
|
||||
|
||||
actions!(
|
||||
collab,
|
||||
|
@ -78,27 +79,34 @@ impl View for CollabTitlebarItem {
|
|||
let user = self.user_store.read(cx).current_user();
|
||||
let peer_id = self.client.peer_id();
|
||||
if let Some(((user, peer_id), room)) = user
|
||||
.as_ref()
|
||||
.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));
|
||||
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
|
||||
right_container.add_child(self.render_leave_call(&theme, cx));
|
||||
let muted = room.read(cx).is_muted();
|
||||
let speaking = room.read(cx).is_speaking();
|
||||
left_container.add_child(
|
||||
self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
|
||||
);
|
||||
left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
|
||||
right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
|
||||
right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
|
||||
right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
|
||||
}
|
||||
|
||||
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));
|
||||
right_container.add_child(self.render_user_menu_button(&theme, cx));
|
||||
let avatar = user.as_ref().and_then(|user| user.avatar.clone());
|
||||
right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
|
||||
} else {
|
||||
right_container.add_children(self.render_connection_status(status, cx));
|
||||
right_container.add_child(self.render_sign_in_button(&theme, cx));
|
||||
right_container.add_child(self.render_user_menu_button(&theme, None, cx));
|
||||
}
|
||||
|
||||
Stack::new()
|
||||
|
@ -108,7 +116,6 @@ impl View for CollabTitlebarItem {
|
|||
.with_child(
|
||||
right_container.contained().with_background_color(
|
||||
theme
|
||||
.workspace
|
||||
.titlebar
|
||||
.container
|
||||
.background_color
|
||||
|
@ -163,7 +170,6 @@ impl CollabTitlebarItem {
|
|||
}),
|
||||
);
|
||||
|
||||
let view_id = cx.view_id();
|
||||
Self {
|
||||
workspace: workspace.weak_handle(),
|
||||
project,
|
||||
|
@ -171,6 +177,7 @@ impl CollabTitlebarItem {
|
|||
client,
|
||||
contacts_popover: None,
|
||||
user_menu: cx.add_view(|cx| {
|
||||
let view_id = cx.view_id();
|
||||
let mut menu = ContextMenu::new(view_id, cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
|
@ -185,55 +192,45 @@ impl CollabTitlebarItem {
|
|||
theme: Arc<Theme>,
|
||||
cx: &ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let names_and_branches = project.visible_worktrees(cx).map(|worktree| {
|
||||
let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
(worktree.root_name(), worktree.root_git_entry())
|
||||
});
|
||||
|
||||
fn push_str(buffer: &mut String, index: &mut usize, str: &str) {
|
||||
buffer.push_str(str);
|
||||
*index += str.chars().count();
|
||||
}
|
||||
|
||||
let mut indices = Vec::new();
|
||||
let mut index = 0;
|
||||
let mut title = String::new();
|
||||
let mut names_and_branches = names_and_branches.peekable();
|
||||
while let Some((name, entry)) = names_and_branches.next() {
|
||||
let pre_index = index;
|
||||
push_str(&mut title, &mut index, name);
|
||||
indices.extend((pre_index..index).into_iter());
|
||||
if let Some(branch) = entry.and_then(|entry| entry.branch()) {
|
||||
push_str(&mut title, &mut index, "/");
|
||||
push_str(&mut title, &mut index, &branch);
|
||||
}
|
||||
if names_and_branches.peek().is_some() {
|
||||
push_str(&mut title, &mut index, ", ");
|
||||
if index >= MAX_TITLE_LENGTH {
|
||||
title.push_str(" …");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let text_style = theme.workspace.titlebar.title.clone();
|
||||
let item_spacing = theme.workspace.titlebar.item_spacing;
|
||||
let (name, entry) = names_and_branches.next().unwrap_or(("", None));
|
||||
let branch_prepended = entry
|
||||
.as_ref()
|
||||
.and_then(RepositoryEntry::branch)
|
||||
.map(|branch| format!("/{branch}"));
|
||||
let text_style = theme.titlebar.title.clone();
|
||||
let item_spacing = theme.titlebar.item_spacing;
|
||||
|
||||
let mut highlight = text_style.clone();
|
||||
highlight.color = theme.workspace.titlebar.highlight_color;
|
||||
highlight.color = theme.titlebar.highlight_color;
|
||||
|
||||
let style = LabelStyle {
|
||||
text: text_style,
|
||||
highlight_text: Some(highlight),
|
||||
};
|
||||
|
||||
Label::new(title, style)
|
||||
.with_highlights(indices)
|
||||
.contained()
|
||||
.with_margin_right(item_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.into_any_named("title-with-git-information")
|
||||
let mut ret = Flex::row().with_child(
|
||||
Label::new(name.to_owned(), style.clone())
|
||||
.with_highlights((0..name.len()).into_iter().collect())
|
||||
.contained()
|
||||
.aligned()
|
||||
.left()
|
||||
.into_any_named("title-project-name"),
|
||||
);
|
||||
if let Some(git_branch) = branch_prepended {
|
||||
ret = ret.with_child(
|
||||
Label::new(git_branch, style)
|
||||
.contained()
|
||||
.with_margin_right(item_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.into_any_named("title-project-branch"),
|
||||
)
|
||||
}
|
||||
ret.into_any()
|
||||
}
|
||||
|
||||
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
|
@ -297,45 +294,29 @@ impl CollabTitlebarItem {
|
|||
}
|
||||
|
||||
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
|
||||
let theme = theme::current(cx).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() {
|
||||
let items = if let Some(_) = 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(),
|
||||
))
|
||||
.contained()
|
||||
.with_style(item_style.container)
|
||||
.into_any()
|
||||
})),
|
||||
ContextMenuItem::action("Sign out", SignOut),
|
||||
ContextMenuItem::action("Settings", zed_actions::OpenSettings),
|
||||
ContextMenuItem::action("Theme", theme_selector::Toggle),
|
||||
ContextMenuItem::separator(),
|
||||
ContextMenuItem::action(
|
||||
"Send Feedback",
|
||||
"Share Feedback",
|
||||
feedback::feedback_editor::GiveFeedback,
|
||||
),
|
||||
ContextMenuItem::action("Sign out", SignOut),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
ContextMenuItem::action("Sign in", SignIn),
|
||||
ContextMenuItem::action("Settings", zed_actions::OpenSettings),
|
||||
ContextMenuItem::action("Theme", theme_selector::Toggle),
|
||||
ContextMenuItem::separator(),
|
||||
ContextMenuItem::action(
|
||||
"Send Feedback",
|
||||
"Share Feedback",
|
||||
feedback::feedback_editor::GiveFeedback,
|
||||
),
|
||||
]
|
||||
};
|
||||
|
||||
user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx);
|
||||
});
|
||||
}
|
||||
|
@ -345,7 +326,7 @@ impl CollabTitlebarItem {
|
|||
theme: &Theme,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
let titlebar = &theme.titlebar;
|
||||
|
||||
let badge = if self
|
||||
.user_store
|
||||
|
@ -361,8 +342,20 @@ impl CollabTitlebarItem {
|
|||
.contained()
|
||||
.with_style(titlebar.toggle_contacts_badge)
|
||||
.contained()
|
||||
.with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
|
||||
.with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
|
||||
.with_margin_left(
|
||||
titlebar
|
||||
.toggle_contacts_button
|
||||
.inactive_state()
|
||||
.default
|
||||
.icon_width,
|
||||
)
|
||||
.with_margin_top(
|
||||
titlebar
|
||||
.toggle_contacts_button
|
||||
.inactive_state()
|
||||
.default
|
||||
.icon_width,
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
};
|
||||
|
@ -372,8 +365,9 @@ impl CollabTitlebarItem {
|
|||
MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
|
||||
let style = titlebar
|
||||
.toggle_contacts_button
|
||||
.style_for(state, self.contacts_popover.is_some());
|
||||
Svg::new("icons/user_plus_16.svg")
|
||||
.in_state(self.contacts_popover.is_some())
|
||||
.style_for(state);
|
||||
Svg::new("icons/radix/person.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
|
@ -400,7 +394,6 @@ impl CollabTitlebarItem {
|
|||
.with_children(self.render_contacts_popover_host(titlebar, cx))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_toggle_screen_sharing_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
|
@ -410,16 +403,21 @@ impl CollabTitlebarItem {
|
|||
let icon;
|
||||
let tooltip;
|
||||
if room.read(cx).is_screen_sharing() {
|
||||
icon = "icons/enable_screen_sharing_12.svg";
|
||||
icon = "icons/radix/desktop.svg";
|
||||
tooltip = "Stop Sharing Screen"
|
||||
} else {
|
||||
icon = "icons/disable_screen_sharing_12.svg";
|
||||
icon = "icons/radix/desktop.svg";
|
||||
tooltip = "Share Screen";
|
||||
}
|
||||
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
let active = room.read(cx).is_screen_sharing();
|
||||
let titlebar = &theme.titlebar;
|
||||
MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
|
||||
let style = titlebar.call_control.style_for(state, false);
|
||||
let style = titlebar
|
||||
.screen_share_button
|
||||
.in_state(active)
|
||||
.style_for(state);
|
||||
|
||||
Svg::new(icon)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
|
@ -445,7 +443,141 @@ impl CollabTitlebarItem {
|
|||
.aligned()
|
||||
.into_any()
|
||||
}
|
||||
fn render_toggle_mute(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
room: &ModelHandle<Room>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let icon;
|
||||
let tooltip;
|
||||
let is_muted = room.read(cx).is_muted();
|
||||
if is_muted {
|
||||
icon = "icons/radix/mic-mute.svg";
|
||||
tooltip = "Unmute microphone\nRight click for options";
|
||||
} else {
|
||||
icon = "icons/radix/mic.svg";
|
||||
tooltip = "Mute microphone\nRight click for options";
|
||||
}
|
||||
|
||||
let titlebar = &theme.titlebar;
|
||||
MouseEventHandler::<ToggleMute, Self>::new(0, cx, |state, _| {
|
||||
let style = titlebar
|
||||
.toggle_microphone_button
|
||||
.in_state(is_muted)
|
||||
.style_for(state);
|
||||
let image = 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);
|
||||
if let Some(color) = style.container.background_color {
|
||||
image.with_background_color(color)
|
||||
} else {
|
||||
image
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
toggle_mute(&Default::default(), cx)
|
||||
})
|
||||
.with_tooltip::<ToggleMute>(
|
||||
0,
|
||||
tooltip.into(),
|
||||
Some(Box::new(ToggleMute)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.into_any()
|
||||
}
|
||||
fn render_toggle_deafen(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
room: &ModelHandle<Room>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let icon;
|
||||
let tooltip;
|
||||
let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
|
||||
if is_deafened {
|
||||
icon = "icons/radix/speaker-off.svg";
|
||||
tooltip = "Unmute speakers\nRight click for options";
|
||||
} else {
|
||||
icon = "icons/radix/speaker-loud.svg";
|
||||
tooltip = "Mute speakers\nRight click for options";
|
||||
}
|
||||
|
||||
let titlebar = &theme.titlebar;
|
||||
MouseEventHandler::<ToggleDeafen, Self>::new(0, cx, |state, _| {
|
||||
let style = titlebar
|
||||
.toggle_speakers_button
|
||||
.in_state(is_deafened)
|
||||
.style_for(state);
|
||||
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)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
toggle_deafen(&Default::default(), cx)
|
||||
})
|
||||
.with_tooltip::<ToggleDeafen>(
|
||||
0,
|
||||
tooltip.into(),
|
||||
Some(Box::new(ToggleDeafen)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.into_any()
|
||||
}
|
||||
fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let icon = "icons/radix/exit.svg";
|
||||
let tooltip = "Leave call";
|
||||
|
||||
let titlebar = &theme.titlebar;
|
||||
MouseEventHandler::<LeaveCall, Self>::new(0, cx, |state, _| {
|
||||
let style = titlebar.leave_call_button.style_for(state);
|
||||
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)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_tooltip::<LeaveCall>(
|
||||
0,
|
||||
tooltip.into(),
|
||||
Some(Box::new(LeaveCall)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.into_any()
|
||||
}
|
||||
fn render_in_call_share_unshare_button(
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
|
@ -458,14 +590,14 @@ impl CollabTitlebarItem {
|
|||
}
|
||||
|
||||
let is_shared = project.read(cx).is_shared();
|
||||
let label = if is_shared { "Unshare" } else { "Share" };
|
||||
let label = if is_shared { "Stop Sharing" } else { "Share" };
|
||||
let tooltip = if is_shared {
|
||||
"Unshare project from call participants"
|
||||
"Stop sharing project with call participants"
|
||||
} else {
|
||||
"Share project with call participants"
|
||||
};
|
||||
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
let titlebar = &theme.titlebar;
|
||||
|
||||
enum ShareUnshare {}
|
||||
Some(
|
||||
|
@ -473,7 +605,7 @@ impl CollabTitlebarItem {
|
|||
.with_child(
|
||||
MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
|
||||
//TODO: Ensure this button has consistent width for both text variations
|
||||
let style = titlebar.share_button.style_for(state, false);
|
||||
let style = titlebar.share_button.inactive_state().style_for(state);
|
||||
Label::new(label, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
@ -496,7 +628,7 @@ impl CollabTitlebarItem {
|
|||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing)
|
||||
.with_margin_left(theme.titlebar.item_spacing)
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
@ -504,24 +636,51 @@ impl CollabTitlebarItem {
|
|||
fn render_user_menu_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
avatar: Option<Arc<ImageData>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
let tooltip = theme.tooltip.clone();
|
||||
let user_menu_button_style = if avatar.is_some() {
|
||||
&theme.titlebar.user_menu.user_menu_button_online
|
||||
} else {
|
||||
&theme.titlebar.user_menu.user_menu_button_offline
|
||||
};
|
||||
|
||||
let avatar_style = &user_menu_button_style.avatar;
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ToggleUserMenu, Self>::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)
|
||||
let style = user_menu_button_style
|
||||
.user_menu
|
||||
.inactive_state()
|
||||
.style_for(state);
|
||||
|
||||
let mut dropdown = Flex::row().align_children_center();
|
||||
|
||||
if let Some(avatar_img) = avatar {
|
||||
dropdown = dropdown.with_child(Self::render_face(
|
||||
avatar_img,
|
||||
*avatar_style,
|
||||
Color::transparent_black(),
|
||||
None,
|
||||
));
|
||||
};
|
||||
|
||||
dropdown
|
||||
.with_child(
|
||||
Svg::new("icons/caret_down_8.svg")
|
||||
.with_color(user_menu_button_style.icon.color)
|
||||
.constrained()
|
||||
.with_width(user_menu_button_style.icon.width)
|
||||
.contained()
|
||||
.into_any(),
|
||||
)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.with_height(style.width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.into_any()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
|
@ -531,11 +690,10 @@ impl CollabTitlebarItem {
|
|||
0,
|
||||
"Toggle user menu".to_owned(),
|
||||
Some(Box::new(ToggleUserMenu)),
|
||||
theme.tooltip.clone(),
|
||||
tooltip,
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing),
|
||||
.contained(),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.user_menu, cx)
|
||||
|
@ -547,9 +705,9 @@ impl CollabTitlebarItem {
|
|||
}
|
||||
|
||||
fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
let titlebar = &theme.titlebar;
|
||||
MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
|
||||
let style = titlebar.sign_in_prompt.style_for(state, false);
|
||||
let style = titlebar.sign_in_button.inactive_state().style_for(state);
|
||||
Label::new("Sign In", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
@ -611,11 +769,13 @@ impl CollabTitlebarItem {
|
|||
replica_id,
|
||||
participant.peer_id,
|
||||
Some(participant.location),
|
||||
participant.muted,
|
||||
participant.speaking,
|
||||
workspace,
|
||||
theme,
|
||||
cx,
|
||||
))
|
||||
.with_margin_right(theme.workspace.titlebar.face_pile_spacing),
|
||||
.with_margin_right(theme.titlebar.face_pile_spacing),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
|
@ -627,19 +787,24 @@ impl CollabTitlebarItem {
|
|||
theme: &Theme,
|
||||
user: &Arc<User>,
|
||||
peer_id: PeerId,
|
||||
muted: bool,
|
||||
speaking: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let replica_id = workspace.read(cx).project().read(cx).replica_id();
|
||||
|
||||
Container::new(self.render_face_pile(
|
||||
user,
|
||||
Some(replica_id),
|
||||
peer_id,
|
||||
None,
|
||||
muted,
|
||||
speaking,
|
||||
workspace,
|
||||
theme,
|
||||
cx,
|
||||
))
|
||||
.with_margin_right(theme.workspace.titlebar.item_spacing)
|
||||
.with_margin_right(theme.titlebar.item_spacing)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
|
@ -649,6 +814,8 @@ impl CollabTitlebarItem {
|
|||
replica_id: Option<ReplicaId>,
|
||||
peer_id: PeerId,
|
||||
location: Option<ParticipantLocation>,
|
||||
muted: bool,
|
||||
speaking: bool,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
theme: &Theme,
|
||||
cx: &mut ViewContext<Self>,
|
||||
|
@ -671,15 +838,23 @@ impl CollabTitlebarItem {
|
|||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let leader_style = theme.workspace.titlebar.leader_avatar;
|
||||
let follower_style = theme.workspace.titlebar.follower_avatar;
|
||||
let leader_style = theme.titlebar.leader_avatar;
|
||||
let follower_style = theme.titlebar.follower_avatar;
|
||||
|
||||
let microphone_state = if muted {
|
||||
Some(theme.titlebar.muted)
|
||||
} else if speaking {
|
||||
Some(theme.titlebar.speaking)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut background_color = theme
|
||||
.workspace
|
||||
.titlebar
|
||||
.container
|
||||
.background_color
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(replica_id) = replica_id {
|
||||
if followed_by_self {
|
||||
let selection = theme.editor.replica_selection_style(replica_id).selection;
|
||||
|
@ -690,11 +865,12 @@ impl CollabTitlebarItem {
|
|||
|
||||
let mut content = Stack::new()
|
||||
.with_children(user.avatar.as_ref().map(|avatar| {
|
||||
let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
|
||||
let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
|
||||
.with_child(Self::render_face(
|
||||
avatar.clone(),
|
||||
Self::location_style(workspace, location, leader_style, cx),
|
||||
background_color,
|
||||
microphone_state,
|
||||
))
|
||||
.with_children(
|
||||
(|| {
|
||||
|
@ -726,6 +902,7 @@ impl CollabTitlebarItem {
|
|||
avatar.clone(),
|
||||
follower_style,
|
||||
background_color,
|
||||
None,
|
||||
))
|
||||
}))
|
||||
})()
|
||||
|
@ -735,7 +912,7 @@ impl CollabTitlebarItem {
|
|||
|
||||
let mut container = face_pile
|
||||
.contained()
|
||||
.with_style(theme.workspace.titlebar.leader_selection);
|
||||
.with_style(theme.titlebar.leader_selection);
|
||||
|
||||
if let Some(replica_id) = replica_id {
|
||||
if followed_by_self {
|
||||
|
@ -752,8 +929,8 @@ impl CollabTitlebarItem {
|
|||
Some(
|
||||
AvatarRibbon::new(color)
|
||||
.constrained()
|
||||
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
|
||||
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
|
||||
.with_width(theme.titlebar.avatar_ribbon.width)
|
||||
.with_height(theme.titlebar.avatar_ribbon.height)
|
||||
.aligned()
|
||||
.bottom(),
|
||||
)
|
||||
|
@ -844,12 +1021,13 @@ impl CollabTitlebarItem {
|
|||
avatar: Arc<ImageData>,
|
||||
avatar_style: AvatarStyle,
|
||||
background_color: Color,
|
||||
microphone_state: Option<Color>,
|
||||
) -> AnyElement<V> {
|
||||
Image::from_data(avatar)
|
||||
.with_style(avatar_style.image)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_background_color(background_color)
|
||||
.with_background_color(microphone_state.unwrap_or(background_color))
|
||||
.with_corner_radius(avatar_style.outer_corner_radius)
|
||||
.constrained()
|
||||
.with_width(avatar_style.outer_width)
|
||||
|
@ -873,22 +1051,22 @@ impl CollabTitlebarItem {
|
|||
| client::Status::Reconnecting { .. }
|
||||
| client::Status::ReconnectionError { .. } => Some(
|
||||
Svg::new("icons/cloud_slash_12.svg")
|
||||
.with_color(theme.workspace.titlebar.offline_icon.color)
|
||||
.with_color(theme.titlebar.offline_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.workspace.titlebar.offline_icon.width)
|
||||
.with_width(theme.titlebar.offline_icon.width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.workspace.titlebar.offline_icon.container)
|
||||
.with_style(theme.titlebar.offline_icon.container)
|
||||
.into_any(),
|
||||
),
|
||||
client::Status::UpgradeRequired => Some(
|
||||
MouseEventHandler::<ConnectionStatusButton, Self>::new(0, cx, |_, _| {
|
||||
Label::new(
|
||||
"Please update Zed to collaborate",
|
||||
theme.workspace.titlebar.outdated_warning.text.clone(),
|
||||
theme.titlebar.outdated_warning.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.workspace.titlebar.outdated_warning.container)
|
||||
.with_style(theme.titlebar.outdated_warning.container)
|
||||
.aligned()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
|
|
|
@ -9,13 +9,23 @@ mod notifications;
|
|||
mod project_shared_notification;
|
||||
mod sharing_status_indicator;
|
||||
|
||||
use call::ActiveCall;
|
||||
use call::{ActiveCall, Room};
|
||||
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
|
||||
use gpui::{actions, AppContext, Task};
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
actions!(collab, [ToggleScreenSharing]);
|
||||
actions!(
|
||||
collab,
|
||||
[
|
||||
ToggleScreenSharing,
|
||||
ToggleMute,
|
||||
ToggleDeafen,
|
||||
LeaveCall,
|
||||
ShareMicrophone
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
collab_titlebar_item::init(cx);
|
||||
|
@ -27,6 +37,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||
sharing_status_indicator::init(cx);
|
||||
|
||||
cx.add_global_action(toggle_screen_sharing);
|
||||
cx.add_global_action(toggle_mute);
|
||||
cx.add_global_action(toggle_deafen);
|
||||
cx.add_global_action(share_microphone);
|
||||
}
|
||||
|
||||
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
|
@ -41,3 +54,26 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
|||
toggle_screen_sharing.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
room.update(cx, Room::toggle_mute)
|
||||
.map(|task| task.detach_and_log_err(cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
room.update(cx, Room::toggle_deafen)
|
||||
.map(|task| task.detach_and_log_err(cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn share_microphone(_: &ShareMicrophone, cx: &mut AppContext) {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
room.update(cx, Room::share_microphone)
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,7 +117,8 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
.contact_finder
|
||||
.picker
|
||||
.item
|
||||
.style_for(mouse_state, selected);
|
||||
.in_state(selected)
|
||||
.style_for(mouse_state);
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
|
|
|
@ -514,10 +514,10 @@ impl ContactList {
|
|||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
host_user_id: participant.user.id,
|
||||
is_last: projects.peek().is_none() && participant.tracks.is_empty(),
|
||||
is_last: projects.peek().is_none() && participant.video_tracks.is_empty(),
|
||||
});
|
||||
}
|
||||
if !participant.tracks.is_empty() {
|
||||
if !participant.video_tracks.is_empty() {
|
||||
participant_entries.push(ContactEntry::ParticipantScreen {
|
||||
peer_id: participant.peer_id,
|
||||
is_last: true,
|
||||
|
@ -774,7 +774,8 @@ impl ContactList {
|
|||
.with_style(
|
||||
*theme
|
||||
.contact_row
|
||||
.style_for(&mut Default::default(), is_selected),
|
||||
.in_state(is_selected)
|
||||
.style_for(&mut Default::default()),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
@ -797,7 +798,7 @@ impl ContactList {
|
|||
.width
|
||||
.or(theme.contact_avatar.height)
|
||||
.unwrap_or(0.);
|
||||
let row = &theme.project_row.default;
|
||||
let row = &theme.project_row.inactive_state().default;
|
||||
let tree_branch = theme.tree_branch;
|
||||
let line_height = row.name.text.line_height(font_cache);
|
||||
let cap_height = row.name.text.cap_height(font_cache);
|
||||
|
@ -810,8 +811,11 @@ impl ContactList {
|
|||
};
|
||||
|
||||
MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
|
||||
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
|
||||
let row = theme.project_row.style_for(mouse_state, is_selected);
|
||||
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
||||
let row = theme
|
||||
.project_row
|
||||
.in_state(is_selected)
|
||||
.style_for(mouse_state);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
|
@ -893,7 +897,7 @@ impl ContactList {
|
|||
.width
|
||||
.or(theme.contact_avatar.height)
|
||||
.unwrap_or(0.);
|
||||
let row = &theme.project_row.default;
|
||||
let row = &theme.project_row.inactive_state().default;
|
||||
let tree_branch = theme.tree_branch;
|
||||
let line_height = row.name.text.line_height(font_cache);
|
||||
let cap_height = row.name.text.cap_height(font_cache);
|
||||
|
@ -904,8 +908,11 @@ impl ContactList {
|
|||
peer_id.as_u64() as usize,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
|
||||
let row = theme.project_row.style_for(mouse_state, is_selected);
|
||||
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
||||
let row = theme
|
||||
.project_row
|
||||
.in_state(is_selected)
|
||||
.style_for(mouse_state);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
|
@ -989,7 +996,8 @@ impl ContactList {
|
|||
|
||||
let header_style = theme
|
||||
.header_row
|
||||
.style_for(&mut Default::default(), is_selected);
|
||||
.in_state(is_selected)
|
||||
.style_for(&mut Default::default());
|
||||
let text = match section {
|
||||
Section::ActiveCall => "Collaborators",
|
||||
Section::Requests => "Contact Requests",
|
||||
|
@ -999,7 +1007,7 @@ impl ContactList {
|
|||
let leave_call = if section == Section::ActiveCall {
|
||||
Some(
|
||||
MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |state, _| {
|
||||
let style = theme.leave_call.style_for(state, false);
|
||||
let style = theme.leave_call.style_for(state);
|
||||
Label::new("Leave Call", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
@ -1110,8 +1118,7 @@ impl ContactList {
|
|||
contact.user.id as usize,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
let button_style =
|
||||
theme.contact_button.style_for(mouse_state, false);
|
||||
let button_style = theme.contact_button.style_for(mouse_state);
|
||||
render_icon_button(button_style, "icons/x_mark_8.svg")
|
||||
.aligned()
|
||||
.flex_float()
|
||||
|
@ -1146,7 +1153,8 @@ impl ContactList {
|
|||
.with_style(
|
||||
*theme
|
||||
.contact_row
|
||||
.style_for(&mut Default::default(), is_selected),
|
||||
.in_state(is_selected)
|
||||
.style_for(&mut Default::default()),
|
||||
)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
|
@ -1204,7 +1212,7 @@ impl ContactList {
|
|||
let button_style = if is_contact_request_pending {
|
||||
&theme.disabled_button
|
||||
} else {
|
||||
theme.contact_button.style_for(mouse_state, false)
|
||||
theme.contact_button.style_for(mouse_state)
|
||||
};
|
||||
render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
|
||||
})
|
||||
|
@ -1227,7 +1235,7 @@ impl ContactList {
|
|||
let button_style = if is_contact_request_pending {
|
||||
&theme.disabled_button
|
||||
} else {
|
||||
theme.contact_button.style_for(mouse_state, false)
|
||||
theme.contact_button.style_for(mouse_state)
|
||||
};
|
||||
render_icon_button(button_style, "icons/check_8.svg")
|
||||
.aligned()
|
||||
|
@ -1250,7 +1258,7 @@ impl ContactList {
|
|||
let button_style = if is_contact_request_pending {
|
||||
&theme.disabled_button
|
||||
} else {
|
||||
theme.contact_button.style_for(mouse_state, false)
|
||||
theme.contact_button.style_for(mouse_state)
|
||||
};
|
||||
render_icon_button(button_style, "icons/x_mark_8.svg")
|
||||
.aligned()
|
||||
|
@ -1277,7 +1285,8 @@ impl ContactList {
|
|||
.with_style(
|
||||
*theme
|
||||
.contact_row
|
||||
.style_for(&mut Default::default(), is_selected),
|
||||
.in_state(is_selected)
|
||||
.style_for(&mut Default::default()),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ where
|
|||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Dismiss, V>::new(user.id as usize, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
let style = theme.dismiss_button.style_for(state);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
|
@ -93,7 +93,7 @@ where
|
|||
.with_children(buttons.into_iter().enumerate().map(
|
||||
|(ix, (message, handler))| {
|
||||
MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
|
||||
let button = theme.button.style_for(state, false);
|
||||
let button = theme.button.style_for(state);
|
||||
Label::new(message, button.text.clone())
|
||||
.contained()
|
||||
.with_style(button.container)
|
||||
|
|
|
@ -185,8 +185,8 @@ impl PickerDelegate for CommandPaletteDelegate {
|
|||
let mat = &self.matches[ix];
|
||||
let command = &self.actions[mat.candidate_id];
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||
let key_style = &theme.command_palette.key.style_for(mouse_state, selected);
|
||||
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
||||
let key_style = &theme.command_palette.key.in_state(selected);
|
||||
let keystroke_spacing = theme.command_palette.keystroke_spacing;
|
||||
|
||||
Flex::row()
|
||||
|
|
|
@ -328,10 +328,8 @@ impl ContextMenu {
|
|||
Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||
match item {
|
||||
ContextMenuItem::Item { label, .. } => {
|
||||
let style = style.item.style_for(
|
||||
&mut Default::default(),
|
||||
Some(ix) == self.selected_index,
|
||||
);
|
||||
let style = style.item.in_state(self.selected_index == Some(ix));
|
||||
let style = style.style_for(&mut Default::default());
|
||||
|
||||
match label {
|
||||
ContextMenuItemLabel::String(label) => {
|
||||
|
@ -363,10 +361,8 @@ impl ContextMenu {
|
|||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||
match item {
|
||||
ContextMenuItem::Item { action, .. } => {
|
||||
let style = style.item.style_for(
|
||||
&mut Default::default(),
|
||||
Some(ix) == self.selected_index,
|
||||
);
|
||||
let style = style.item.in_state(self.selected_index == Some(ix));
|
||||
let style = style.style_for(&mut Default::default());
|
||||
|
||||
match action {
|
||||
ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
|
||||
|
@ -412,8 +408,8 @@ impl ContextMenu {
|
|||
let action = action.clone();
|
||||
let view_id = self.parent_view_id;
|
||||
MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
|
||||
let style =
|
||||
style.item.style_for(state, Some(ix) == self.selected_index);
|
||||
let style = style.item.in_state(self.selected_index == Some(ix));
|
||||
let style = style.style_for(state);
|
||||
let keystroke = match &action {
|
||||
ContextMenuItemAction::Action(action) => Some(
|
||||
KeystrokeLabel::new(
|
||||
|
|
|
@ -127,16 +127,16 @@ impl CopilotCodeVerification {
|
|||
.with_child(
|
||||
Label::new(
|
||||
if copied { "Copied!" } else { "Copy" },
|
||||
device_code_style.cta.style_for(state, false).text.clone(),
|
||||
device_code_style.cta.style_for(state).text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(*device_code_style.right_container.style_for(state, false))
|
||||
.with_style(*device_code_style.right_container.style_for(state))
|
||||
.constrained()
|
||||
.with_width(device_code_style.right),
|
||||
)
|
||||
.contained()
|
||||
.with_style(device_code_style.cta.style_for(state, false).container)
|
||||
.with_style(device_code_style.cta.style_for(state).container)
|
||||
})
|
||||
.on_click(gpui::platform::MouseButton::Left, {
|
||||
let user_code = data.user_code.clone();
|
||||
|
|
|
@ -71,7 +71,8 @@ impl View for CopilotButton {
|
|||
.status_bar
|
||||
.panel_buttons
|
||||
.button
|
||||
.style_for(state, active);
|
||||
.in_state(active)
|
||||
.style_for(state);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
|
@ -255,7 +256,7 @@ impl CopilotButton {
|
|||
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
|
||||
Flex::row()
|
||||
.with_child(Label::new("Copilot Settings", style.label.clone()))
|
||||
.with_child(theme::ui::icon(icon_style.style_for(state, false)))
|
||||
.with_child(theme::ui::icon(icon_style.style_for(state)))
|
||||
.align_children_center()
|
||||
.into_any()
|
||||
},
|
||||
|
|
|
@ -1509,7 +1509,8 @@ mod tests {
|
|||
let snapshot = editor.snapshot(cx);
|
||||
snapshot
|
||||
.blocks_in_range(0..snapshot.max_point().row())
|
||||
.filter_map(|(row, block)| {
|
||||
.enumerate()
|
||||
.filter_map(|(ix, (row, block))| {
|
||||
let name = match block {
|
||||
TransformBlock::Custom(block) => block
|
||||
.render(&mut BlockContext {
|
||||
|
@ -1520,6 +1521,7 @@ mod tests {
|
|||
gutter_width: 0.,
|
||||
line_height: 0.,
|
||||
em_width: 0.,
|
||||
block_id: ix,
|
||||
})
|
||||
.name()?
|
||||
.to_string(),
|
||||
|
|
|
@ -100,7 +100,7 @@ impl View for DiagnosticIndicator {
|
|||
.workspace
|
||||
.status_bar
|
||||
.diagnostic_summary
|
||||
.style_for(state, false);
|
||||
.style_for(state);
|
||||
|
||||
let mut summary_row = Flex::row();
|
||||
if self.summary.error_count > 0 {
|
||||
|
@ -198,7 +198,7 @@ impl View for DiagnosticIndicator {
|
|||
MouseEventHandler::<Message, _>::new(1, cx, |state, _| {
|
||||
Label::new(
|
||||
diagnostic.message.split('\n').next().unwrap().to_string(),
|
||||
message_style.style_for(state, false).text.clone(),
|
||||
message_style.style_for(state).text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
|
|
|
@ -88,6 +88,7 @@ pub struct BlockContext<'a, 'b, 'c> {
|
|||
pub gutter_padding: f32,
|
||||
pub em_width: f32,
|
||||
pub line_height: f32,
|
||||
pub block_id: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
|
|
@ -206,6 +206,7 @@ actions!(
|
|||
DuplicateLine,
|
||||
MoveLineUp,
|
||||
MoveLineDown,
|
||||
JoinLines,
|
||||
Transpose,
|
||||
Cut,
|
||||
Copy,
|
||||
|
@ -321,6 +322,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
cx.add_action(Editor::indent);
|
||||
cx.add_action(Editor::outdent);
|
||||
cx.add_action(Editor::delete_line);
|
||||
cx.add_action(Editor::join_lines);
|
||||
cx.add_action(Editor::delete_to_previous_word_start);
|
||||
cx.add_action(Editor::delete_to_previous_subword_start);
|
||||
cx.add_action(Editor::delete_to_next_word_end);
|
||||
|
@ -3320,15 +3322,21 @@ impl Editor {
|
|||
pub fn render_code_actions_indicator(
|
||||
&self,
|
||||
style: &EditorStyle,
|
||||
active: bool,
|
||||
is_active: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement<Self>> {
|
||||
if self.available_code_actions.is_some() {
|
||||
enum CodeActions {}
|
||||
Some(
|
||||
MouseEventHandler::<CodeActions, _>::new(0, cx, |state, _| {
|
||||
Svg::new("icons/bolt_8.svg")
|
||||
.with_color(style.code_actions.indicator.style_for(state, active).color)
|
||||
Svg::new("icons/bolt_8.svg").with_color(
|
||||
style
|
||||
.code_actions
|
||||
.indicator
|
||||
.in_state(is_active)
|
||||
.style_for(state)
|
||||
.color,
|
||||
)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_padding(Padding::uniform(3.))
|
||||
|
@ -3378,10 +3386,8 @@ impl Editor {
|
|||
.with_color(
|
||||
style
|
||||
.indicator
|
||||
.style_for(
|
||||
mouse_state,
|
||||
fold_status == FoldStatus::Folded,
|
||||
)
|
||||
.in_state(fold_status == FoldStatus::Folded)
|
||||
.style_for(mouse_state)
|
||||
.color,
|
||||
)
|
||||
.constrained()
|
||||
|
@ -3952,6 +3958,60 @@ impl Editor {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
|
||||
let mut row_ranges = Vec::<Range<u32>>::new();
|
||||
for selection in self.selections.all::<Point>(cx) {
|
||||
let start = selection.start.row;
|
||||
let end = if selection.start.row == selection.end.row {
|
||||
selection.start.row + 1
|
||||
} else {
|
||||
selection.end.row
|
||||
};
|
||||
|
||||
if let Some(last_row_range) = row_ranges.last_mut() {
|
||||
if start <= last_row_range.end {
|
||||
last_row_range.end = end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
row_ranges.push(start..end);
|
||||
}
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let mut cursor_positions = Vec::new();
|
||||
for row_range in &row_ranges {
|
||||
let anchor = snapshot.anchor_before(Point::new(
|
||||
row_range.end - 1,
|
||||
snapshot.line_len(row_range.end - 1),
|
||||
));
|
||||
cursor_positions.push(anchor.clone()..anchor);
|
||||
}
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
for row_range in row_ranges.into_iter().rev() {
|
||||
for row in row_range.rev() {
|
||||
let end_of_line = Point::new(row, snapshot.line_len(row));
|
||||
let indent = snapshot.indent_size_for_line(row + 1);
|
||||
let start_of_next_line = Point::new(row + 1, indent.len);
|
||||
|
||||
let replace = if snapshot.line_len(row + 1) > indent.len {
|
||||
" "
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_anchor_ranges(cursor_positions)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
|
@ -7581,8 +7641,14 @@ impl View for Editor {
|
|||
keymap.add_identifier("renaming");
|
||||
}
|
||||
match self.context_menu.as_ref() {
|
||||
Some(ContextMenu::Completions(_)) => keymap.add_identifier("showing_completions"),
|
||||
Some(ContextMenu::CodeActions(_)) => keymap.add_identifier("showing_code_actions"),
|
||||
Some(ContextMenu::Completions(_)) => {
|
||||
keymap.add_identifier("menu");
|
||||
keymap.add_identifier("showing_completions")
|
||||
}
|
||||
Some(ContextMenu::CodeActions(_)) => {
|
||||
keymap.add_identifier("menu");
|
||||
keymap.add_identifier("showing_code_actions")
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
for layer in self.keymap_context_layers.values() {
|
||||
|
@ -7949,6 +8015,7 @@ impl Deref for EditorStyle {
|
|||
|
||||
pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock {
|
||||
let mut highlighted_lines = Vec::new();
|
||||
|
||||
for (index, line) in diagnostic.message.lines().enumerate() {
|
||||
let line = match &diagnostic.source {
|
||||
Some(source) if index == 0 => {
|
||||
|
@ -7960,25 +8027,44 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
|||
};
|
||||
highlighted_lines.push(line);
|
||||
}
|
||||
|
||||
let message = diagnostic.message;
|
||||
Arc::new(move |cx: &mut BlockContext| {
|
||||
let message = message.clone();
|
||||
let settings = settings::get::<ThemeSettings>(cx);
|
||||
let tooltip_style = settings.theme.tooltip.clone();
|
||||
let theme = &settings.theme.editor;
|
||||
let style = diagnostic_style(diagnostic.severity, is_valid, theme);
|
||||
let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
|
||||
Flex::column()
|
||||
.with_children(highlighted_lines.iter().map(|(line, highlights)| {
|
||||
Label::new(
|
||||
line.clone(),
|
||||
style.message.clone().with_font_size(font_size),
|
||||
)
|
||||
.with_highlights(highlights.clone())
|
||||
.contained()
|
||||
.with_margin_left(cx.anchor_x)
|
||||
}))
|
||||
.aligned()
|
||||
.left()
|
||||
.into_any()
|
||||
let anchor_x = cx.anchor_x;
|
||||
enum BlockContextToolip {}
|
||||
MouseEventHandler::<BlockContext, _>::new(cx.block_id, cx, |_, _| {
|
||||
Flex::column()
|
||||
.with_children(highlighted_lines.iter().map(|(line, highlights)| {
|
||||
Label::new(
|
||||
line.clone(),
|
||||
style.message.clone().with_font_size(font_size),
|
||||
)
|
||||
.with_highlights(highlights.clone())
|
||||
.contained()
|
||||
.with_margin_left(anchor_x)
|
||||
}))
|
||||
.aligned()
|
||||
.left()
|
||||
.into_any()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
|
||||
})
|
||||
// We really need to rethink this ID system...
|
||||
.with_tooltip::<BlockContextToolip>(
|
||||
cx.block_id,
|
||||
"Copy diagnostic message".to_string(),
|
||||
None,
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
use super::*;
|
||||
use crate::test::{
|
||||
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
|
||||
editor_test_context::EditorTestContext, select_ranges,
|
||||
use crate::{
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
test::{
|
||||
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
|
||||
editor_test_context::EditorTestContext, select_ranges,
|
||||
},
|
||||
JoinLines,
|
||||
};
|
||||
use drag_and_drop::DragAndDrop;
|
||||
use futures::StreamExt;
|
||||
|
@ -1356,6 +1360,43 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
|
||||
cx.simulate_window_resize(cx.window_id, vec2f(1000., 4. * line_height + 0.5));
|
||||
|
||||
cx.set_state(
|
||||
&r#"ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
"#,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.));
|
||||
editor.scroll_screen(&ScrollAmount::Page(1.), cx);
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
|
||||
editor.scroll_screen(&ScrollAmount::Page(1.), cx);
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.));
|
||||
editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
|
||||
|
||||
editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.));
|
||||
editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -2325,6 +2366,137 @@ fn test_delete_line(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
|
||||
let mut editor = build_editor(buffer.clone(), cx);
|
||||
let buffer = buffer.read(cx).as_singleton().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
editor.selections.ranges::<Point>(cx),
|
||||
&[Point::new(0, 0)..Point::new(0, 0)]
|
||||
);
|
||||
|
||||
// When on single line, replace newline at end by space
|
||||
editor.join_lines(&JoinLines, cx);
|
||||
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
|
||||
assert_eq!(
|
||||
editor.selections.ranges::<Point>(cx),
|
||||
&[Point::new(0, 3)..Point::new(0, 3)]
|
||||
);
|
||||
|
||||
// When multiple lines are selected, remove newlines that are spanned by the selection
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
|
||||
});
|
||||
editor.join_lines(&JoinLines, cx);
|
||||
assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
|
||||
assert_eq!(
|
||||
editor.selections.ranges::<Point>(cx),
|
||||
&[Point::new(0, 11)..Point::new(0, 11)]
|
||||
);
|
||||
|
||||
// Undo should be transactional
|
||||
editor.undo(&Undo, cx);
|
||||
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
|
||||
assert_eq!(
|
||||
editor.selections.ranges::<Point>(cx),
|
||||
&[Point::new(0, 5)..Point::new(2, 2)]
|
||||
);
|
||||
|
||||
// When joining an empty line don't insert a space
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
|
||||
});
|
||||
editor.join_lines(&JoinLines, cx);
|
||||
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
|
||||
assert_eq!(
|
||||
editor.selections.ranges::<Point>(cx),
|
||||
[Point::new(2, 3)..Point::new(2, 3)]
|
||||
);
|
||||
|
||||
// We can remove trailing newlines
|
||||
editor.join_lines(&JoinLines, cx);
|
||||
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
|
||||
assert_eq!(
|
||||
editor.selections.ranges::<Point>(cx),
|
||||
[Point::new(2, 3)..Point::new(2, 3)]
|
||||
);
|
||||
|
||||
// We don't blow up on the last line
|
||||
editor.join_lines(&JoinLines, cx);
|
||||
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
|
||||
assert_eq!(
|
||||
editor.selections.ranges::<Point>(cx),
|
||||
[Point::new(2, 3)..Point::new(2, 3)]
|
||||
);
|
||||
|
||||
// reset to test indentation
|
||||
editor.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[
|
||||
(Point::new(1, 0)..Point::new(1, 2), " "),
|
||||
(Point::new(2, 0)..Point::new(2, 3), " \n\td"),
|
||||
],
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// We remove any leading spaces
|
||||
assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
|
||||
});
|
||||
editor.join_lines(&JoinLines, cx);
|
||||
assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
|
||||
|
||||
// We don't insert a space for a line containing only spaces
|
||||
editor.join_lines(&JoinLines, cx);
|
||||
assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
|
||||
|
||||
// We ignore any leading tabs
|
||||
editor.join_lines(&JoinLines, cx);
|
||||
assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
|
||||
|
||||
editor
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
|
||||
let mut editor = build_editor(buffer.clone(), cx);
|
||||
let buffer = buffer.read(cx).as_singleton().unwrap();
|
||||
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([
|
||||
Point::new(0, 2)..Point::new(1, 1),
|
||||
Point::new(1, 2)..Point::new(1, 2),
|
||||
Point::new(3, 1)..Point::new(3, 2),
|
||||
])
|
||||
});
|
||||
|
||||
editor.join_lines(&JoinLines, cx);
|
||||
assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
|
||||
|
||||
assert_eq!(
|
||||
editor.selections.ranges::<Point>(cx),
|
||||
[
|
||||
Point::new(0, 7)..Point::new(0, 7),
|
||||
Point::new(1, 3)..Point::new(1, 3)
|
||||
]
|
||||
);
|
||||
editor
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_duplicate_line(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
|
|
@ -1467,6 +1467,7 @@ impl EditorElement {
|
|||
editor: &mut Editor,
|
||||
cx: &mut LayoutContext<Editor>,
|
||||
) -> (f32, Vec<BlockLayout>) {
|
||||
let mut block_id = 0;
|
||||
let scroll_x = snapshot.scroll_anchor.offset.x();
|
||||
let (fixed_blocks, non_fixed_blocks) = snapshot
|
||||
.blocks_in_range(rows.clone())
|
||||
|
@ -1474,7 +1475,7 @@ impl EditorElement {
|
|||
TransformBlock::ExcerptHeader { .. } => false,
|
||||
TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
|
||||
});
|
||||
let mut render_block = |block: &TransformBlock, width: f32| {
|
||||
let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| {
|
||||
let mut element = match block {
|
||||
TransformBlock::Custom(block) => {
|
||||
let align_to = block
|
||||
|
@ -1499,6 +1500,7 @@ impl EditorElement {
|
|||
scroll_x,
|
||||
gutter_width,
|
||||
em_width,
|
||||
block_id,
|
||||
})
|
||||
}
|
||||
TransformBlock::ExcerptHeader {
|
||||
|
@ -1527,7 +1529,7 @@ impl EditorElement {
|
|||
|
||||
enum JumpIcon {}
|
||||
MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| {
|
||||
let style = style.jump_icon.style_for(state, false);
|
||||
let style = style.jump_icon.style_for(state);
|
||||
Svg::new("icons/arrow_up_right_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
|
@ -1634,7 +1636,8 @@ impl EditorElement {
|
|||
let mut fixed_block_max_width = 0f32;
|
||||
let mut blocks = Vec::new();
|
||||
for (row, block) in fixed_blocks {
|
||||
let element = render_block(block, f32::INFINITY);
|
||||
let element = render_block(block, f32::INFINITY, block_id);
|
||||
block_id += 1;
|
||||
fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width);
|
||||
blocks.push(BlockLayout {
|
||||
row,
|
||||
|
@ -1654,7 +1657,8 @@ impl EditorElement {
|
|||
.max(gutter_width + scroll_width),
|
||||
BlockStyle::Fixed => unreachable!(),
|
||||
};
|
||||
let element = render_block(block, width);
|
||||
let element = render_block(block, width, block_id);
|
||||
block_id += 1;
|
||||
blocks.push(BlockLayout {
|
||||
row,
|
||||
element,
|
||||
|
@ -2090,7 +2094,7 @@ impl Element<Editor> for EditorElement {
|
|||
.folds
|
||||
.ellipses
|
||||
.background
|
||||
.style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize), false)
|
||||
.style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
|
||||
.color;
|
||||
|
||||
(id, fold, color)
|
||||
|
|
|
@ -368,7 +368,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
let cur_position = self.scroll_position(cx);
|
||||
let new_pos = cur_position + vec2f(0., amount.lines(self) - 1.);
|
||||
let new_pos = cur_position + vec2f(0., amount.lines(self));
|
||||
self.set_scroll_position(new_pos, cx);
|
||||
}
|
||||
|
||||
|
|
|
@ -27,22 +27,22 @@ pub fn init(cx: &mut AppContext) {
|
|||
cx.add_action(Editor::scroll_cursor_center);
|
||||
cx.add_action(Editor::scroll_cursor_bottom);
|
||||
cx.add_action(|this: &mut Editor, _: &LineDown, cx| {
|
||||
this.scroll_screen(&ScrollAmount::LineDown, cx)
|
||||
this.scroll_screen(&ScrollAmount::Line(1.), cx)
|
||||
});
|
||||
cx.add_action(|this: &mut Editor, _: &LineUp, cx| {
|
||||
this.scroll_screen(&ScrollAmount::LineUp, cx)
|
||||
this.scroll_screen(&ScrollAmount::Line(-1.), cx)
|
||||
});
|
||||
cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| {
|
||||
this.scroll_screen(&ScrollAmount::HalfPageDown, cx)
|
||||
this.scroll_screen(&ScrollAmount::Page(0.5), cx)
|
||||
});
|
||||
cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| {
|
||||
this.scroll_screen(&ScrollAmount::HalfPageUp, cx)
|
||||
this.scroll_screen(&ScrollAmount::Page(-0.5), cx)
|
||||
});
|
||||
cx.add_action(|this: &mut Editor, _: &PageDown, cx| {
|
||||
this.scroll_screen(&ScrollAmount::PageDown, cx)
|
||||
this.scroll_screen(&ScrollAmount::Page(1.), cx)
|
||||
});
|
||||
cx.add_action(|this: &mut Editor, _: &PageUp, cx| {
|
||||
this.scroll_screen(&ScrollAmount::PageUp, cx)
|
||||
this.scroll_screen(&ScrollAmount::Page(-1.), cx)
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,12 +6,10 @@ use crate::Editor;
|
|||
|
||||
#[derive(Clone, PartialEq, Deserialize)]
|
||||
pub enum ScrollAmount {
|
||||
LineUp,
|
||||
LineDown,
|
||||
HalfPageUp,
|
||||
HalfPageDown,
|
||||
PageUp,
|
||||
PageDown,
|
||||
// Scroll N lines (positive is towards the end of the document)
|
||||
Line(f32),
|
||||
// Scroll N pages (positive is towards the end of the document)
|
||||
Page(f32),
|
||||
}
|
||||
|
||||
impl ScrollAmount {
|
||||
|
@ -24,10 +22,10 @@ impl ScrollAmount {
|
|||
let context_menu = editor.context_menu.as_mut()?;
|
||||
|
||||
match self {
|
||||
Self::LineDown | Self::HalfPageDown => context_menu.select_next(cx),
|
||||
Self::LineUp | Self::HalfPageUp => context_menu.select_prev(cx),
|
||||
Self::PageDown => context_menu.select_last(cx),
|
||||
Self::PageUp => context_menu.select_first(cx),
|
||||
Self::Line(c) if *c > 0. => context_menu.select_next(cx),
|
||||
Self::Line(_) => context_menu.select_prev(cx),
|
||||
Self::Page(c) if *c > 0. => context_menu.select_last(cx),
|
||||
Self::Page(_) => context_menu.select_first(cx),
|
||||
}
|
||||
.then_some(())
|
||||
})
|
||||
|
@ -36,13 +34,13 @@ impl ScrollAmount {
|
|||
|
||||
pub fn lines(&self, editor: &mut Editor) -> f32 {
|
||||
match self {
|
||||
Self::LineDown => 1.,
|
||||
Self::LineUp => -1.,
|
||||
Self::HalfPageDown => editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.),
|
||||
Self::HalfPageUp => -editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.),
|
||||
// Minus 1. here so that there is a pivot line that stays on the screen
|
||||
Self::PageDown => editor.visible_line_count().unwrap_or(1.) - 1.,
|
||||
Self::PageUp => -editor.visible_line_count().unwrap_or(1.) - 1.,
|
||||
Self::Line(count) => *count,
|
||||
Self::Page(count) => editor
|
||||
.visible_line_count()
|
||||
// subtract one to leave an anchor line
|
||||
// round towards zero (so page-up and page-down are symmetric)
|
||||
.map(|l| ((l - 1.) * count).trunc())
|
||||
.unwrap_or(0.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,8 @@ impl View for DeployFeedbackButton {
|
|||
.status_bar
|
||||
.panel_buttons
|
||||
.button
|
||||
.style_for(state, active);
|
||||
.in_state(active)
|
||||
.style_for(state);
|
||||
|
||||
Svg::new("icons/feedback_16.svg")
|
||||
.with_color(style.icon_color)
|
||||
|
|
|
@ -48,7 +48,7 @@ impl View for SubmitFeedbackButton {
|
|||
let theme = theme::current(cx).clone();
|
||||
enum SubmitFeedbackButton {}
|
||||
MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| {
|
||||
let style = theme.feedback.submit_button.style_for(state, false);
|
||||
let style = theme.feedback.submit_button.style_for(state);
|
||||
Label::new("Submit as Markdown", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
|
|
@ -546,7 +546,7 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
.get(ix)
|
||||
.expect("Invalid matches state: no element for index {ix}");
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||
self.labels_for_match(path_match, cx, ix);
|
||||
Flex::column()
|
||||
|
|
|
@ -32,5 +32,8 @@ serde_json.workspace = true
|
|||
log.workspace = true
|
||||
libc = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
|
|
@ -108,6 +108,7 @@ pub trait Fs: Send + Sync {
|
|||
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
|
||||
async fn is_file(&self, path: &Path) -> bool;
|
||||
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>>;
|
||||
async fn read_link(&self, path: &Path) -> Result<PathBuf>;
|
||||
async fn read_dir(
|
||||
&self,
|
||||
path: &Path,
|
||||
|
@ -323,6 +324,11 @@ impl Fs for RealFs {
|
|||
}))
|
||||
}
|
||||
|
||||
async fn read_link(&self, path: &Path) -> Result<PathBuf> {
|
||||
let path = smol::fs::read_link(path).await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
async fn read_dir(
|
||||
&self,
|
||||
path: &Path,
|
||||
|
@ -382,6 +388,7 @@ struct FakeFsState {
|
|||
event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
|
||||
events_paused: bool,
|
||||
buffered_events: Vec<fsevent::Event>,
|
||||
read_dir_call_count: usize,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -407,46 +414,51 @@ enum FakeFsEntry {
|
|||
impl FakeFsState {
|
||||
fn read_path<'a>(&'a self, target: &Path) -> Result<Arc<Mutex<FakeFsEntry>>> {
|
||||
Ok(self
|
||||
.try_read_path(target)
|
||||
.try_read_path(target, true)
|
||||
.ok_or_else(|| anyhow!("path does not exist: {}", target.display()))?
|
||||
.0)
|
||||
}
|
||||
|
||||
fn try_read_path<'a>(&'a self, target: &Path) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
|
||||
fn try_read_path<'a>(
|
||||
&'a self,
|
||||
target: &Path,
|
||||
follow_symlink: bool,
|
||||
) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
|
||||
let mut path = target.to_path_buf();
|
||||
let mut real_path = PathBuf::new();
|
||||
let mut canonical_path = PathBuf::new();
|
||||
let mut entry_stack = Vec::new();
|
||||
'outer: loop {
|
||||
let mut path_components = path.components().collect::<collections::VecDeque<_>>();
|
||||
while let Some(component) = path_components.pop_front() {
|
||||
let mut path_components = path.components().peekable();
|
||||
while let Some(component) = path_components.next() {
|
||||
match component {
|
||||
Component::Prefix(_) => panic!("prefix paths aren't supported"),
|
||||
Component::RootDir => {
|
||||
entry_stack.clear();
|
||||
entry_stack.push(self.root.clone());
|
||||
real_path.clear();
|
||||
real_path.push("/");
|
||||
canonical_path.clear();
|
||||
canonical_path.push("/");
|
||||
}
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
entry_stack.pop()?;
|
||||
real_path.pop();
|
||||
canonical_path.pop();
|
||||
}
|
||||
Component::Normal(name) => {
|
||||
let current_entry = entry_stack.last().cloned()?;
|
||||
let current_entry = current_entry.lock();
|
||||
if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
|
||||
let entry = entries.get(name.to_str().unwrap()).cloned()?;
|
||||
let _entry = entry.lock();
|
||||
if let FakeFsEntry::Symlink { target, .. } = &*_entry {
|
||||
let mut target = target.clone();
|
||||
target.extend(path_components);
|
||||
path = target;
|
||||
continue 'outer;
|
||||
} else {
|
||||
entry_stack.push(entry.clone());
|
||||
real_path.push(name);
|
||||
if path_components.peek().is_some() || follow_symlink {
|
||||
let entry = entry.lock();
|
||||
if let FakeFsEntry::Symlink { target, .. } = &*entry {
|
||||
let mut target = target.clone();
|
||||
target.extend(path_components);
|
||||
path = target;
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
entry_stack.push(entry.clone());
|
||||
canonical_path.push(name);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
@ -455,7 +467,7 @@ impl FakeFsState {
|
|||
}
|
||||
break;
|
||||
}
|
||||
entry_stack.pop().map(|entry| (entry, real_path))
|
||||
Some((entry_stack.pop()?, canonical_path))
|
||||
}
|
||||
|
||||
fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
|
||||
|
@ -525,6 +537,7 @@ impl FakeFs {
|
|||
event_txs: Default::default(),
|
||||
buffered_events: Vec::new(),
|
||||
events_paused: false,
|
||||
read_dir_call_count: 0,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
@ -761,6 +774,10 @@ impl FakeFs {
|
|||
result
|
||||
}
|
||||
|
||||
pub fn read_dir_call_count(&self) -> usize {
|
||||
self.state.lock().read_dir_call_count
|
||||
}
|
||||
|
||||
async fn simulate_random_delay(&self) {
|
||||
self.executor
|
||||
.upgrade()
|
||||
|
@ -776,6 +793,10 @@ impl FakeFsEntry {
|
|||
matches!(self, Self::File { .. })
|
||||
}
|
||||
|
||||
fn is_symlink(&self) -> bool {
|
||||
matches!(self, Self::Symlink { .. })
|
||||
}
|
||||
|
||||
fn file_content(&self, path: &Path) -> Result<&String> {
|
||||
if let Self::File { content, .. } = self {
|
||||
Ok(content)
|
||||
|
@ -1056,8 +1077,8 @@ impl Fs for FakeFs {
|
|||
let path = normalize_path(path);
|
||||
self.simulate_random_delay().await;
|
||||
let state = self.state.lock();
|
||||
if let Some((_, real_path)) = state.try_read_path(&path) {
|
||||
Ok(real_path)
|
||||
if let Some((_, canonical_path)) = state.try_read_path(&path, true) {
|
||||
Ok(canonical_path)
|
||||
} else {
|
||||
Err(anyhow!("path does not exist: {}", path.display()))
|
||||
}
|
||||
|
@ -1067,7 +1088,7 @@ impl Fs for FakeFs {
|
|||
let path = normalize_path(path);
|
||||
self.simulate_random_delay().await;
|
||||
let state = self.state.lock();
|
||||
if let Some((entry, _)) = state.try_read_path(&path) {
|
||||
if let Some((entry, _)) = state.try_read_path(&path, true) {
|
||||
entry.lock().is_file()
|
||||
} else {
|
||||
false
|
||||
|
@ -1078,10 +1099,17 @@ impl Fs for FakeFs {
|
|||
self.simulate_random_delay().await;
|
||||
let path = normalize_path(path);
|
||||
let state = self.state.lock();
|
||||
if let Some((entry, real_path)) = state.try_read_path(&path) {
|
||||
let entry = entry.lock();
|
||||
let is_symlink = real_path != path;
|
||||
if let Some((mut entry, _)) = state.try_read_path(&path, false) {
|
||||
let is_symlink = entry.lock().is_symlink();
|
||||
if is_symlink {
|
||||
if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) {
|
||||
entry = e;
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
let entry = entry.lock();
|
||||
Ok(Some(match &*entry {
|
||||
FakeFsEntry::File { inode, mtime, .. } => Metadata {
|
||||
inode: *inode,
|
||||
|
@ -1102,13 +1130,30 @@ impl Fs for FakeFs {
|
|||
}
|
||||
}
|
||||
|
||||
async fn read_link(&self, path: &Path) -> Result<PathBuf> {
|
||||
self.simulate_random_delay().await;
|
||||
let path = normalize_path(path);
|
||||
let state = self.state.lock();
|
||||
if let Some((entry, _)) = state.try_read_path(&path, false) {
|
||||
let entry = entry.lock();
|
||||
if let FakeFsEntry::Symlink { target } = &*entry {
|
||||
Ok(target.clone())
|
||||
} else {
|
||||
Err(anyhow!("not a symlink: {}", path.display()))
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!("path does not exist: {}", path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_dir(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
|
||||
self.simulate_random_delay().await;
|
||||
let path = normalize_path(path);
|
||||
let state = self.state.lock();
|
||||
let mut state = self.state.lock();
|
||||
state.read_dir_call_count += 1;
|
||||
let entry = state.read_path(&path)?;
|
||||
let mut entry = entry.lock();
|
||||
let children = entry.dir_entries(&path)?;
|
||||
|
|
|
@ -152,6 +152,29 @@ impl App {
|
|||
asset_source,
|
||||
))));
|
||||
|
||||
foreground_platform.on_event(Box::new({
|
||||
let cx = app.0.clone();
|
||||
move |event| {
|
||||
if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = &event {
|
||||
// Allow system menu "cmd-?" shortcut to be overridden
|
||||
if keystroke.cmd
|
||||
&& !keystroke.shift
|
||||
&& !keystroke.alt
|
||||
&& !keystroke.function
|
||||
&& keystroke.key == "?"
|
||||
{
|
||||
if cx
|
||||
.borrow_mut()
|
||||
.update_active_window(|cx| cx.dispatch_keystroke(keystroke))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}));
|
||||
foreground_platform.on_quit(Box::new({
|
||||
let cx = app.0.clone();
|
||||
move || {
|
||||
|
|
|
@ -6,15 +6,16 @@ use std::{
|
|||
|
||||
use crate::json::ToJson;
|
||||
use pathfinder_color::{ColorF, ColorU};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{
|
||||
de::{self, Unexpected},
|
||||
Deserialize, Deserializer,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)]
|
||||
#[repr(transparent)]
|
||||
pub struct Color(ColorU);
|
||||
pub struct Color(#[schemars(with = "String")] ColorU);
|
||||
|
||||
impl Color {
|
||||
pub fn transparent_black() -> Self {
|
||||
|
|
|
@ -41,13 +41,7 @@ use collections::HashMap;
|
|||
use core::panic;
|
||||
use json::ToJson;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
};
|
||||
use std::{any::Any, borrow::Cow, mem, ops::Range};
|
||||
|
||||
pub trait Element<V: View>: 'static {
|
||||
type LayoutState;
|
||||
|
@ -567,90 +561,6 @@ impl<V: View> RootElement<V> {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait Component<V: View>: 'static {
|
||||
fn render(&self, view: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
|
||||
}
|
||||
|
||||
pub struct ComponentHost<V: View, C: Component<V>> {
|
||||
component: C,
|
||||
view_type: PhantomData<V>,
|
||||
}
|
||||
|
||||
impl<V: View, C: Component<V>> ComponentHost<V, C> {
|
||||
pub fn new(c: C) -> Self {
|
||||
Self {
|
||||
component: c,
|
||||
view_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View, C: Component<V>> Deref for ComponentHost<V, C> {
|
||||
type Target = C;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.component
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View, C: Component<V>> DerefMut for ComponentHost<V, C> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.component
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View, C: Component<V>> Element<V> for ComponentHost<V, C> {
|
||||
type LayoutState = AnyElement<V>;
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
) -> (Vector2F, AnyElement<V>) {
|
||||
let mut element = self.component.render(view, cx);
|
||||
let size = element.layout(constraint, view, cx);
|
||||
(size, element)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
scene: &mut SceneBuilder,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
element: &mut AnyElement<V>,
|
||||
view: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
element.paint(scene, bounds.origin(), visible_bounds, view, cx);
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
element: &AnyElement<V>,
|
||||
_: &(),
|
||||
view: &V,
|
||||
cx: &ViewContext<V>,
|
||||
) -> Option<RectF> {
|
||||
element.rect_for_text_range(range_utf16, view, cx)
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
_: RectF,
|
||||
element: &AnyElement<V>,
|
||||
_: &(),
|
||||
view: &V,
|
||||
cx: &ViewContext<V>,
|
||||
) -> serde_json::Value {
|
||||
element.debug(view, cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnyRootElement {
|
||||
fn layout(
|
||||
&mut self,
|
||||
|
|
|
@ -12,10 +12,11 @@ use crate::{
|
|||
scene::{self, Border, CursorRegion, Quad},
|
||||
AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
|
||||
pub struct ContainerStyle {
|
||||
#[serde(default)]
|
||||
pub margin: Margin,
|
||||
|
@ -332,7 +333,7 @@ impl ToJson for ContainerStyle {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[derive(Clone, Copy, Debug, Default, JsonSchema)]
|
||||
pub struct Margin {
|
||||
pub top: f32,
|
||||
pub left: f32,
|
||||
|
@ -359,7 +360,7 @@ impl ToJson for Margin {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[derive(Clone, Copy, Debug, Default, JsonSchema)]
|
||||
pub struct Padding {
|
||||
pub top: f32,
|
||||
pub left: f32,
|
||||
|
@ -486,9 +487,10 @@ impl ToJson for Padding {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
|
||||
pub struct Shadow {
|
||||
#[serde(default, deserialize_with = "deserialize_vec2f")]
|
||||
#[schemars(with = "Vec::<f32>")]
|
||||
offset: Vector2F,
|
||||
#[serde(default)]
|
||||
blur: f32,
|
||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
|||
scene, Border, Element, ImageData, LayoutContext, SceneBuilder, SizeConstraint, View,
|
||||
ViewContext,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
|
@ -21,7 +22,7 @@ pub struct Image {
|
|||
style: ImageStyle,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Deserialize)]
|
||||
#[derive(Copy, Clone, Default, Deserialize, JsonSchema)]
|
||||
pub struct ImageStyle {
|
||||
#[serde(default)]
|
||||
pub border: Border,
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::{
|
|||
text_layout::{Line, RunStyle},
|
||||
Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
@ -20,7 +21,7 @@ pub struct Label {
|
|||
highlight_indices: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
||||
pub struct LabelStyle {
|
||||
pub text: TextStyle,
|
||||
pub highlight_text: Option<TextStyle>,
|
||||
|
@ -164,6 +165,7 @@ impl<V: View> Element<V> for Label {
|
|||
_: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::PaintState {
|
||||
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
line.paint(
|
||||
scene,
|
||||
bounds.origin(),
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use super::constrain_size_preserving_aspect_ratio;
|
||||
use crate::json::ToJson;
|
||||
use crate::{
|
||||
color::Color,
|
||||
geometry::{
|
||||
|
@ -10,6 +8,10 @@ use crate::{
|
|||
},
|
||||
scene, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
pub struct Svg {
|
||||
path: Cow<'static, str>,
|
||||
|
@ -24,6 +26,14 @@ impl Svg {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn for_style<V: View>(style: SvgStyle) -> impl Element<V> {
|
||||
Self::new(style.asset)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.dimensions.width)
|
||||
.with_height(style.dimensions.height)
|
||||
}
|
||||
|
||||
pub fn with_color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
|
@ -105,9 +115,24 @@ impl<V: View> Element<V> for Svg {
|
|||
}
|
||||
}
|
||||
|
||||
use crate::json::ToJson;
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct SvgStyle {
|
||||
pub color: Color,
|
||||
pub asset: String,
|
||||
pub dimensions: Dimensions,
|
||||
}
|
||||
|
||||
use super::constrain_size_preserving_aspect_ratio;
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Dimensions {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
impl Dimensions {
|
||||
pub fn to_vec(&self) -> Vector2F {
|
||||
vec2f(self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
fn from_usvg_rect(rect: usvg::Rect) -> RectF {
|
||||
RectF::new(
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::{
|
|||
Action, Axis, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, Task, View,
|
||||
ViewContext,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
|
@ -33,7 +34,7 @@ struct TooltipState {
|
|||
debounce: RefCell<Option<Task<()>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct TooltipStyle {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -42,7 +43,7 @@ pub struct TooltipStyle {
|
|||
pub max_text_width: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct KeystrokeStyle {
|
||||
#[serde(flatten)]
|
||||
container: ContainerStyle,
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::{
|
|||
fmt::{self, Display},
|
||||
marker::PhantomData,
|
||||
mem,
|
||||
panic::Location,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
|
@ -965,10 +966,12 @@ impl<T> Task<T> {
|
|||
}
|
||||
|
||||
impl<T: 'static, E: 'static + Display> Task<Result<T, E>> {
|
||||
#[track_caller]
|
||||
pub fn detach_and_log_err(self, cx: &mut AppContext) {
|
||||
let caller = Location::caller();
|
||||
cx.spawn(|_| async move {
|
||||
if let Err(err) = self.await {
|
||||
log::error!("{:#}", err);
|
||||
log::error!("{}:{}: {:#}", caller.file(), caller.line(), err);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
|
|
@ -7,13 +7,14 @@ use crate::{
|
|||
use anyhow::{anyhow, Result};
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
|
||||
use schemars::JsonSchema;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
|
||||
pub struct FamilyId(usize);
|
||||
|
||||
struct Family {
|
||||
|
|
|
@ -16,7 +16,7 @@ use serde::{de, Deserialize, Serialize};
|
|||
use serde_json::Value;
|
||||
use std::{cell::RefCell, sync::Arc};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
|
||||
pub struct FontId(pub usize);
|
||||
|
||||
pub type GlyphId = u32;
|
||||
|
@ -59,20 +59,44 @@ pub struct Features {
|
|||
pub zero: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, JsonSchema)]
|
||||
pub struct TextStyle {
|
||||
pub color: Color,
|
||||
pub font_family_name: Arc<str>,
|
||||
pub font_family_id: FamilyId,
|
||||
pub font_id: FontId,
|
||||
pub font_size: f32,
|
||||
#[schemars(with = "PropertiesDef")]
|
||||
pub font_properties: Properties,
|
||||
pub underline: Underline,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
#[derive(JsonSchema)]
|
||||
#[serde(remote = "Properties")]
|
||||
pub struct PropertiesDef {
|
||||
/// The font style, as defined in CSS.
|
||||
pub style: StyleDef,
|
||||
/// The font weight, as defined in CSS.
|
||||
pub weight: f32,
|
||||
/// The font stretchiness, as defined in CSS.
|
||||
pub stretch: f32,
|
||||
}
|
||||
|
||||
#[derive(JsonSchema)]
|
||||
#[schemars(remote = "Style")]
|
||||
pub enum StyleDef {
|
||||
/// A face that is neither italic not obliqued.
|
||||
Normal,
|
||||
/// A form that is generally cursive in nature.
|
||||
Italic,
|
||||
/// A typically-sloped version of the regular face.
|
||||
Oblique,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, JsonSchema)]
|
||||
pub struct HighlightStyle {
|
||||
pub color: Option<Color>,
|
||||
#[schemars(with = "Option::<f32>")]
|
||||
pub weight: Option<Weight>,
|
||||
pub italic: Option<bool>,
|
||||
pub underline: Option<Underline>,
|
||||
|
@ -81,9 +105,10 @@ pub struct HighlightStyle {
|
|||
|
||||
impl Eq for HighlightStyle {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, JsonSchema)]
|
||||
pub struct Underline {
|
||||
pub color: Option<Color>,
|
||||
#[schemars(with = "f32")]
|
||||
pub thickness: OrderedFloat<f32>,
|
||||
pub squiggly: bool,
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ pub mod color;
|
|||
pub mod json;
|
||||
pub mod keymap_matcher;
|
||||
pub mod platform;
|
||||
pub use gpui_macros::test;
|
||||
pub use gpui_macros::{test, Element};
|
||||
pub use window::{Axis, SizeConstraint, Vector2FExt, WindowContext};
|
||||
|
||||
pub use anyhow;
|
||||
|
|
|
@ -25,6 +25,7 @@ use anyhow::{anyhow, bail, Result};
|
|||
use async_task::Runnable;
|
||||
pub use event::*;
|
||||
use postage::oneshot;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
|
@ -282,7 +283,7 @@ pub enum PromptLevel {
|
|||
Critical,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||
#[derive(Copy, Clone, Debug, Deserialize, JsonSchema)]
|
||||
pub enum CursorStyle {
|
||||
Arrow,
|
||||
ResizeLeftRight,
|
||||
|
|
|
@ -939,7 +939,6 @@ extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
msg_send![super(this, class!(NSApplication)), sendEvent: native_event]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ mod mouse_region;
|
|||
|
||||
#[cfg(debug_assertions)]
|
||||
use collections::HashSet;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
@ -99,7 +100,7 @@ pub struct Icon {
|
|||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug)]
|
||||
#[derive(Clone, Copy, Default, Debug, JsonSchema)]
|
||||
pub struct Border {
|
||||
pub width: f32,
|
||||
pub color: Color,
|
||||
|
|
|
@ -3,8 +3,8 @@ use proc_macro2::Ident;
|
|||
use quote::{format_ident, quote};
|
||||
use std::mem;
|
||||
use syn::{
|
||||
parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, FnArg, ItemFn, Lit, Meta,
|
||||
NestedMeta, Type,
|
||||
parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, DeriveInput, FnArg,
|
||||
ItemFn, Lit, Meta, NestedMeta, Type,
|
||||
};
|
||||
|
||||
#[proc_macro_attribute]
|
||||
|
@ -275,3 +275,68 @@ fn parse_bool(literal: &Lit) -> Result<bool, TokenStream> {
|
|||
|
||||
result.map_err(|err| TokenStream::from(err.into_compile_error()))
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Element)]
|
||||
pub fn element_derive(input: TokenStream) -> TokenStream {
|
||||
// Parse the input tokens into a syntax tree
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
// The name of the struct/enum
|
||||
let name = input.ident;
|
||||
|
||||
let expanded = quote! {
|
||||
impl<V: gpui::View> gpui::elements::Element<V> for #name {
|
||||
type LayoutState = gpui::elements::AnyElement<V>;
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
view: &mut V,
|
||||
cx: &mut gpui::LayoutContext<V>,
|
||||
) -> (gpui::geometry::vector::Vector2F, gpui::elements::AnyElement<V>) {
|
||||
let mut element = self.render(view, cx);
|
||||
let size = element.layout(constraint, view, cx);
|
||||
(size, element)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
scene: &mut gpui::SceneBuilder,
|
||||
bounds: gpui::geometry::rect::RectF,
|
||||
visible_bounds: gpui::geometry::rect::RectF,
|
||||
element: &mut gpui::elements::AnyElement<V>,
|
||||
view: &mut V,
|
||||
cx: &mut gpui::ViewContext<V>,
|
||||
) {
|
||||
element.paint(scene, bounds.origin(), visible_bounds, view, cx);
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: std::ops::Range<usize>,
|
||||
_: gpui::geometry::rect::RectF,
|
||||
_: gpui::geometry::rect::RectF,
|
||||
element: &gpui::elements::AnyElement<V>,
|
||||
_: &(),
|
||||
view: &V,
|
||||
cx: &gpui::ViewContext<V>,
|
||||
) -> Option<gpui::geometry::rect::RectF> {
|
||||
element.rect_for_text_range(range_utf16, view, cx)
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
_: gpui::geometry::rect::RectF,
|
||||
element: &gpui::elements::AnyElement<V>,
|
||||
_: &(),
|
||||
view: &V,
|
||||
cx: &gpui::ViewContext<V>,
|
||||
) -> serde_json::Value {
|
||||
element.debug(view, cx)
|
||||
}
|
||||
}
|
||||
};
|
||||
// Return generated code
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use std::{
|
|||
cell::RefCell,
|
||||
cmp::{self, Ordering, Reverse},
|
||||
collections::BinaryHeap,
|
||||
iter,
|
||||
fmt, iter,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
sync::Arc,
|
||||
};
|
||||
|
@ -428,6 +428,8 @@ impl SyntaxSnapshot {
|
|||
invalidated_ranges: Vec<Range<usize>>,
|
||||
registry: Option<&Arc<LanguageRegistry>>,
|
||||
) {
|
||||
log::trace!("reparse. invalidated ranges:{:?}", invalidated_ranges);
|
||||
|
||||
let max_depth = self.layers.summary().max_depth;
|
||||
let mut cursor = self.layers.cursor::<SyntaxLayerSummary>();
|
||||
cursor.next(&text);
|
||||
|
@ -489,6 +491,15 @@ impl SyntaxSnapshot {
|
|||
let Some(layer) = cursor.item() else { break };
|
||||
|
||||
if changed_regions.intersects(&layer, text) {
|
||||
if let SyntaxLayerContent::Parsed { language, .. } = &layer.content {
|
||||
log::trace!(
|
||||
"discard layer. language:{}, range:{:?}. changed_regions:{:?}",
|
||||
language.name(),
|
||||
LogAnchorRange(&layer.range, text),
|
||||
LogChangedRegions(&changed_regions, text),
|
||||
);
|
||||
}
|
||||
|
||||
changed_regions.insert(
|
||||
ChangedRegion {
|
||||
depth: layer.depth + 1,
|
||||
|
@ -541,26 +552,24 @@ impl SyntaxSnapshot {
|
|||
.to_ts_point();
|
||||
}
|
||||
|
||||
if included_ranges.is_empty() {
|
||||
included_ranges.push(tree_sitter::Range {
|
||||
start_byte: 0,
|
||||
end_byte: 0,
|
||||
start_point: Default::default(),
|
||||
end_point: Default::default(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(SyntaxLayerContent::Parsed { tree: old_tree, .. }) =
|
||||
old_layer.map(|layer| &layer.content)
|
||||
if let Some((SyntaxLayerContent::Parsed { tree: old_tree, .. }, layer_start)) =
|
||||
old_layer.map(|layer| (&layer.content, layer.range.start))
|
||||
{
|
||||
log::trace!(
|
||||
"existing layer. language:{}, start:{:?}, ranges:{:?}",
|
||||
language.name(),
|
||||
LogPoint(layer_start.to_point(&text)),
|
||||
LogIncludedRanges(&old_tree.included_ranges())
|
||||
);
|
||||
|
||||
if let ParseMode::Combined {
|
||||
mut parent_layer_changed_ranges,
|
||||
..
|
||||
} = step.mode
|
||||
{
|
||||
for range in &mut parent_layer_changed_ranges {
|
||||
range.start -= step_start_byte;
|
||||
range.end -= step_start_byte;
|
||||
range.start = range.start.saturating_sub(step_start_byte);
|
||||
range.end = range.end.saturating_sub(step_start_byte);
|
||||
}
|
||||
|
||||
included_ranges = splice_included_ranges(
|
||||
|
@ -570,6 +579,22 @@ impl SyntaxSnapshot {
|
|||
);
|
||||
}
|
||||
|
||||
if included_ranges.is_empty() {
|
||||
included_ranges.push(tree_sitter::Range {
|
||||
start_byte: 0,
|
||||
end_byte: 0,
|
||||
start_point: Default::default(),
|
||||
end_point: Default::default(),
|
||||
});
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
"update layer. language:{}, start:{:?}, ranges:{:?}",
|
||||
language.name(),
|
||||
LogAnchorRange(&step.range, text),
|
||||
LogIncludedRanges(&included_ranges),
|
||||
);
|
||||
|
||||
tree = parse_text(
|
||||
grammar,
|
||||
text.as_rope(),
|
||||
|
@ -586,6 +611,22 @@ impl SyntaxSnapshot {
|
|||
}),
|
||||
);
|
||||
} else {
|
||||
if included_ranges.is_empty() {
|
||||
included_ranges.push(tree_sitter::Range {
|
||||
start_byte: 0,
|
||||
end_byte: 0,
|
||||
start_point: Default::default(),
|
||||
end_point: Default::default(),
|
||||
});
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
"create layer. language:{}, range:{:?}, included_ranges:{:?}",
|
||||
language.name(),
|
||||
LogAnchorRange(&step.range, text),
|
||||
LogIncludedRanges(&included_ranges),
|
||||
);
|
||||
|
||||
tree = parse_text(
|
||||
grammar,
|
||||
text.as_rope(),
|
||||
|
@ -613,6 +654,7 @@ impl SyntaxSnapshot {
|
|||
get_injections(
|
||||
config,
|
||||
text,
|
||||
step.range.clone(),
|
||||
tree.root_node_with_offset(
|
||||
step_start_byte,
|
||||
step_start_point.to_ts_point(),
|
||||
|
@ -1117,6 +1159,7 @@ fn parse_text(
|
|||
fn get_injections(
|
||||
config: &InjectionConfig,
|
||||
text: &BufferSnapshot,
|
||||
outer_range: Range<Anchor>,
|
||||
node: Node,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
depth: usize,
|
||||
|
@ -1153,16 +1196,17 @@ fn get_injections(
|
|||
continue;
|
||||
}
|
||||
|
||||
// Avoid duplicate matches if two changed ranges intersect the same injection.
|
||||
let content_range =
|
||||
content_ranges.first().unwrap().start_byte..content_ranges.last().unwrap().end_byte;
|
||||
if let Some((last_pattern_ix, last_range)) = &prev_match {
|
||||
if mat.pattern_index == *last_pattern_ix && content_range == *last_range {
|
||||
|
||||
// Avoid duplicate matches if two changed ranges intersect the same injection.
|
||||
if let Some((prev_pattern_ix, prev_range)) = &prev_match {
|
||||
if mat.pattern_index == *prev_pattern_ix && content_range == *prev_range {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
prev_match = Some((mat.pattern_index, content_range.clone()));
|
||||
|
||||
prev_match = Some((mat.pattern_index, content_range.clone()));
|
||||
let combined = config.patterns[mat.pattern_index].combined;
|
||||
|
||||
let mut language_name = None;
|
||||
|
@ -1218,11 +1262,10 @@ fn get_injections(
|
|||
|
||||
for (language, mut included_ranges) in combined_injection_ranges.drain() {
|
||||
included_ranges.sort_unstable();
|
||||
let range = text.anchor_before(node.start_byte())..text.anchor_after(node.end_byte());
|
||||
queue.push(ParseStep {
|
||||
depth,
|
||||
language: ParseStepLanguage::Loaded { language },
|
||||
range,
|
||||
range: outer_range.clone(),
|
||||
included_ranges,
|
||||
mode: ParseMode::Combined {
|
||||
parent_layer_range: node.start_byte()..node.end_byte(),
|
||||
|
@ -1234,72 +1277,77 @@ fn get_injections(
|
|||
|
||||
pub(crate) fn splice_included_ranges(
|
||||
mut ranges: Vec<tree_sitter::Range>,
|
||||
changed_ranges: &[Range<usize>],
|
||||
removed_ranges: &[Range<usize>],
|
||||
new_ranges: &[tree_sitter::Range],
|
||||
) -> Vec<tree_sitter::Range> {
|
||||
let mut changed_ranges = changed_ranges.into_iter().peekable();
|
||||
let mut new_ranges = new_ranges.into_iter().peekable();
|
||||
let mut removed_ranges = removed_ranges.iter().cloned().peekable();
|
||||
let mut new_ranges = new_ranges.into_iter().cloned().peekable();
|
||||
let mut ranges_ix = 0;
|
||||
loop {
|
||||
let new_range = new_ranges.peek();
|
||||
let mut changed_range = changed_ranges.peek();
|
||||
let next_new_range = new_ranges.peek();
|
||||
let next_removed_range = removed_ranges.peek();
|
||||
|
||||
// Remove ranges that have changed before inserting any new ranges
|
||||
// into those ranges.
|
||||
if let Some((changed, new)) = changed_range.zip(new_range) {
|
||||
if new.end_byte < changed.start {
|
||||
changed_range = None;
|
||||
let (remove, insert) = match (next_removed_range, next_new_range) {
|
||||
(None, None) => break,
|
||||
(Some(_), None) => (removed_ranges.next().unwrap(), None),
|
||||
(Some(next_removed_range), Some(next_new_range)) => {
|
||||
if next_removed_range.end < next_new_range.start_byte {
|
||||
(removed_ranges.next().unwrap(), None)
|
||||
} else {
|
||||
let mut start = next_new_range.start_byte;
|
||||
let mut end = next_new_range.end_byte;
|
||||
|
||||
while let Some(next_removed_range) = removed_ranges.peek() {
|
||||
if next_removed_range.start > next_new_range.end_byte {
|
||||
break;
|
||||
}
|
||||
let next_removed_range = removed_ranges.next().unwrap();
|
||||
start = cmp::min(start, next_removed_range.start);
|
||||
end = cmp::max(end, next_removed_range.end);
|
||||
}
|
||||
|
||||
(start..end, Some(new_ranges.next().unwrap()))
|
||||
}
|
||||
}
|
||||
(None, Some(next_new_range)) => (
|
||||
next_new_range.start_byte..next_new_range.end_byte,
|
||||
Some(new_ranges.next().unwrap()),
|
||||
),
|
||||
};
|
||||
|
||||
let mut start_ix = ranges_ix
|
||||
+ match ranges[ranges_ix..].binary_search_by_key(&remove.start, |r| r.end_byte) {
|
||||
Ok(ix) => ix,
|
||||
Err(ix) => ix,
|
||||
};
|
||||
let mut end_ix = ranges_ix
|
||||
+ match ranges[ranges_ix..].binary_search_by_key(&remove.end, |r| r.start_byte) {
|
||||
Ok(ix) => ix + 1,
|
||||
Err(ix) => ix,
|
||||
};
|
||||
|
||||
// If there are empty ranges, then there may be multiple ranges with the same
|
||||
// start or end. Expand the splice to include any adjacent ranges that touch
|
||||
// the changed range.
|
||||
while start_ix > 0 {
|
||||
if ranges[start_ix - 1].end_byte == remove.start {
|
||||
start_ix -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
while let Some(range) = ranges.get(end_ix) {
|
||||
if range.start_byte == remove.end {
|
||||
end_ix += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(changed) = changed_range {
|
||||
let mut start_ix = ranges_ix
|
||||
+ match ranges[ranges_ix..].binary_search_by_key(&changed.start, |r| r.end_byte) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
let mut end_ix = ranges_ix
|
||||
+ match ranges[ranges_ix..].binary_search_by_key(&changed.end, |r| r.start_byte) {
|
||||
Ok(ix) => ix + 1,
|
||||
Err(ix) => ix,
|
||||
};
|
||||
|
||||
// If there are empty ranges, then there may be multiple ranges with the same
|
||||
// start or end. Expand the splice to include any adjacent ranges that touch
|
||||
// the changed range.
|
||||
while start_ix > 0 {
|
||||
if ranges[start_ix - 1].end_byte == changed.start {
|
||||
start_ix -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
while let Some(range) = ranges.get(end_ix) {
|
||||
if range.start_byte == changed.end {
|
||||
end_ix += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if end_ix > start_ix {
|
||||
ranges.splice(start_ix..end_ix, []);
|
||||
}
|
||||
changed_ranges.next();
|
||||
ranges_ix = start_ix;
|
||||
} else if let Some(new_range) = new_range {
|
||||
let ix = ranges_ix
|
||||
+ match ranges[ranges_ix..]
|
||||
.binary_search_by_key(&new_range.start_byte, |r| r.start_byte)
|
||||
{
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
ranges.insert(ix, **new_range);
|
||||
new_ranges.next();
|
||||
ranges_ix = ix + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
ranges.splice(start_ix..end_ix, insert);
|
||||
ranges_ix = start_ix;
|
||||
}
|
||||
|
||||
ranges
|
||||
}
|
||||
|
||||
|
@ -1628,3 +1676,46 @@ impl ToTreeSitterPoint for Point {
|
|||
Point::new(point.row as u32, point.column as u32)
|
||||
}
|
||||
}
|
||||
|
||||
struct LogIncludedRanges<'a>(&'a [tree_sitter::Range]);
|
||||
struct LogPoint(Point);
|
||||
struct LogAnchorRange<'a>(&'a Range<Anchor>, &'a text::BufferSnapshot);
|
||||
struct LogChangedRegions<'a>(&'a ChangeRegionSet, &'a text::BufferSnapshot);
|
||||
|
||||
impl<'a> fmt::Debug for LogIncludedRanges<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_list()
|
||||
.entries(self.0.iter().map(|range| {
|
||||
let start = range.start_point;
|
||||
let end = range.end_point;
|
||||
(start.row, start.column)..(end.row, end.column)
|
||||
}))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for LogAnchorRange<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let range = self.0.to_point(self.1);
|
||||
(LogPoint(range.start)..LogPoint(range.end)).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for LogChangedRegions<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_list()
|
||||
.entries(
|
||||
self.0
|
||||
.0
|
||||
.iter()
|
||||
.map(|region| LogAnchorRange(®ion.range, self.1)),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for LogPoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
(self.0.row, self.0.column).fmt(f)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,13 @@ fn test_splice_included_ranges() {
|
|||
let new_ranges = splice_included_ranges(ranges.clone(), &[30..50], &[ts_range(25..55)]);
|
||||
assert_eq!(new_ranges, &[ts_range(25..55), ts_range(80..90)]);
|
||||
|
||||
// does not create overlapping ranges
|
||||
let new_ranges = splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]);
|
||||
assert_eq!(
|
||||
new_ranges,
|
||||
&[ts_range(20..32), ts_range(50..60), ts_range(80..90)]
|
||||
);
|
||||
|
||||
fn ts_range(range: Range<usize>) -> tree_sitter::Range {
|
||||
tree_sitter::Range {
|
||||
start_byte: range.start,
|
||||
|
@ -624,6 +631,26 @@ fn test_combined_injections_splitting_some_injections() {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_combined_injections_editing_after_last_injection() {
|
||||
test_edit_sequence(
|
||||
"ERB",
|
||||
&[
|
||||
r#"
|
||||
<% foo %>
|
||||
<div></div>
|
||||
<% bar %>
|
||||
"#,
|
||||
r#"
|
||||
<% foo %>
|
||||
<div></div>
|
||||
<% bar %>«
|
||||
more text»
|
||||
"#,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_combined_injections_inside_injections() {
|
||||
let (_buffer, _syntax_map) = test_edit_sequence(
|
||||
|
@ -974,13 +1001,16 @@ fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap
|
|||
mutated_syntax_map.reparse(language.clone(), &buffer);
|
||||
|
||||
for (i, marked_string) in steps.into_iter().enumerate() {
|
||||
buffer.edit_via_marked_text(&marked_string.unindent());
|
||||
let marked_string = marked_string.unindent();
|
||||
log::info!("incremental parse {i}: {marked_string:?}");
|
||||
buffer.edit_via_marked_text(&marked_string);
|
||||
|
||||
// Reparse the syntax map
|
||||
mutated_syntax_map.interpolate(&buffer);
|
||||
mutated_syntax_map.reparse(language.clone(), &buffer);
|
||||
|
||||
// Create a second syntax map from scratch
|
||||
log::info!("fresh parse {i}: {marked_string:?}");
|
||||
let mut reference_syntax_map = SyntaxMap::new();
|
||||
reference_syntax_map.set_language_registry(registry.clone());
|
||||
reference_syntax_map.reparse(language.clone(), &buffer);
|
||||
|
@ -1133,6 +1163,7 @@ fn range_for_text(buffer: &Buffer, text: &str) -> Range<usize> {
|
|||
start..start + text.len()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_layers_for_range(
|
||||
syntax_map: &SyntaxMap,
|
||||
buffer: &BufferSnapshot,
|
||||
|
|
|
@ -55,7 +55,7 @@ impl View for ActiveBufferLanguage {
|
|||
|
||||
MouseEventHandler::<Self, Self>::new(0, cx, |state, cx| {
|
||||
let theme = &theme::current(cx).workspace.status_bar;
|
||||
let style = theme.active_language.style_for(state, false);
|
||||
let style = theme.active_language.style_for(state);
|
||||
Label::new(active_language_text, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
|
|
@ -180,7 +180,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
|
|||
) -> AnyElement<Picker<Self>> {
|
||||
let theme = theme::current(cx);
|
||||
let mat = &self.matches[ix];
|
||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
||||
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
|
||||
let mut label = mat.string.clone();
|
||||
if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
|
||||
|
|
|
@ -681,7 +681,7 @@ impl LspLogToolbarItemView {
|
|||
)
|
||||
})
|
||||
.unwrap_or_else(|| "No server selected".into());
|
||||
let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
|
||||
let style = theme.toolbar_dropdown_menu.header.style_for(state);
|
||||
Label::new(label, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
@ -722,7 +722,8 @@ impl LspLogToolbarItemView {
|
|||
let style = theme
|
||||
.toolbar_dropdown_menu
|
||||
.item
|
||||
.style_for(state, logs_selected);
|
||||
.in_state(logs_selected)
|
||||
.style_for(state);
|
||||
Label::new(SERVER_LOGS, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
@ -739,7 +740,8 @@ impl LspLogToolbarItemView {
|
|||
let style = theme
|
||||
.toolbar_dropdown_menu
|
||||
.item
|
||||
.style_for(state, rpc_trace_selected);
|
||||
.in_state(rpc_trace_selected)
|
||||
.style_for(state);
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(RPC_MESSAGES, style.text.clone())
|
||||
|
|
|
@ -565,7 +565,7 @@ impl SyntaxTreeToolbarItemView {
|
|||
) -> impl Element<Self> {
|
||||
enum ToggleMenu {}
|
||||
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| {
|
||||
let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
|
||||
let style = theme.toolbar_dropdown_menu.header.style_for(state);
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(active_layer.language.name().to_string(), style.text.clone())
|
||||
|
@ -601,7 +601,8 @@ impl SyntaxTreeToolbarItemView {
|
|||
let style = theme
|
||||
.toolbar_dropdown_menu
|
||||
.item
|
||||
.style_for(state, is_selected);
|
||||
.in_state(is_selected)
|
||||
.style_for(state);
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(layer.language.name().to_string(), style.text.clone())
|
||||
|
|
|
@ -8,14 +8,18 @@ class LKRoomDelegate: RoomDelegate {
|
|||
var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void
|
||||
var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
|
||||
var onDidUnsubscribeFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
|
||||
var onMuteChangedFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void
|
||||
var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void
|
||||
var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
|
||||
var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
|
||||
|
||||
|
||||
init(
|
||||
data: UnsafeRawPointer,
|
||||
onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
|
||||
onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
|
||||
onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
|
||||
onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
|
||||
onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,
|
||||
onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
|
||||
onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void)
|
||||
{
|
||||
|
@ -25,6 +29,8 @@ class LKRoomDelegate: RoomDelegate {
|
|||
self.onDidUnsubscribeFromRemoteAudioTrack = onDidUnsubscribeFromRemoteAudioTrack
|
||||
self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack
|
||||
self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack
|
||||
self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack
|
||||
self.onActiveSpeakersChanged = onActiveSpeakersChanged
|
||||
}
|
||||
|
||||
func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) {
|
||||
|
@ -40,6 +46,17 @@ class LKRoomDelegate: RoomDelegate {
|
|||
self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
|
||||
}
|
||||
}
|
||||
|
||||
func room(_ room: Room, participant: Participant, didUpdate publication: TrackPublication, muted: Bool) {
|
||||
if publication.kind == .audio {
|
||||
self.onMuteChangedFromRemoteAudioTrack(self.data, publication.sid as CFString, muted)
|
||||
}
|
||||
}
|
||||
|
||||
func room(_ room: Room, didUpdate speakers: [Participant]) {
|
||||
guard let speaker_ids = speakers.compactMap({ $0.identity as CFString }) as CFArray? else { return }
|
||||
self.onActiveSpeakersChanged(self.data, speaker_ids)
|
||||
}
|
||||
|
||||
func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) {
|
||||
if track.kind == .video {
|
||||
|
@ -89,6 +106,8 @@ public func LKRoomDelegateCreate(
|
|||
onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
|
||||
onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
|
||||
onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
|
||||
onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
|
||||
onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,
|
||||
onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
|
||||
onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
|
||||
) -> UnsafeMutableRawPointer {
|
||||
|
@ -97,6 +116,8 @@ public func LKRoomDelegateCreate(
|
|||
onDidDisconnect: onDidDisconnect,
|
||||
onDidSubscribeToRemoteAudioTrack: onDidSubscribeToRemoteAudioTrack,
|
||||
onDidUnsubscribeFromRemoteAudioTrack: onDidUnsubscribeFromRemoteAudioTrack,
|
||||
onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack,
|
||||
onActiveSpeakersChanged: onActiveSpeakerChanged,
|
||||
onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack,
|
||||
onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack
|
||||
)
|
||||
|
@ -169,6 +190,18 @@ public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, partic
|
|||
return nil;
|
||||
}
|
||||
|
||||
@_cdecl("LKRoomAudioTrackPublicationsForRemoteParticipant")
|
||||
public func LKRoomAudioTrackPublicationsForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
|
||||
let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
|
||||
|
||||
for (_, participant) in room.remoteParticipants {
|
||||
if participant.identity == participantId as String {
|
||||
return participant.audioTracks.compactMap { $0 as? RemoteTrackPublication } as CFArray?
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@_cdecl("LKRoomVideoTracksForRemoteParticipant")
|
||||
public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
|
||||
|
@ -201,19 +234,6 @@ public func LKCreateScreenShareTrackForDisplay(display: UnsafeMutableRawPointer)
|
|||
return Unmanaged.passRetained(track).toOpaque()
|
||||
}
|
||||
|
||||
@_cdecl("LKRemoteAudioTrackStart")
|
||||
public func LKRemoteAudioTrackStart(track: UnsafeRawPointer, onStart: @escaping @convention(c) (UnsafeRawPointer, Bool) -> Void, callbackData: UnsafeRawPointer) {
|
||||
let track = Unmanaged<Track>.fromOpaque(track).takeUnretainedValue() as! RemoteAudioTrack
|
||||
|
||||
track.start().then { success in
|
||||
onStart(callbackData, success)
|
||||
}
|
||||
.catch { _ in
|
||||
onStart(callbackData, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@_cdecl("LKVideoRendererCreate")
|
||||
public func LKVideoRendererCreate(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Bool, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) -> UnsafeMutableRawPointer {
|
||||
Unmanaged.passRetained(LKVideoRenderer(data: data, onFrame: onFrame, onDrop: onDrop)).toOpaque()
|
||||
|
@ -247,3 +267,43 @@ public func LKDisplaySources(data: UnsafeRawPointer, callback: @escaping @conven
|
|||
callback(data, nil, error.localizedDescription as CFString)
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("LKLocalTrackPublicationSetMute")
|
||||
public func LKLocalTrackPublicationSetMute(
|
||||
publication: UnsafeRawPointer,
|
||||
muted: Bool,
|
||||
on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void,
|
||||
callback_data: UnsafeRawPointer
|
||||
) {
|
||||
let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
|
||||
|
||||
if muted {
|
||||
publication.mute().then {
|
||||
on_complete(callback_data, nil)
|
||||
}.catch { error in
|
||||
on_complete(callback_data, error.localizedDescription as CFString)
|
||||
}
|
||||
} else {
|
||||
publication.unmute().then {
|
||||
on_complete(callback_data, nil)
|
||||
}.catch { error in
|
||||
on_complete(callback_data, error.localizedDescription as CFString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("LKRemoteTrackPublicationSetEnabled")
|
||||
public func LKRemoteTrackPublicationSetEnabled(
|
||||
publication: UnsafeRawPointer,
|
||||
enabled: Bool,
|
||||
on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void,
|
||||
callback_data: UnsafeRawPointer
|
||||
) {
|
||||
let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
|
||||
|
||||
publication.set(enabled: enabled).then {
|
||||
on_complete(callback_data, nil)
|
||||
}.catch { error in
|
||||
on_complete(callback_data, error.localizedDescription as CFString)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,19 +74,51 @@ fn main() {
|
|||
panic!("unexpected message");
|
||||
}
|
||||
|
||||
audio_track_publication.set_mute(true).await.unwrap();
|
||||
|
||||
println!("waiting for mute changed!");
|
||||
if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } =
|
||||
audio_track_updates.next().await.unwrap()
|
||||
{
|
||||
let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
|
||||
assert_eq!(remote_tracks[0].sid(), track_id);
|
||||
assert_eq!(muted, true);
|
||||
} else {
|
||||
panic!("unexpected message");
|
||||
}
|
||||
|
||||
audio_track_publication.set_mute(false).await.unwrap();
|
||||
|
||||
if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } =
|
||||
audio_track_updates.next().await.unwrap()
|
||||
{
|
||||
let remote_tracks = room_b.remote_audio_tracks("test-participant-1");
|
||||
assert_eq!(remote_tracks[0].sid(), track_id);
|
||||
assert_eq!(muted, false);
|
||||
} else {
|
||||
panic!("unexpected message");
|
||||
}
|
||||
|
||||
println!("Pausing for 5 seconds to test audio, make some noise!");
|
||||
let timer = cx.background().timer(Duration::from_secs(5));
|
||||
timer.await;
|
||||
|
||||
let remote_audio_track = room_b
|
||||
.remote_audio_tracks("test-participant-1")
|
||||
.pop()
|
||||
.unwrap();
|
||||
room_a.unpublish_track(audio_track_publication);
|
||||
|
||||
// Clear out any active speakers changed messages
|
||||
let mut next = audio_track_updates.next().await.unwrap();
|
||||
while let RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } = next {
|
||||
println!("Speakers changed: {:?}", speakers);
|
||||
next = audio_track_updates.next().await.unwrap();
|
||||
}
|
||||
|
||||
if let RemoteAudioTrackUpdate::Unsubscribed {
|
||||
publisher_id,
|
||||
track_id,
|
||||
} = audio_track_updates.next().await.unwrap()
|
||||
} = next
|
||||
{
|
||||
assert_eq!(publisher_id, "test-participant-1");
|
||||
assert_eq!(remote_audio_track.sid(), track_id);
|
||||
|
|
|
@ -32,6 +32,15 @@ extern "C" {
|
|||
publisher_id: CFStringRef,
|
||||
track_id: CFStringRef,
|
||||
),
|
||||
on_mute_changed_from_remote_audio_track: extern "C" fn(
|
||||
callback_data: *mut c_void,
|
||||
track_id: CFStringRef,
|
||||
muted: bool,
|
||||
),
|
||||
on_active_speakers_changed: extern "C" fn(
|
||||
callback_data: *mut c_void,
|
||||
participants: CFArrayRef,
|
||||
),
|
||||
on_did_subscribe_to_remote_video_track: extern "C" fn(
|
||||
callback_data: *mut c_void,
|
||||
publisher_id: CFStringRef,
|
||||
|
@ -72,6 +81,11 @@ extern "C" {
|
|||
participant_id: CFStringRef,
|
||||
) -> CFArrayRef;
|
||||
|
||||
fn LKRoomAudioTrackPublicationsForRemoteParticipant(
|
||||
room: *const c_void,
|
||||
participant_id: CFStringRef,
|
||||
) -> CFArrayRef;
|
||||
|
||||
fn LKRoomVideoTracksForRemoteParticipant(
|
||||
room: *const c_void,
|
||||
participant_id: CFStringRef,
|
||||
|
@ -84,12 +98,6 @@ extern "C" {
|
|||
) -> *const c_void;
|
||||
|
||||
fn LKRemoteAudioTrackGetSid(track: *const c_void) -> CFStringRef;
|
||||
// fn LKRemoteAudioTrackStart(
|
||||
// track: *const c_void,
|
||||
// callback: extern "C" fn(*mut c_void, bool),
|
||||
// callback_data: *mut c_void
|
||||
// );
|
||||
|
||||
fn LKVideoTrackAddRenderer(track: *const c_void, renderer: *const c_void);
|
||||
fn LKRemoteVideoTrackGetSid(track: *const c_void) -> CFStringRef;
|
||||
|
||||
|
@ -103,6 +111,20 @@ extern "C" {
|
|||
);
|
||||
fn LKCreateScreenShareTrackForDisplay(display: *const c_void) -> *const c_void;
|
||||
fn LKLocalAudioTrackCreateTrack() -> *const c_void;
|
||||
|
||||
fn LKLocalTrackPublicationSetMute(
|
||||
publication: *const c_void,
|
||||
muted: bool,
|
||||
on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
|
||||
callback_data: *mut c_void,
|
||||
);
|
||||
|
||||
fn LKRemoteTrackPublicationSetEnabled(
|
||||
publication: *const c_void,
|
||||
enabled: bool,
|
||||
on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
|
||||
callback_data: *mut c_void,
|
||||
);
|
||||
}
|
||||
|
||||
pub type Sid = String;
|
||||
|
@ -206,7 +228,7 @@ impl Room {
|
|||
let tx =
|
||||
unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
|
||||
if error.is_null() {
|
||||
let _ = tx.send(Ok(LocalTrackPublication(publication)));
|
||||
let _ = tx.send(Ok(LocalTrackPublication::new(publication)));
|
||||
} else {
|
||||
let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
|
||||
let _ = tx.send(Err(anyhow!(error)));
|
||||
|
@ -232,7 +254,7 @@ impl Room {
|
|||
let tx =
|
||||
unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<LocalTrackPublication>>) };
|
||||
if error.is_null() {
|
||||
let _ = tx.send(Ok(LocalTrackPublication(publication)));
|
||||
let _ = tx.send(Ok(LocalTrackPublication::new(publication)));
|
||||
} else {
|
||||
let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
|
||||
let _ = tx.send(Err(anyhow!(error)));
|
||||
|
@ -246,7 +268,7 @@ impl Room {
|
|||
Box::into_raw(Box::new(tx)) as *mut c_void,
|
||||
);
|
||||
}
|
||||
async { rx.await.unwrap().context("error publishing video track") }
|
||||
async { rx.await.unwrap().context("error publishing audio track") }
|
||||
}
|
||||
|
||||
pub fn unpublish_track(&self, publication: LocalTrackPublication) {
|
||||
|
@ -313,6 +335,31 @@ impl Room {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn remote_audio_track_publications(
|
||||
&self,
|
||||
participant_id: &str,
|
||||
) -> Vec<Arc<RemoteTrackPublication>> {
|
||||
unsafe {
|
||||
let tracks = LKRoomAudioTrackPublicationsForRemoteParticipant(
|
||||
self.native_room,
|
||||
CFString::new(participant_id).as_concrete_TypeRef(),
|
||||
);
|
||||
|
||||
if tracks.is_null() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let tracks = CFArray::wrap_under_get_rule(tracks);
|
||||
tracks
|
||||
.into_iter()
|
||||
.map(|native_track_publication| {
|
||||
let native_track_publication = *native_track_publication;
|
||||
Arc::new(RemoteTrackPublication::new(native_track_publication))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remote_audio_track_updates(&self) -> mpsc::UnboundedReceiver<RemoteAudioTrackUpdate> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.remote_audio_track_subscribers.lock().push(tx);
|
||||
|
@ -343,6 +390,28 @@ impl Room {
|
|||
});
|
||||
}
|
||||
|
||||
fn mute_changed_from_remote_audio_track(&self, track_id: String, muted: bool) {
|
||||
self.remote_audio_track_subscribers.lock().retain(|tx| {
|
||||
tx.unbounded_send(RemoteAudioTrackUpdate::MuteChanged {
|
||||
track_id: track_id.clone(),
|
||||
muted,
|
||||
})
|
||||
.is_ok()
|
||||
});
|
||||
}
|
||||
|
||||
// A vec of publisher IDs
|
||||
fn active_speakers_changed(&self, speakers: Vec<String>) {
|
||||
self.remote_audio_track_subscribers
|
||||
.lock()
|
||||
.retain(move |tx| {
|
||||
tx.unbounded_send(RemoteAudioTrackUpdate::ActiveSpeakersChanged {
|
||||
speakers: speakers.clone(),
|
||||
})
|
||||
.is_ok()
|
||||
});
|
||||
}
|
||||
|
||||
fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {
|
||||
let track = Arc::new(track);
|
||||
self.remote_video_track_subscribers.lock().retain(|tx| {
|
||||
|
@ -407,6 +476,8 @@ impl RoomDelegate {
|
|||
Self::on_did_disconnect,
|
||||
Self::on_did_subscribe_to_remote_audio_track,
|
||||
Self::on_did_unsubscribe_from_remote_audio_track,
|
||||
Self::on_mute_change_from_remote_audio_track,
|
||||
Self::on_active_speakers_changed,
|
||||
Self::on_did_subscribe_to_remote_video_track,
|
||||
Self::on_did_unsubscribe_from_remote_video_track,
|
||||
)
|
||||
|
@ -455,6 +526,42 @@ impl RoomDelegate {
|
|||
let _ = Weak::into_raw(room);
|
||||
}
|
||||
|
||||
extern "C" fn on_mute_change_from_remote_audio_track(
|
||||
room: *mut c_void,
|
||||
track_id: CFStringRef,
|
||||
muted: bool,
|
||||
) {
|
||||
let room = unsafe { Weak::from_raw(room as *mut Room) };
|
||||
let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
|
||||
if let Some(room) = room.upgrade() {
|
||||
room.mute_changed_from_remote_audio_track(track_id, muted);
|
||||
}
|
||||
let _ = Weak::into_raw(room);
|
||||
}
|
||||
|
||||
extern "C" fn on_active_speakers_changed(room: *mut c_void, participants: CFArrayRef) {
|
||||
if participants.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let room = unsafe { Weak::from_raw(room as *mut Room) };
|
||||
let speakers = unsafe {
|
||||
CFArray::wrap_under_get_rule(participants)
|
||||
.into_iter()
|
||||
.map(
|
||||
|speaker: core_foundation::base::ItemRef<'_, *const c_void>| {
|
||||
CFString::wrap_under_get_rule(*speaker as CFStringRef).to_string()
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
};
|
||||
|
||||
if let Some(room) = room.upgrade() {
|
||||
room.active_speakers_changed(speakers);
|
||||
}
|
||||
let _ = Weak::into_raw(room);
|
||||
}
|
||||
|
||||
extern "C" fn on_did_subscribe_to_remote_video_track(
|
||||
room: *mut c_void,
|
||||
publisher_id: CFStringRef,
|
||||
|
@ -525,12 +632,88 @@ impl Drop for LocalVideoTrack {
|
|||
|
||||
pub struct LocalTrackPublication(*const c_void);
|
||||
|
||||
impl LocalTrackPublication {
|
||||
pub fn new(native_track_publication: *const c_void) -> Self {
|
||||
unsafe {
|
||||
CFRetain(native_track_publication);
|
||||
}
|
||||
Self(native_track_publication)
|
||||
}
|
||||
|
||||
pub fn set_mute(&self, muted: bool) -> impl Future<Output = Result<()>> {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
|
||||
extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) {
|
||||
let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender<Result<()>>) };
|
||||
if error.is_null() {
|
||||
tx.send(Ok(())).ok();
|
||||
} else {
|
||||
let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
|
||||
tx.send(Err(anyhow!(error))).ok();
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
LKLocalTrackPublicationSetMute(
|
||||
self.0,
|
||||
muted,
|
||||
complete_callback,
|
||||
Box::into_raw(Box::new(tx)) as *mut c_void,
|
||||
)
|
||||
}
|
||||
|
||||
async move { rx.await.unwrap() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LocalTrackPublication {
|
||||
fn drop(&mut self) {
|
||||
unsafe { CFRelease(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RemoteTrackPublication(*const c_void);
|
||||
|
||||
impl RemoteTrackPublication {
|
||||
pub fn new(native_track_publication: *const c_void) -> Self {
|
||||
unsafe {
|
||||
CFRetain(native_track_publication);
|
||||
}
|
||||
Self(native_track_publication)
|
||||
}
|
||||
|
||||
pub fn set_enabled(&self, enabled: bool) -> impl Future<Output = Result<()>> {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
|
||||
extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) {
|
||||
let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender<Result<()>>) };
|
||||
if error.is_null() {
|
||||
tx.send(Ok(())).ok();
|
||||
} else {
|
||||
let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
|
||||
tx.send(Err(anyhow!(error))).ok();
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
LKRemoteTrackPublicationSetEnabled(
|
||||
self.0,
|
||||
enabled,
|
||||
complete_callback,
|
||||
Box::into_raw(Box::new(tx)) as *mut c_void,
|
||||
)
|
||||
}
|
||||
|
||||
async move { rx.await.unwrap() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RemoteTrackPublication {
|
||||
fn drop(&mut self) {
|
||||
unsafe { CFRelease(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteAudioTrack {
|
||||
_native_track: *const c_void,
|
||||
|
@ -557,6 +740,14 @@ impl RemoteAudioTrack {
|
|||
pub fn publisher_id(&self) -> &str {
|
||||
&self.publisher_id
|
||||
}
|
||||
|
||||
pub fn enable(&self) -> impl Future<Output = Result<()>> {
|
||||
async { Ok(()) }
|
||||
}
|
||||
|
||||
pub fn disable(&self) -> impl Future<Output = Result<()>> {
|
||||
async { Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -639,6 +830,8 @@ pub enum RemoteVideoTrackUpdate {
|
|||
}
|
||||
|
||||
pub enum RemoteAudioTrackUpdate {
|
||||
ActiveSpeakersChanged { speakers: Vec<Sid> },
|
||||
MuteChanged { track_id: Sid, muted: bool },
|
||||
Subscribed(Arc<RemoteAudioTrack>),
|
||||
Unsubscribed { publisher_id: Sid, track_id: Sid },
|
||||
}
|
||||
|
|
|
@ -410,6 +410,23 @@ impl Room {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn remote_audio_track_publications(
|
||||
&self,
|
||||
publisher_id: &str,
|
||||
) -> Vec<Arc<RemoteTrackPublication>> {
|
||||
if !self.is_connected() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.test_server()
|
||||
.audio_tracks(self.token())
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter(|track| track.publisher_id() == publisher_id)
|
||||
.map(|_track| Arc::new(RemoteTrackPublication {}))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn remote_video_tracks(&self, publisher_id: &str) -> Vec<Arc<RemoteVideoTrack>> {
|
||||
if !self.is_connected() {
|
||||
return Vec::new();
|
||||
|
@ -475,6 +492,20 @@ impl Drop for Room {
|
|||
|
||||
pub struct LocalTrackPublication;
|
||||
|
||||
impl LocalTrackPublication {
|
||||
pub fn set_mute(&self, _mute: bool) -> impl Future<Output = Result<()>> {
|
||||
async { Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RemoteTrackPublication;
|
||||
|
||||
impl RemoteTrackPublication {
|
||||
pub fn set_enabled(&self, _enabled: bool) -> impl Future<Output = Result<()>> {
|
||||
async { Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LocalVideoTrack {
|
||||
frames_rx: async_broadcast::Receiver<Frame>,
|
||||
|
@ -517,6 +548,7 @@ impl RemoteVideoTrack {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteAudioTrack {
|
||||
sid: Sid,
|
||||
publisher_id: Sid,
|
||||
|
@ -530,6 +562,14 @@ impl RemoteAudioTrack {
|
|||
pub fn publisher_id(&self) -> &str {
|
||||
&self.publisher_id
|
||||
}
|
||||
|
||||
pub fn enable(&self) -> impl Future<Output = Result<()>> {
|
||||
async { Ok(()) }
|
||||
}
|
||||
|
||||
pub fn disable(&self) -> impl Future<Output = Result<()>> {
|
||||
async { Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -540,6 +580,8 @@ pub enum RemoteVideoTrackUpdate {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub enum RemoteAudioTrackUpdate {
|
||||
ActiveSpeakersChanged { speakers: Vec<Sid> },
|
||||
MuteChanged { track_id: Sid, muted: bool },
|
||||
Subscribed(Arc<RemoteAudioTrack>),
|
||||
Unsubscribed { publisher_id: Sid, track_id: Sid },
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ const JSON_RPC_VERSION: &str = "2.0";
|
|||
const CONTENT_LEN_HEADER: &str = "Content-Length: ";
|
||||
|
||||
type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
|
||||
type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
|
||||
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
|
||||
type IoHandler = Box<dyn Send + FnMut(bool, &str)>;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
@ -309,9 +309,9 @@ impl LanguageServer {
|
|||
if let Some(error) = error {
|
||||
handler(Err(error));
|
||||
} else if let Some(result) = result {
|
||||
handler(Ok(result.get()));
|
||||
handler(Ok(result.get().into()));
|
||||
} else {
|
||||
handler(Ok("null"));
|
||||
handler(Ok("null".into()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -464,11 +464,13 @@ impl LanguageServer {
|
|||
let response_handlers = self.response_handlers.clone();
|
||||
let next_id = AtomicUsize::new(self.next_id.load(SeqCst));
|
||||
let outbound_tx = self.outbound_tx.clone();
|
||||
let executor = self.executor.clone();
|
||||
let mut output_done = self.output_done_rx.lock().take().unwrap();
|
||||
let shutdown_request = Self::request_internal::<request::Shutdown>(
|
||||
&next_id,
|
||||
&response_handlers,
|
||||
&outbound_tx,
|
||||
&executor,
|
||||
(),
|
||||
);
|
||||
let exit = Self::notify_internal::<notification::Exit>(&outbound_tx, ());
|
||||
|
@ -665,6 +667,7 @@ impl LanguageServer {
|
|||
&self.next_id,
|
||||
&self.response_handlers,
|
||||
&self.outbound_tx,
|
||||
&self.executor,
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
@ -673,6 +676,7 @@ impl LanguageServer {
|
|||
next_id: &AtomicUsize,
|
||||
response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
|
||||
outbound_tx: &channel::Sender<String>,
|
||||
executor: &Arc<executor::Background>,
|
||||
params: T::Params,
|
||||
) -> impl 'static + Future<Output = Result<T::Result>>
|
||||
where
|
||||
|
@ -693,15 +697,20 @@ impl LanguageServer {
|
|||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("server shut down"))
|
||||
.map(|handlers| {
|
||||
let executor = executor.clone();
|
||||
handlers.insert(
|
||||
id,
|
||||
Box::new(move |result| {
|
||||
let response = match result {
|
||||
Ok(response) => serde_json::from_str(response)
|
||||
.context("failed to deserialize response"),
|
||||
Err(error) => Err(anyhow!("{}", error.message)),
|
||||
};
|
||||
let _ = tx.send(response);
|
||||
executor
|
||||
.spawn(async move {
|
||||
let response = match result {
|
||||
Ok(response) => serde_json::from_str(&response)
|
||||
.context("failed to deserialize response"),
|
||||
Err(error) => Err(anyhow!("{}", error.message)),
|
||||
};
|
||||
let _ = tx.send(response);
|
||||
})
|
||||
.detach();
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -204,7 +204,7 @@ impl PickerDelegate for OutlineViewDelegate {
|
|||
cx: &AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
||||
let string_match = &self.matches[ix];
|
||||
let outline_item = &self.outline.items[string_match.candidate_id];
|
||||
|
||||
|
|
|
@ -45,6 +45,12 @@ pub trait PickerDelegate: Sized + 'static {
|
|||
fn center_selection_after_match_updates(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn render_header(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
|
||||
None
|
||||
}
|
||||
fn render_footer(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: PickerDelegate> Entity for Picker<D> {
|
||||
|
@ -77,6 +83,7 @@ impl<D: PickerDelegate> View for Picker<D> {
|
|||
.contained()
|
||||
.with_style(editor_style),
|
||||
)
|
||||
.with_children(self.delegate.render_header(cx))
|
||||
.with_children(if match_count == 0 {
|
||||
if query.is_empty() {
|
||||
None
|
||||
|
@ -118,6 +125,7 @@ impl<D: PickerDelegate> View for Picker<D> {
|
|||
.into_any(),
|
||||
)
|
||||
})
|
||||
.with_children(self.delegate.render_footer(cx))
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.constrained()
|
||||
|
|
|
@ -64,7 +64,7 @@ use std::{
|
|||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::Range,
|
||||
path::{Component, Path, PathBuf},
|
||||
path::{self, Component, Path, PathBuf},
|
||||
process::Stdio,
|
||||
rc::Rc,
|
||||
str,
|
||||
|
@ -481,6 +481,7 @@ impl Project {
|
|||
client.add_model_request_handler(Self::handle_rename_project_entry);
|
||||
client.add_model_request_handler(Self::handle_copy_project_entry);
|
||||
client.add_model_request_handler(Self::handle_delete_project_entry);
|
||||
client.add_model_request_handler(Self::handle_expand_project_entry);
|
||||
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
|
||||
client.add_model_request_handler(Self::handle_apply_code_action);
|
||||
client.add_model_request_handler(Self::handle_on_type_formatting);
|
||||
|
@ -1075,6 +1076,40 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn expand_entry(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
entry_id: ProjectEntryId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let worktree = self.worktree_for_id(worktree_id, cx)?;
|
||||
if self.is_local() {
|
||||
worktree.update(cx, |worktree, cx| {
|
||||
worktree.as_local_mut().unwrap().expand_entry(entry_id, cx)
|
||||
})
|
||||
} else {
|
||||
let worktree = worktree.downgrade();
|
||||
let request = self.client.request(proto::ExpandProjectEntry {
|
||||
project_id: self.remote_id().unwrap(),
|
||||
entry_id: entry_id.to_proto(),
|
||||
});
|
||||
Some(cx.spawn_weak(|_, mut cx| async move {
|
||||
let response = request.await?;
|
||||
if let Some(worktree) = worktree.upgrade(&cx) {
|
||||
worktree
|
||||
.update(&mut cx, |worktree, _| {
|
||||
worktree
|
||||
.as_remote_mut()
|
||||
.unwrap()
|
||||
.wait_for_snapshot(response.worktree_scan_id as usize)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
if self.client_state.is_some() {
|
||||
return Err(anyhow!("project was already shared"));
|
||||
|
@ -3305,23 +3340,44 @@ impl Project {
|
|||
for watcher in params.watchers {
|
||||
for worktree in &self.worktrees {
|
||||
if let Some(worktree) = worktree.upgrade(cx) {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Some(abs_path) = worktree.abs_path().to_str() {
|
||||
if let Some(suffix) = match &watcher.glob_pattern {
|
||||
lsp::GlobPattern::String(s) => s,
|
||||
lsp::GlobPattern::Relative(rp) => &rp.pattern,
|
||||
}
|
||||
.strip_prefix(abs_path)
|
||||
.and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR))
|
||||
{
|
||||
if let Some(glob) = Glob::new(suffix).log_err() {
|
||||
builders
|
||||
.entry(worktree.id())
|
||||
.or_insert_with(|| GlobSetBuilder::new())
|
||||
.add(glob);
|
||||
let glob_is_inside_worktree = worktree.update(cx, |tree, _| {
|
||||
if let Some(abs_path) = tree.abs_path().to_str() {
|
||||
let relative_glob_pattern = match &watcher.glob_pattern {
|
||||
lsp::GlobPattern::String(s) => s
|
||||
.strip_prefix(abs_path)
|
||||
.and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR)),
|
||||
lsp::GlobPattern::Relative(rp) => {
|
||||
let base_uri = match &rp.base_uri {
|
||||
lsp::OneOf::Left(workspace_folder) => {
|
||||
&workspace_folder.uri
|
||||
}
|
||||
lsp::OneOf::Right(base_uri) => base_uri,
|
||||
};
|
||||
base_uri.to_file_path().ok().and_then(|file_path| {
|
||||
(file_path.to_str() == Some(abs_path))
|
||||
.then_some(rp.pattern.as_str())
|
||||
})
|
||||
}
|
||||
};
|
||||
if let Some(relative_glob_pattern) = relative_glob_pattern {
|
||||
let literal_prefix =
|
||||
glob_literal_prefix(&relative_glob_pattern);
|
||||
tree.as_local_mut()
|
||||
.unwrap()
|
||||
.add_path_prefix_to_scan(Path::new(literal_prefix).into());
|
||||
if let Some(glob) = Glob::new(relative_glob_pattern).log_err() {
|
||||
builders
|
||||
.entry(tree.id())
|
||||
.or_insert_with(|| GlobSetBuilder::new())
|
||||
.add(glob);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
false
|
||||
});
|
||||
if glob_is_inside_worktree {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5947,6 +6003,29 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
async fn handle_expand_project_entry(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::ExpandProjectEntry>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::ExpandProjectEntryResponse> {
|
||||
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
|
||||
let worktree = this
|
||||
.read_with(&cx, |this, cx| this.worktree_for_entry(entry_id, cx))
|
||||
.ok_or_else(|| anyhow!("invalid request"))?;
|
||||
worktree
|
||||
.update(&mut cx, |worktree, cx| {
|
||||
worktree
|
||||
.as_local_mut()
|
||||
.unwrap()
|
||||
.expand_entry(entry_id, cx)
|
||||
.ok_or_else(|| anyhow!("invalid entry"))
|
||||
})?
|
||||
.await?;
|
||||
let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()) as u64;
|
||||
Ok(proto::ExpandProjectEntryResponse { worktree_scan_id })
|
||||
}
|
||||
|
||||
async fn handle_update_diagnostic_summary(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateDiagnosticSummary>,
|
||||
|
@ -7289,6 +7368,22 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
fn glob_literal_prefix<'a>(glob: &'a str) -> &'a str {
|
||||
let mut literal_end = 0;
|
||||
for (i, part) in glob.split(path::MAIN_SEPARATOR).enumerate() {
|
||||
if part.contains(&['*', '?', '{', '}']) {
|
||||
break;
|
||||
} else {
|
||||
if i > 0 {
|
||||
// Acount for separator prior to this part
|
||||
literal_end += path::MAIN_SEPARATOR.len_utf8();
|
||||
}
|
||||
literal_end += part.len();
|
||||
}
|
||||
}
|
||||
&glob[..literal_end]
|
||||
}
|
||||
|
||||
impl WorktreeHandle {
|
||||
pub fn upgrade(&self, cx: &AppContext) -> Option<ModelHandle<Worktree>> {
|
||||
match self {
|
||||
|
|
|
@ -535,8 +535,28 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
|||
fs.insert_tree(
|
||||
"/the-root",
|
||||
json!({
|
||||
"a.rs": "",
|
||||
"b.rs": "",
|
||||
".gitignore": "target\n",
|
||||
"src": {
|
||||
"a.rs": "",
|
||||
"b.rs": "",
|
||||
},
|
||||
"target": {
|
||||
"x": {
|
||||
"out": {
|
||||
"x.rs": ""
|
||||
}
|
||||
},
|
||||
"y": {
|
||||
"out": {
|
||||
"y.rs": "",
|
||||
}
|
||||
},
|
||||
"z": {
|
||||
"out": {
|
||||
"z.rs": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
@ -550,11 +570,32 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
|||
// Start the language server by opening a buffer with a compatible file extension.
|
||||
let _buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/the-root/a.rs", cx)
|
||||
project.open_local_buffer("/the-root/src/a.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
|
||||
project.read_with(cx, |project, cx| {
|
||||
let worktree = project.worktrees(cx).next().unwrap();
|
||||
assert_eq!(
|
||||
worktree
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
.entries(true)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_ignored))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new(""), false),
|
||||
(Path::new(".gitignore"), false),
|
||||
(Path::new("src"), false),
|
||||
(Path::new("src/a.rs"), false),
|
||||
(Path::new("src/b.rs"), false),
|
||||
(Path::new("target"), true),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Keep track of the FS events reported to the language server.
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
let file_changes = Arc::new(Mutex::new(Vec::new()));
|
||||
|
@ -565,12 +606,20 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
|||
method: "workspace/didChangeWatchedFiles".to_string(),
|
||||
register_options: serde_json::to_value(
|
||||
lsp::DidChangeWatchedFilesRegistrationOptions {
|
||||
watchers: vec![lsp::FileSystemWatcher {
|
||||
glob_pattern: lsp::GlobPattern::String(
|
||||
"/the-root/*.{rs,c}".to_string(),
|
||||
),
|
||||
kind: None,
|
||||
}],
|
||||
watchers: vec![
|
||||
lsp::FileSystemWatcher {
|
||||
glob_pattern: lsp::GlobPattern::String(
|
||||
"/the-root/src/*.{rs,c}".to_string(),
|
||||
),
|
||||
kind: None,
|
||||
},
|
||||
lsp::FileSystemWatcher {
|
||||
glob_pattern: lsp::GlobPattern::String(
|
||||
"/the-root/target/y/**/*.rs".to_string(),
|
||||
),
|
||||
kind: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
.ok(),
|
||||
|
@ -588,17 +637,50 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
|||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(file_changes.lock().len(), 0);
|
||||
assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
|
||||
|
||||
// Now the language server has asked us to watch an ignored directory path,
|
||||
// so we recursively load it.
|
||||
project.read_with(cx, |project, cx| {
|
||||
let worktree = project.worktrees(cx).next().unwrap();
|
||||
assert_eq!(
|
||||
worktree
|
||||
.read(cx)
|
||||
.snapshot()
|
||||
.entries(true)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_ignored))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new(""), false),
|
||||
(Path::new(".gitignore"), false),
|
||||
(Path::new("src"), false),
|
||||
(Path::new("src/a.rs"), false),
|
||||
(Path::new("src/b.rs"), false),
|
||||
(Path::new("target"), true),
|
||||
(Path::new("target/x"), true),
|
||||
(Path::new("target/y"), true),
|
||||
(Path::new("target/y/out"), true),
|
||||
(Path::new("target/y/out/y.rs"), true),
|
||||
(Path::new("target/z"), true),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Perform some file system mutations, two of which match the watched patterns,
|
||||
// and one of which does not.
|
||||
fs.create_file("/the-root/c.rs".as_ref(), Default::default())
|
||||
fs.create_file("/the-root/src/c.rs".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
fs.create_file("/the-root/d.txt".as_ref(), Default::default())
|
||||
fs.create_file("/the-root/src/d.txt".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
fs.remove_file("/the-root/b.rs".as_ref(), Default::default())
|
||||
fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -608,11 +690,15 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
|||
&*file_changes.lock(),
|
||||
&[
|
||||
lsp::FileEvent {
|
||||
uri: lsp::Url::from_file_path("/the-root/b.rs").unwrap(),
|
||||
uri: lsp::Url::from_file_path("/the-root/src/b.rs").unwrap(),
|
||||
typ: lsp::FileChangeType::DELETED,
|
||||
},
|
||||
lsp::FileEvent {
|
||||
uri: lsp::Url::from_file_path("/the-root/c.rs").unwrap(),
|
||||
uri: lsp::Url::from_file_path("/the-root/src/c.rs").unwrap(),
|
||||
typ: lsp::FileChangeType::CREATED,
|
||||
},
|
||||
lsp::FileEvent {
|
||||
uri: lsp::Url::from_file_path("/the-root/target/y/out/y2.rs").unwrap(),
|
||||
typ: lsp::FileChangeType::CREATED,
|
||||
},
|
||||
]
|
||||
|
@ -3846,6 +3932,14 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_glob_literal_prefix() {
|
||||
assert_eq!(glob_literal_prefix("**/*.js"), "");
|
||||
assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules");
|
||||
assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo");
|
||||
assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
|
||||
}
|
||||
|
||||
async fn search(
|
||||
project: &ModelHandle<Project>,
|
||||
query: SearchQuery,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
worktree::{Event, Snapshot, WorktreeHandle},
|
||||
EntryKind, PathChange, Worktree,
|
||||
Entry, EntryKind, PathChange, Worktree,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::Client;
|
||||
|
@ -8,12 +8,14 @@ use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
|
|||
use git::GITIGNORE;
|
||||
use gpui::{executor::Deterministic, ModelContext, Task, TestAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use postage::stream::Stream;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
env,
|
||||
fmt::Write,
|
||||
mem,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
@ -34,11 +36,8 @@ async fn test_traversal(cx: &mut TestAppContext) {
|
|||
)
|
||||
.await;
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
build_client(cx),
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs,
|
||||
|
@ -107,11 +106,8 @@ async fn test_descendent_entries(cx: &mut TestAppContext) {
|
|||
)
|
||||
.await;
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
build_client(cx),
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs,
|
||||
|
@ -154,7 +150,18 @@ async fn test_descendent_entries(cx: &mut TestAppContext) {
|
|||
.collect::<Vec<_>>(),
|
||||
vec![Path::new("g"), Path::new("g/h"),]
|
||||
);
|
||||
});
|
||||
|
||||
// Expand gitignored directory.
|
||||
tree.read_with(cx, |tree, _| {
|
||||
tree.as_local()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(vec![Path::new("i/j").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.descendent_entries(false, false, Path::new("i"))
|
||||
.map(|entry| entry.path.as_ref())
|
||||
|
@ -196,9 +203,8 @@ async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppCo
|
|||
fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
|
||||
fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
|
||||
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
build_client(cx),
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs.clone(),
|
||||
|
@ -257,32 +263,473 @@ async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppCo
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||
// .gitignores are handled explicitly by Zed and do not use the git
|
||||
// machinery that the git_tests module checks
|
||||
let parent_dir = temp_tree(json!({
|
||||
".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
|
||||
"tree": {
|
||||
".git": {},
|
||||
".gitignore": "ignored-dir\n",
|
||||
"tracked-dir": {
|
||||
"tracked-file1": "",
|
||||
"ancestor-ignored-file1": "",
|
||||
async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"dir1": {
|
||||
"deps": {
|
||||
// symlinks here
|
||||
},
|
||||
"src": {
|
||||
"a.rs": "",
|
||||
"b.rs": "",
|
||||
},
|
||||
},
|
||||
"ignored-dir": {
|
||||
"ignored-file1": ""
|
||||
"dir2": {
|
||||
"src": {
|
||||
"c.rs": "",
|
||||
"d.rs": "",
|
||||
}
|
||||
},
|
||||
"dir3": {
|
||||
"deps": {},
|
||||
"src": {
|
||||
"e.rs": "",
|
||||
"f.rs": "",
|
||||
},
|
||||
}
|
||||
}
|
||||
}));
|
||||
let dir = parent_dir.path().join("tree");
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
// These symlinks point to directories outside of the worktree's root, dir1.
|
||||
fs.insert_symlink("/root/dir1/deps/dep-dir2", "../../dir2".into())
|
||||
.await;
|
||||
fs.insert_symlink("/root/dir1/deps/dep-dir3", "../../dir3".into())
|
||||
.await;
|
||||
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
dir.as_path(),
|
||||
build_client(cx),
|
||||
Path::new("/root/dir1"),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
let tree_updates = Arc::new(Mutex::new(Vec::new()));
|
||||
tree.update(cx, |_, cx| {
|
||||
let tree_updates = tree_updates.clone();
|
||||
cx.subscribe(&tree, move |_, _, event, _| {
|
||||
if let Event::UpdatedEntries(update) = event {
|
||||
tree_updates.lock().extend(
|
||||
update
|
||||
.iter()
|
||||
.map(|(path, _, change)| (path.clone(), *change)),
|
||||
);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
// The symlinked directories are not scanned by default.
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(true)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_external))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(Path::new(""), false),
|
||||
(Path::new("deps"), false),
|
||||
(Path::new("deps/dep-dir2"), true),
|
||||
(Path::new("deps/dep-dir3"), true),
|
||||
(Path::new("src"), false),
|
||||
(Path::new("src/a.rs"), false),
|
||||
(Path::new("src/b.rs"), false),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
tree.entry_for_path("deps/dep-dir2").unwrap().kind,
|
||||
EntryKind::UnloadedDir
|
||||
);
|
||||
});
|
||||
|
||||
// Expand one of the symlinked directories.
|
||||
tree.read_with(cx, |tree, _| {
|
||||
tree.as_local()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
|
||||
// The expanded directory's contents are loaded. Subdirectories are
|
||||
// not scanned yet.
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(true)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_external))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(Path::new(""), false),
|
||||
(Path::new("deps"), false),
|
||||
(Path::new("deps/dep-dir2"), true),
|
||||
(Path::new("deps/dep-dir3"), true),
|
||||
(Path::new("deps/dep-dir3/deps"), true),
|
||||
(Path::new("deps/dep-dir3/src"), true),
|
||||
(Path::new("src"), false),
|
||||
(Path::new("src/a.rs"), false),
|
||||
(Path::new("src/b.rs"), false),
|
||||
]
|
||||
);
|
||||
});
|
||||
assert_eq!(
|
||||
mem::take(&mut *tree_updates.lock()),
|
||||
&[
|
||||
(Path::new("deps/dep-dir3").into(), PathChange::Loaded),
|
||||
(Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded),
|
||||
(Path::new("deps/dep-dir3/src").into(), PathChange::Loaded)
|
||||
]
|
||||
);
|
||||
|
||||
// Expand a subdirectory of one of the symlinked directories.
|
||||
tree.read_with(cx, |tree, _| {
|
||||
tree.as_local()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
|
||||
// The expanded subdirectory's contents are loaded.
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(true)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_external))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(Path::new(""), false),
|
||||
(Path::new("deps"), false),
|
||||
(Path::new("deps/dep-dir2"), true),
|
||||
(Path::new("deps/dep-dir3"), true),
|
||||
(Path::new("deps/dep-dir3/deps"), true),
|
||||
(Path::new("deps/dep-dir3/src"), true),
|
||||
(Path::new("deps/dep-dir3/src/e.rs"), true),
|
||||
(Path::new("deps/dep-dir3/src/f.rs"), true),
|
||||
(Path::new("src"), false),
|
||||
(Path::new("src/a.rs"), false),
|
||||
(Path::new("src/b.rs"), false),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
mem::take(&mut *tree_updates.lock()),
|
||||
&[
|
||||
(Path::new("deps/dep-dir3/src").into(), PathChange::Loaded),
|
||||
(
|
||||
Path::new("deps/dep-dir3/src/e.rs").into(),
|
||||
PathChange::Loaded
|
||||
),
|
||||
(
|
||||
Path::new("deps/dep-dir3/src/f.rs").into(),
|
||||
PathChange::Loaded
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
".gitignore": "node_modules\n",
|
||||
"one": {
|
||||
"node_modules": {
|
||||
"a": {
|
||||
"a1.js": "a1",
|
||||
"a2.js": "a2",
|
||||
},
|
||||
"b": {
|
||||
"b1.js": "b1",
|
||||
"b2.js": "b2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"two": {
|
||||
"x.js": "",
|
||||
"y.js": "",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let tree = Worktree::local(
|
||||
build_client(cx),
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(true)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_ignored))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(Path::new(""), false),
|
||||
(Path::new(".gitignore"), false),
|
||||
(Path::new("one"), false),
|
||||
(Path::new("one/node_modules"), true),
|
||||
(Path::new("two"), false),
|
||||
(Path::new("two/x.js"), false),
|
||||
(Path::new("two/y.js"), false),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Open a file that is nested inside of a gitignored directory that
|
||||
// has not yet been expanded.
|
||||
let prev_read_dir_count = fs.read_dir_call_count();
|
||||
let buffer = tree
|
||||
.update(cx, |tree, cx| {
|
||||
tree.as_local_mut()
|
||||
.unwrap()
|
||||
.load_buffer(0, "one/node_modules/b/b1.js".as_ref(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.read_with(cx, |tree, cx| {
|
||||
assert_eq!(
|
||||
tree.entries(true)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_ignored))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(Path::new(""), false),
|
||||
(Path::new(".gitignore"), false),
|
||||
(Path::new("one"), false),
|
||||
(Path::new("one/node_modules"), true),
|
||||
(Path::new("one/node_modules/a"), true),
|
||||
(Path::new("one/node_modules/b"), true),
|
||||
(Path::new("one/node_modules/b/b1.js"), true),
|
||||
(Path::new("one/node_modules/b/b2.js"), true),
|
||||
(Path::new("two"), false),
|
||||
(Path::new("two/x.js"), false),
|
||||
(Path::new("two/y.js"), false),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
buffer.read(cx).file().unwrap().path().as_ref(),
|
||||
Path::new("one/node_modules/b/b1.js")
|
||||
);
|
||||
|
||||
// Only the newly-expanded directories are scanned.
|
||||
assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2);
|
||||
});
|
||||
|
||||
// Open another file in a different subdirectory of the same
|
||||
// gitignored directory.
|
||||
let prev_read_dir_count = fs.read_dir_call_count();
|
||||
let buffer = tree
|
||||
.update(cx, |tree, cx| {
|
||||
tree.as_local_mut()
|
||||
.unwrap()
|
||||
.load_buffer(0, "one/node_modules/a/a2.js".as_ref(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.read_with(cx, |tree, cx| {
|
||||
assert_eq!(
|
||||
tree.entries(true)
|
||||
.map(|entry| (entry.path.as_ref(), entry.is_ignored))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
(Path::new(""), false),
|
||||
(Path::new(".gitignore"), false),
|
||||
(Path::new("one"), false),
|
||||
(Path::new("one/node_modules"), true),
|
||||
(Path::new("one/node_modules/a"), true),
|
||||
(Path::new("one/node_modules/a/a1.js"), true),
|
||||
(Path::new("one/node_modules/a/a2.js"), true),
|
||||
(Path::new("one/node_modules/b"), true),
|
||||
(Path::new("one/node_modules/b/b1.js"), true),
|
||||
(Path::new("one/node_modules/b/b2.js"), true),
|
||||
(Path::new("two"), false),
|
||||
(Path::new("two/x.js"), false),
|
||||
(Path::new("two/y.js"), false),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
buffer.read(cx).file().unwrap().path().as_ref(),
|
||||
Path::new("one/node_modules/a/a2.js")
|
||||
);
|
||||
|
||||
// Only the newly-expanded directory is scanned.
|
||||
assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
".gitignore": "node_modules\n",
|
||||
"a": {
|
||||
"a.js": "",
|
||||
},
|
||||
"b": {
|
||||
"b.js": "",
|
||||
},
|
||||
"node_modules": {
|
||||
"c": {
|
||||
"c.js": "",
|
||||
},
|
||||
"d": {
|
||||
"d.js": "",
|
||||
"e": {
|
||||
"e1.js": "",
|
||||
"e2.js": "",
|
||||
},
|
||||
"f": {
|
||||
"f1.js": "",
|
||||
"f2.js": "",
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let tree = Worktree::local(
|
||||
build_client(cx),
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
// Open a file within the gitignored directory, forcing some of its
|
||||
// subdirectories to be read, but not all.
|
||||
let read_dir_count_1 = fs.read_dir_call_count();
|
||||
tree.read_with(cx, |tree, _| {
|
||||
tree.as_local()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
|
||||
// Those subdirectories are now loaded.
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(true)
|
||||
.map(|e| (e.path.as_ref(), e.is_ignored))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new(""), false),
|
||||
(Path::new(".gitignore"), false),
|
||||
(Path::new("a"), false),
|
||||
(Path::new("a/a.js"), false),
|
||||
(Path::new("b"), false),
|
||||
(Path::new("b/b.js"), false),
|
||||
(Path::new("node_modules"), true),
|
||||
(Path::new("node_modules/c"), true),
|
||||
(Path::new("node_modules/d"), true),
|
||||
(Path::new("node_modules/d/d.js"), true),
|
||||
(Path::new("node_modules/d/e"), true),
|
||||
(Path::new("node_modules/d/f"), true),
|
||||
]
|
||||
);
|
||||
});
|
||||
let read_dir_count_2 = fs.read_dir_call_count();
|
||||
assert_eq!(read_dir_count_2 - read_dir_count_1, 2);
|
||||
|
||||
// Update the gitignore so that node_modules is no longer ignored,
|
||||
// but a subdirectory is ignored
|
||||
fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
cx.foreground().run_until_parked();
|
||||
|
||||
// All of the directories that are no longer ignored are now loaded.
|
||||
tree.read_with(cx, |tree, _| {
|
||||
assert_eq!(
|
||||
tree.entries(true)
|
||||
.map(|e| (e.path.as_ref(), e.is_ignored))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new(""), false),
|
||||
(Path::new(".gitignore"), false),
|
||||
(Path::new("a"), false),
|
||||
(Path::new("a/a.js"), false),
|
||||
(Path::new("b"), false),
|
||||
(Path::new("b/b.js"), false),
|
||||
// This directory is no longer ignored
|
||||
(Path::new("node_modules"), false),
|
||||
(Path::new("node_modules/c"), false),
|
||||
(Path::new("node_modules/c/c.js"), false),
|
||||
(Path::new("node_modules/d"), false),
|
||||
(Path::new("node_modules/d/d.js"), false),
|
||||
// This subdirectory is now ignored
|
||||
(Path::new("node_modules/d/e"), true),
|
||||
(Path::new("node_modules/d/f"), false),
|
||||
(Path::new("node_modules/d/f/f1.js"), false),
|
||||
(Path::new("node_modules/d/f/f2.js"), false),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Each of the newly-loaded directories is scanned only once.
|
||||
let read_dir_count_3 = fs.read_dir_call_count();
|
||||
assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
|
||||
"tree": {
|
||||
".git": {},
|
||||
".gitignore": "ignored-dir\n",
|
||||
"tracked-dir": {
|
||||
"tracked-file1": "",
|
||||
"ancestor-ignored-file1": "",
|
||||
},
|
||||
"ignored-dir": {
|
||||
"ignored-file1": ""
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let tree = Worktree::local(
|
||||
build_client(cx),
|
||||
"/root/tree".as_ref(),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
|
@ -290,7 +737,15 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
|||
.unwrap();
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
tree.as_local()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
assert!(
|
||||
|
@ -311,10 +766,26 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
|||
);
|
||||
});
|
||||
|
||||
std::fs::write(dir.join("tracked-dir/tracked-file2"), "").unwrap();
|
||||
std::fs::write(dir.join("tracked-dir/ancestor-ignored-file2"), "").unwrap();
|
||||
std::fs::write(dir.join("ignored-dir/ignored-file2"), "").unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
fs.create_file(
|
||||
"/root/tree/tracked-dir/tracked-file2".as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
fs.create_file(
|
||||
"/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
fs.create_file(
|
||||
"/root/tree/ignored-dir/ignored-file2".as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
assert!(
|
||||
|
@ -346,10 +817,8 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
|||
"ignored-dir": {}
|
||||
}));
|
||||
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
build_client(cx),
|
||||
dir.path(),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
|
@ -393,8 +862,6 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
|||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
|
@ -407,7 +874,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
|||
.await;
|
||||
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
build_client(cx),
|
||||
"/root".as_ref(),
|
||||
true,
|
||||
fs,
|
||||
|
@ -472,9 +939,8 @@ async fn test_random_worktree_operations_during_initial_scan(
|
|||
}
|
||||
log::info!("generated initial tree");
|
||||
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let worktree = Worktree::local(
|
||||
client.clone(),
|
||||
build_client(cx),
|
||||
root_dir,
|
||||
true,
|
||||
fs.clone(),
|
||||
|
@ -506,7 +972,7 @@ async fn test_random_worktree_operations_during_initial_scan(
|
|||
.await
|
||||
.log_err();
|
||||
worktree.read_with(cx, |tree, _| {
|
||||
tree.as_local().unwrap().snapshot().check_invariants()
|
||||
tree.as_local().unwrap().snapshot().check_invariants(true)
|
||||
});
|
||||
|
||||
if rng.gen_bool(0.6) {
|
||||
|
@ -523,7 +989,7 @@ async fn test_random_worktree_operations_during_initial_scan(
|
|||
let final_snapshot = worktree.read_with(cx, |tree, _| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
let snapshot = tree.snapshot();
|
||||
snapshot.check_invariants();
|
||||
snapshot.check_invariants(true);
|
||||
snapshot
|
||||
});
|
||||
|
||||
|
@ -562,9 +1028,8 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng)
|
|||
}
|
||||
log::info!("generated initial tree");
|
||||
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let worktree = Worktree::local(
|
||||
client.clone(),
|
||||
build_client(cx),
|
||||
root_dir,
|
||||
true,
|
||||
fs.clone(),
|
||||
|
@ -627,12 +1092,17 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng)
|
|||
log::info!("quiescing");
|
||||
fs.as_fake().flush_events(usize::MAX);
|
||||
cx.foreground().run_until_parked();
|
||||
|
||||
let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
|
||||
snapshot.check_invariants();
|
||||
snapshot.check_invariants(true);
|
||||
let expanded_paths = snapshot
|
||||
.expanded_entries()
|
||||
.map(|e| e.path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
{
|
||||
let new_worktree = Worktree::local(
|
||||
client.clone(),
|
||||
build_client(cx),
|
||||
root_dir,
|
||||
true,
|
||||
fs.clone(),
|
||||
|
@ -644,6 +1114,14 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng)
|
|||
new_worktree
|
||||
.update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete())
|
||||
.await;
|
||||
new_worktree
|
||||
.update(cx, |tree, _| {
|
||||
tree.as_local_mut()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(expanded_paths)
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
let new_snapshot =
|
||||
new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
|
||||
assert_eq!(
|
||||
|
@ -660,11 +1138,25 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng)
|
|||
}
|
||||
|
||||
assert_eq!(
|
||||
prev_snapshot.entries(true).collect::<Vec<_>>(),
|
||||
snapshot.entries(true).collect::<Vec<_>>(),
|
||||
prev_snapshot
|
||||
.entries(true)
|
||||
.map(ignore_pending_dir)
|
||||
.collect::<Vec<_>>(),
|
||||
snapshot
|
||||
.entries(true)
|
||||
.map(ignore_pending_dir)
|
||||
.collect::<Vec<_>>(),
|
||||
"wrong updates after snapshot {i}: {updates:#?}",
|
||||
);
|
||||
}
|
||||
|
||||
fn ignore_pending_dir(entry: &Entry) -> Entry {
|
||||
let mut entry = entry.clone();
|
||||
if entry.kind.is_dir() {
|
||||
entry.kind = EntryKind::Dir
|
||||
}
|
||||
entry
|
||||
}
|
||||
}
|
||||
|
||||
// The worktree's `UpdatedEntries` event can be used to follow along with
|
||||
|
@ -679,7 +1171,6 @@ fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Workt
|
|||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
match change_type {
|
||||
PathChange::Loaded => entries.insert(ix, entry.unwrap()),
|
||||
PathChange::Added => entries.insert(ix, entry.unwrap()),
|
||||
PathChange::Removed => drop(entries.remove(ix)),
|
||||
PathChange::Updated => {
|
||||
|
@ -688,7 +1179,7 @@ fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext<Workt
|
|||
assert_eq!(existing_entry.path, entry.path);
|
||||
*existing_entry = entry;
|
||||
}
|
||||
PathChange::AddedOrUpdated => {
|
||||
PathChange::AddedOrUpdated | PathChange::Loaded => {
|
||||
let entry = entry.unwrap();
|
||||
if entries.get(ix).map(|e| &e.path) == Some(&entry.path) {
|
||||
*entries.get_mut(ix).unwrap() = entry;
|
||||
|
@ -947,10 +1438,8 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
|||
}));
|
||||
let root_path = root.path();
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
build_client(cx),
|
||||
root_path,
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
|
@ -1026,10 +1515,8 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
|||
},
|
||||
}));
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
build_client(cx),
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
|
@ -1150,10 +1637,8 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
|
|||
|
||||
}));
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
build_client(cx),
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
|
@ -1357,10 +1842,8 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
|||
],
|
||||
);
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
build_client(cx),
|
||||
Path::new("/root"),
|
||||
true,
|
||||
fs.clone(),
|
||||
|
@ -1439,6 +1922,11 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
|||
}
|
||||
}
|
||||
|
||||
fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
cx.read(|cx| Client::new(http_client, cx))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_init(path: &Path) -> git2::Repository {
|
||||
git2::Repository::init(path).expect("Failed to initialize git repository")
|
||||
|
|
|
@ -153,6 +153,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
OpenedEntry {
|
||||
entry_id: ProjectEntryId,
|
||||
|
@ -410,17 +411,23 @@ impl ProjectPanel {
|
|||
fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
|
||||
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||
if entry.is_dir() {
|
||||
let worktree_id = worktree.id();
|
||||
let entry_id = entry.id;
|
||||
let expanded_dir_ids =
|
||||
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) {
|
||||
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
|
||||
expanded_dir_ids
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
match expanded_dir_ids.binary_search(&entry.id) {
|
||||
match expanded_dir_ids.binary_search(&entry_id) {
|
||||
Ok(_) => self.select_next(&SelectNext, cx),
|
||||
Err(ix) => {
|
||||
expanded_dir_ids.insert(ix, entry.id);
|
||||
self.project.update(cx, |project, cx| {
|
||||
project.expand_entry(worktree_id, entry_id, cx);
|
||||
});
|
||||
|
||||
expanded_dir_ids.insert(ix, entry_id);
|
||||
self.update_visible_entries(None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -431,18 +438,20 @@ impl ProjectPanel {
|
|||
|
||||
fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
|
||||
if let Some((worktree, mut entry)) = self.selected_entry(cx) {
|
||||
let worktree_id = worktree.id();
|
||||
let expanded_dir_ids =
|
||||
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) {
|
||||
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
|
||||
expanded_dir_ids
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
loop {
|
||||
match expanded_dir_ids.binary_search(&entry.id) {
|
||||
let entry_id = entry.id;
|
||||
match expanded_dir_ids.binary_search(&entry_id) {
|
||||
Ok(ix) => {
|
||||
expanded_dir_ids.remove(ix);
|
||||
self.update_visible_entries(Some((worktree.id(), entry.id)), cx);
|
||||
self.update_visible_entries(Some((worktree_id, entry_id)), cx);
|
||||
cx.notify();
|
||||
break;
|
||||
}
|
||||
|
@ -463,14 +472,17 @@ impl ProjectPanel {
|
|||
fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
|
||||
if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
|
||||
match expanded_dir_ids.binary_search(&entry_id) {
|
||||
Ok(ix) => {
|
||||
expanded_dir_ids.remove(ix);
|
||||
self.project.update(cx, |project, cx| {
|
||||
match expanded_dir_ids.binary_search(&entry_id) {
|
||||
Ok(ix) => {
|
||||
expanded_dir_ids.remove(ix);
|
||||
}
|
||||
Err(ix) => {
|
||||
project.expand_entry(worktree_id, entry_id, cx);
|
||||
expanded_dir_ids.insert(ix, entry_id);
|
||||
}
|
||||
}
|
||||
Err(ix) => {
|
||||
expanded_dir_ids.insert(ix, entry_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.update_visible_entries(Some((worktree_id, entry_id)), cx);
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
|
@ -938,10 +950,19 @@ impl ProjectPanel {
|
|||
}
|
||||
|
||||
fn selected_entry<'a>(&self, cx: &'a AppContext) -> Option<(&'a Worktree, &'a project::Entry)> {
|
||||
let (worktree, entry) = self.selected_entry_handle(cx)?;
|
||||
Some((worktree.read(cx), entry))
|
||||
}
|
||||
|
||||
fn selected_entry_handle<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<(ModelHandle<Worktree>, &'a project::Entry)> {
|
||||
let selection = self.selection?;
|
||||
let project = self.project.read(cx);
|
||||
let worktree = project.worktree_for_id(selection.worktree_id, cx)?.read(cx);
|
||||
Some((worktree, worktree.entry_for_id(selection.entry_id)?))
|
||||
let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
|
||||
let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
|
||||
Some((worktree, entry))
|
||||
}
|
||||
|
||||
fn update_visible_entries(
|
||||
|
@ -1002,6 +1023,7 @@ impl ProjectPanel {
|
|||
mtime: entry.mtime,
|
||||
is_symlink: false,
|
||||
is_ignored: false,
|
||||
is_external: false,
|
||||
git_status: entry.git_status,
|
||||
});
|
||||
}
|
||||
|
@ -1058,29 +1080,31 @@ impl ProjectPanel {
|
|||
entry_id: ProjectEntryId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let project = self.project.read(cx);
|
||||
if let Some((worktree, expanded_dir_ids)) = project
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.zip(self.expanded_dir_ids.get_mut(&worktree_id))
|
||||
{
|
||||
let worktree = worktree.read(cx);
|
||||
self.project.update(cx, |project, cx| {
|
||||
if let Some((worktree, expanded_dir_ids)) = project
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.zip(self.expanded_dir_ids.get_mut(&worktree_id))
|
||||
{
|
||||
project.expand_entry(worktree_id, entry_id, cx);
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
if let Some(mut entry) = worktree.entry_for_id(entry_id) {
|
||||
loop {
|
||||
if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
|
||||
expanded_dir_ids.insert(ix, entry.id);
|
||||
}
|
||||
if let Some(mut entry) = worktree.entry_for_id(entry_id) {
|
||||
loop {
|
||||
if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
|
||||
expanded_dir_ids.insert(ix, entry.id);
|
||||
}
|
||||
|
||||
if let Some(parent_entry) =
|
||||
entry.path.parent().and_then(|p| worktree.entry_for_path(p))
|
||||
{
|
||||
entry = parent_entry;
|
||||
} else {
|
||||
break;
|
||||
if let Some(parent_entry) =
|
||||
entry.path.parent().and_then(|p| worktree.entry_for_path(p))
|
||||
{
|
||||
entry = parent_entry;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn for_each_visible_entry(
|
||||
|
@ -1190,7 +1214,7 @@ impl ProjectPanel {
|
|||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
if kind == EntryKind::Dir {
|
||||
if kind.is_dir() {
|
||||
if details.is_expanded {
|
||||
Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color)
|
||||
} else {
|
||||
|
@ -1253,7 +1277,10 @@ impl ProjectPanel {
|
|||
let show_editor = details.is_editing && !details.is_processing;
|
||||
|
||||
MouseEventHandler::<Self, _>::new(entry_id.to_usize(), cx, |state, cx| {
|
||||
let mut style = entry_style.style_for(state, details.is_selected).clone();
|
||||
let mut style = entry_style
|
||||
.in_state(details.is_selected)
|
||||
.style_for(state)
|
||||
.clone();
|
||||
|
||||
if cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
|
@ -1264,7 +1291,7 @@ impl ProjectPanel {
|
|||
.filter(|destination| details.path.starts_with(destination))
|
||||
.is_some()
|
||||
{
|
||||
style = entry_style.active.clone().unwrap();
|
||||
style = entry_style.active_state().default.clone();
|
||||
}
|
||||
|
||||
let row_container_style = if show_editor {
|
||||
|
@ -1284,7 +1311,7 @@ impl ProjectPanel {
|
|||
})
|
||||
.on_click(MouseButton::Left, move |event, this, cx| {
|
||||
if !show_editor {
|
||||
if kind == EntryKind::Dir {
|
||||
if kind.is_dir() {
|
||||
this.toggle_expanded(entry_id, cx);
|
||||
} else {
|
||||
this.open_entry(entry_id, event.click_count > 1, cx);
|
||||
|
@ -1405,9 +1432,11 @@ impl View for ProjectPanel {
|
|||
let button_style = theme.open_project_button.clone();
|
||||
let context_menu_item_style = theme::current(cx).context_menu.item.clone();
|
||||
move |state, cx| {
|
||||
let button_style = button_style.style_for(state, false).clone();
|
||||
let context_menu_item =
|
||||
context_menu_item_style.style_for(state, true).clone();
|
||||
let button_style = button_style.style_for(state).clone();
|
||||
let context_menu_item = context_menu_item_style
|
||||
.active_state()
|
||||
.style_for(state)
|
||||
.clone();
|
||||
|
||||
theme::ui::keystroke_label(
|
||||
"Open a project",
|
||||
|
@ -2343,7 +2372,7 @@ mod tests {
|
|||
}
|
||||
|
||||
let indent = " ".repeat(details.depth);
|
||||
let icon = if matches!(details.kind, EntryKind::Dir | EntryKind::PendingDir) {
|
||||
let icon = if details.kind.is_dir() {
|
||||
if details.is_expanded {
|
||||
"v "
|
||||
} else {
|
||||
|
|
|
@ -196,7 +196,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
|
|||
) -> AnyElement<Picker<Self>> {
|
||||
let theme = theme::current(cx);
|
||||
let style = &theme.picker.item;
|
||||
let current_style = style.style_for(mouse_state, selected);
|
||||
let current_style = style.in_state(selected).style_for(mouse_state);
|
||||
|
||||
let string_match = &self.matches[ix];
|
||||
let symbol = &self.symbols[string_match.candidate_id];
|
||||
|
@ -229,7 +229,10 @@ impl PickerDelegate for ProjectSymbolsDelegate {
|
|||
.with_child(
|
||||
// Avoid styling the path differently when it is selected, since
|
||||
// the symbol's syntax highlighting doesn't change when selected.
|
||||
Label::new(path.to_string(), style.default.label.clone()),
|
||||
Label::new(
|
||||
path.to_string(),
|
||||
style.inactive_state().default.label.clone(),
|
||||
),
|
||||
)
|
||||
.contained()
|
||||
.with_style(current_style.container)
|
||||
|
|
|
@ -173,7 +173,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
||||
|
||||
let string_match = &self.matches[ix];
|
||||
|
||||
|
|
|
@ -63,6 +63,8 @@ message Envelope {
|
|||
CopyProjectEntry copy_project_entry = 47;
|
||||
DeleteProjectEntry delete_project_entry = 48;
|
||||
ProjectEntryResponse project_entry_response = 49;
|
||||
ExpandProjectEntry expand_project_entry = 114;
|
||||
ExpandProjectEntryResponse expand_project_entry_response = 115;
|
||||
|
||||
UpdateDiagnosticSummary update_diagnostic_summary = 50;
|
||||
StartLanguageServer start_language_server = 51;
|
||||
|
@ -372,6 +374,15 @@ message DeleteProjectEntry {
|
|||
uint64 entry_id = 2;
|
||||
}
|
||||
|
||||
message ExpandProjectEntry {
|
||||
uint64 project_id = 1;
|
||||
uint64 entry_id = 2;
|
||||
}
|
||||
|
||||
message ExpandProjectEntryResponse {
|
||||
uint64 worktree_scan_id = 1;
|
||||
}
|
||||
|
||||
message ProjectEntryResponse {
|
||||
Entry entry = 1;
|
||||
uint64 worktree_scan_id = 2;
|
||||
|
@ -1005,7 +1016,8 @@ message Entry {
|
|||
Timestamp mtime = 5;
|
||||
bool is_symlink = 6;
|
||||
bool is_ignored = 7;
|
||||
optional GitStatus git_status = 8;
|
||||
bool is_external = 8;
|
||||
optional GitStatus git_status = 9;
|
||||
}
|
||||
|
||||
message RepositoryEntry {
|
||||
|
|
|
@ -150,6 +150,7 @@ messages!(
|
|||
(DeclineCall, Foreground),
|
||||
(DeleteProjectEntry, Foreground),
|
||||
(Error, Foreground),
|
||||
(ExpandProjectEntry, Foreground),
|
||||
(Follow, Foreground),
|
||||
(FollowResponse, Foreground),
|
||||
(FormatBuffers, Foreground),
|
||||
|
@ -200,6 +201,7 @@ messages!(
|
|||
(Ping, Foreground),
|
||||
(PrepareRename, Background),
|
||||
(PrepareRenameResponse, Background),
|
||||
(ExpandProjectEntryResponse, Foreground),
|
||||
(ProjectEntryResponse, Foreground),
|
||||
(RejoinRoom, Foreground),
|
||||
(RejoinRoomResponse, Foreground),
|
||||
|
@ -255,6 +257,7 @@ request_messages!(
|
|||
(CreateRoom, CreateRoomResponse),
|
||||
(DeclineCall, Ack),
|
||||
(DeleteProjectEntry, ProjectEntryResponse),
|
||||
(ExpandProjectEntry, ExpandProjectEntryResponse),
|
||||
(Follow, FollowResponse),
|
||||
(FormatBuffers, FormatBuffersResponse),
|
||||
(GetChannelMessages, GetChannelMessagesResponse),
|
||||
|
@ -311,6 +314,7 @@ entity_messages!(
|
|||
CreateBufferForPeer,
|
||||
CreateProjectEntry,
|
||||
DeleteProjectEntry,
|
||||
ExpandProjectEntry,
|
||||
Follow,
|
||||
FormatBuffers,
|
||||
GetCodeActions,
|
||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
|||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 58;
|
||||
pub const PROTOCOL_VERSION: u32 = 59;
|
||||
|
|
|
@ -259,7 +259,11 @@ impl BufferSearchBar {
|
|||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
|
||||
pub fn is_dismissed(&self) -> bool {
|
||||
self.dismissed
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
|
||||
self.dismissed = true;
|
||||
for searchable_item in self.seachable_items_with_matches.keys() {
|
||||
if let Some(searchable_item) =
|
||||
|
@ -275,7 +279,7 @@ impl BufferSearchBar {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
|
||||
pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
|
||||
let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
|
||||
SearchableItemHandle::boxed_clone(searchable_item.as_ref())
|
||||
} else {
|
||||
|
@ -328,7 +332,11 @@ impl BufferSearchBar {
|
|||
Some(
|
||||
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.search.option_button.style_for(state, is_active);
|
||||
let style = theme
|
||||
.search
|
||||
.option_button
|
||||
.in_state(is_active)
|
||||
.style_for(state);
|
||||
Label::new(icon, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
@ -371,7 +379,7 @@ impl BufferSearchBar {
|
|||
enum NavButton {}
|
||||
MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.search.option_button.style_for(state, false);
|
||||
let style = theme.search.option_button.inactive_state().style_for(state);
|
||||
Label::new(icon, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
@ -403,7 +411,7 @@ impl BufferSearchBar {
|
|||
|
||||
enum CloseButton {}
|
||||
MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
let style = theme.dismiss_button.style_for(state);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
|
@ -480,7 +488,7 @@ impl BufferSearchBar {
|
|||
self.select_match(Direction::Prev, cx);
|
||||
}
|
||||
|
||||
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||
pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = self.active_match_index {
|
||||
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
|
||||
if let Some(matches) = self
|
||||
|
|
|
@ -896,7 +896,7 @@ impl ProjectSearchBar {
|
|||
enum NavButton {}
|
||||
MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.search.option_button.style_for(state, false);
|
||||
let style = theme.search.option_button.inactive_state().style_for(state);
|
||||
Label::new(icon, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
@ -927,7 +927,11 @@ impl ProjectSearchBar {
|
|||
let is_active = self.is_option_enabled(option, cx);
|
||||
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.search.option_button.style_for(state, is_active);
|
||||
let style = theme
|
||||
.search
|
||||
.option_button
|
||||
.in_state(is_active)
|
||||
.style_for(state);
|
||||
Label::new(icon, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
|
|
|
@ -25,6 +25,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
cx.add_action(TerminalPanel::new_terminal);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event {
|
||||
Close,
|
||||
DockPositionChanged,
|
||||
|
|
|
@ -4,15 +4,16 @@ pub mod ui;
|
|||
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, TooltipStyle},
|
||||
elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
|
||||
fonts::{HighlightStyle, TextStyle},
|
||||
platform, AppContext, AssetSource, Border, MouseState,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde_json::Value;
|
||||
use settings::SettingsStore;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle, SvgStyle};
|
||||
use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle};
|
||||
|
||||
pub use theme_registry::*;
|
||||
pub use theme_settings::*;
|
||||
|
@ -36,7 +37,7 @@ pub fn init(source: impl AssetSource, cx: &mut AppContext) {
|
|||
.detach();
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct Theme {
|
||||
#[serde(default)]
|
||||
pub meta: ThemeMeta,
|
||||
|
@ -65,9 +66,10 @@ pub struct Theme {
|
|||
pub feedback: FeedbackStyle,
|
||||
pub welcome: WelcomeStyle,
|
||||
pub color_scheme: ColorScheme,
|
||||
pub titlebar: Titlebar,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
pub struct ThemeMeta {
|
||||
#[serde(skip_deserializing)]
|
||||
pub id: usize,
|
||||
|
@ -75,11 +77,10 @@ pub struct ThemeMeta {
|
|||
pub is_light: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct Workspace {
|
||||
pub background: Color,
|
||||
pub blank_pane: BlankPaneStyle,
|
||||
pub titlebar: Titlebar,
|
||||
pub tab_bar: TabBar,
|
||||
pub pane_divider: Border,
|
||||
pub leader_border_opacity: f32,
|
||||
|
@ -102,7 +103,7 @@ pub struct Workspace {
|
|||
pub drop_target_overlay_color: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct BlankPaneStyle {
|
||||
pub logo: SvgStyle,
|
||||
pub logo_shadow: SvgStyle,
|
||||
|
@ -112,7 +113,7 @@ pub struct BlankPaneStyle {
|
|||
pub keyboard_hint_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Titlebar {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -128,16 +129,34 @@ pub struct Titlebar {
|
|||
pub leader_avatar: AvatarStyle,
|
||||
pub follower_avatar: AvatarStyle,
|
||||
pub inactive_avatar_grayscale: bool,
|
||||
pub sign_in_prompt: Interactive<ContainedText>,
|
||||
pub sign_in_button: Toggleable<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 share_button: Toggleable<Interactive<ContainedText>>,
|
||||
pub muted: Color,
|
||||
pub speaking: Color,
|
||||
pub screen_share_button: Toggleable<Interactive<IconButton>>,
|
||||
pub toggle_contacts_button: Toggleable<Interactive<IconButton>>,
|
||||
pub toggle_microphone_button: Toggleable<Interactive<IconButton>>,
|
||||
pub toggle_speakers_button: Toggleable<Interactive<IconButton>>,
|
||||
pub leave_call_button: Interactive<IconButton>,
|
||||
pub toggle_contacts_badge: ContainerStyle,
|
||||
pub user_menu: UserMenu,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct UserMenu {
|
||||
pub user_menu_button_online: UserMenuButton,
|
||||
pub user_menu_button_offline: UserMenuButton,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct UserMenuButton {
|
||||
pub user_menu: Toggleable<Interactive<Icon>>,
|
||||
pub avatar: AvatarStyle,
|
||||
pub icon: Icon,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct AvatarStyle {
|
||||
#[serde(flatten)]
|
||||
pub image: ImageStyle,
|
||||
|
@ -145,14 +164,14 @@ pub struct AvatarStyle {
|
|||
pub outer_corner_radius: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
pub struct Copilot {
|
||||
pub out_link_icon: Interactive<IconStyle>,
|
||||
pub modal: ModalStyle,
|
||||
pub auth: CopilotAuth,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
pub struct CopilotAuth {
|
||||
pub content_width: f32,
|
||||
pub prompting: CopilotAuthPrompting,
|
||||
|
@ -162,14 +181,14 @@ pub struct CopilotAuth {
|
|||
pub header: IconStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
pub struct CopilotAuthPrompting {
|
||||
pub subheading: ContainedText,
|
||||
pub hint: ContainedText,
|
||||
pub device_code: DeviceCode,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
pub struct DeviceCode {
|
||||
pub text: TextStyle,
|
||||
pub cta: ButtonStyle,
|
||||
|
@ -179,19 +198,19 @@ pub struct DeviceCode {
|
|||
pub right_container: Interactive<ContainerStyle>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
pub struct CopilotAuthNotAuthorized {
|
||||
pub subheading: ContainedText,
|
||||
pub warning: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
pub struct CopilotAuthAuthorized {
|
||||
pub subheading: ContainedText,
|
||||
pub hint: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ContactsPopover {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -199,17 +218,17 @@ pub struct ContactsPopover {
|
|||
pub width: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ContactList {
|
||||
pub user_query_editor: FieldEditor,
|
||||
pub user_query_editor_height: f32,
|
||||
pub add_contact_button: IconButton,
|
||||
pub header_row: Interactive<ContainedText>,
|
||||
pub header_row: Toggleable<Interactive<ContainedText>>,
|
||||
pub leave_call: Interactive<ContainedText>,
|
||||
pub contact_row: Interactive<ContainerStyle>,
|
||||
pub contact_row: Toggleable<Interactive<ContainerStyle>>,
|
||||
pub row_height: f32,
|
||||
pub project_row: Interactive<ProjectRow>,
|
||||
pub tree_branch: Interactive<TreeBranch>,
|
||||
pub project_row: Toggleable<Interactive<ProjectRow>>,
|
||||
pub tree_branch: Toggleable<Interactive<TreeBranch>>,
|
||||
pub contact_avatar: ImageStyle,
|
||||
pub contact_status_free: ContainerStyle,
|
||||
pub contact_status_busy: ContainerStyle,
|
||||
|
@ -221,7 +240,7 @@ pub struct ContactList {
|
|||
pub calling_indicator: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ProjectRow {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -229,13 +248,13 @@ pub struct ProjectRow {
|
|||
pub name: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Clone, Copy)]
|
||||
#[derive(Deserialize, Default, Clone, Copy, JsonSchema)]
|
||||
pub struct TreeBranch {
|
||||
pub width: f32,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ContactFinder {
|
||||
pub picker: Picker,
|
||||
pub row_height: f32,
|
||||
|
@ -245,17 +264,17 @@ pub struct ContactFinder {
|
|||
pub disabled_contact_button: IconButton,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct DropdownMenu {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub header: Interactive<DropdownMenuItem>,
|
||||
pub section_header: ContainedText,
|
||||
pub item: Interactive<DropdownMenuItem>,
|
||||
pub item: Toggleable<Interactive<DropdownMenuItem>>,
|
||||
pub row_height: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct DropdownMenuItem {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -266,11 +285,11 @@ pub struct DropdownMenuItem {
|
|||
pub secondary_text_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct TabBar {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub pane_button: Interactive<IconButton>,
|
||||
pub pane_button: Toggleable<Interactive<IconButton>>,
|
||||
pub pane_button_container: ContainerStyle,
|
||||
pub active_pane: TabStyles,
|
||||
pub inactive_pane: TabStyles,
|
||||
|
@ -294,13 +313,13 @@ impl TabBar {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct TabStyles {
|
||||
pub active_tab: Tab,
|
||||
pub inactive_tab: Tab,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct AvatarRibbon {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -308,7 +327,7 @@ pub struct AvatarRibbon {
|
|||
pub height: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct OfflineIcon {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -316,7 +335,7 @@ pub struct OfflineIcon {
|
|||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Tab {
|
||||
pub height: f32,
|
||||
#[serde(flatten)]
|
||||
|
@ -333,7 +352,7 @@ pub struct Tab {
|
|||
pub icon_conflict: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Toolbar {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -342,14 +361,14 @@ pub struct Toolbar {
|
|||
pub nav_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Notifications {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Search {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -359,14 +378,14 @@ pub struct Search {
|
|||
pub include_exclude_editor: FindEditor,
|
||||
pub invalid_include_exclude_editor: ContainerStyle,
|
||||
pub include_exclude_inputs: ContainedText,
|
||||
pub option_button: Interactive<ContainedText>,
|
||||
pub option_button: Toggleable<Interactive<ContainedText>>,
|
||||
pub match_background: Color,
|
||||
pub match_index: ContainedText,
|
||||
pub results_status: TextStyle,
|
||||
pub dismiss_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct FindEditor {
|
||||
#[serde(flatten)]
|
||||
pub input: FieldEditor,
|
||||
|
@ -374,7 +393,7 @@ pub struct FindEditor {
|
|||
pub max_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct StatusBar {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -390,15 +409,15 @@ pub struct StatusBar {
|
|||
pub diagnostic_message: Interactive<ContainedText>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct StatusBarPanelButtons {
|
||||
pub group_left: ContainerStyle,
|
||||
pub group_bottom: ContainerStyle,
|
||||
pub group_right: ContainerStyle,
|
||||
pub button: Interactive<PanelButton>,
|
||||
pub button: Toggleable<Interactive<PanelButton>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct StatusBarDiagnosticSummary {
|
||||
pub container_ok: ContainerStyle,
|
||||
pub container_warning: ContainerStyle,
|
||||
|
@ -413,7 +432,7 @@ pub struct StatusBarDiagnosticSummary {
|
|||
pub summary_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct StatusBarLspStatus {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -424,14 +443,14 @@ pub struct StatusBarLspStatus {
|
|||
pub message: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct Dock {
|
||||
pub left: ContainerStyle,
|
||||
pub bottom: ContainerStyle,
|
||||
pub right: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct PanelButton {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -440,20 +459,20 @@ pub struct PanelButton {
|
|||
pub label: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ProjectPanel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub entry: Interactive<ProjectPanelEntry>,
|
||||
pub entry: Toggleable<Interactive<ProjectPanelEntry>>,
|
||||
pub dragged_entry: ProjectPanelEntry,
|
||||
pub ignored_entry: Interactive<ProjectPanelEntry>,
|
||||
pub cut_entry: Interactive<ProjectPanelEntry>,
|
||||
pub ignored_entry: Toggleable<Interactive<ProjectPanelEntry>>,
|
||||
pub cut_entry: Toggleable<Interactive<ProjectPanelEntry>>,
|
||||
pub filename_editor: FieldEditor,
|
||||
pub indent_width: f32,
|
||||
pub open_project_button: Interactive<ContainedText>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
||||
pub struct ProjectPanelEntry {
|
||||
pub height: f32,
|
||||
#[serde(flatten)]
|
||||
|
@ -465,28 +484,28 @@ pub struct ProjectPanelEntry {
|
|||
pub status: EntryStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
||||
pub struct EntryStatus {
|
||||
pub git: GitProjectStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
||||
pub struct GitProjectStatus {
|
||||
pub modified: Color,
|
||||
pub inserted: Color,
|
||||
pub conflict: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
||||
pub struct ContextMenu {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub item: Interactive<ContextMenuItem>,
|
||||
pub item: Toggleable<Interactive<ContextMenuItem>>,
|
||||
pub keystroke_margin: f32,
|
||||
pub separator: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
||||
pub struct ContextMenuItem {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -496,13 +515,13 @@ pub struct ContextMenuItem {
|
|||
pub icon_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[derive(Debug, Deserialize, Default, JsonSchema)]
|
||||
pub struct CommandPalette {
|
||||
pub key: Interactive<ContainedLabel>,
|
||||
pub key: Toggleable<ContainedLabel>,
|
||||
pub keystroke_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct InviteLink {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -511,7 +530,7 @@ pub struct InviteLink {
|
|||
pub icon: Icon,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Copy, Default)]
|
||||
#[derive(Deserialize, Clone, Copy, Default, JsonSchema)]
|
||||
pub struct Icon {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -519,7 +538,7 @@ pub struct Icon {
|
|||
pub width: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Copy, Default)]
|
||||
#[derive(Deserialize, Clone, Copy, Default, JsonSchema)]
|
||||
pub struct IconButton {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -528,7 +547,7 @@ pub struct IconButton {
|
|||
pub button_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ChatMessage {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -537,7 +556,7 @@ pub struct ChatMessage {
|
|||
pub timestamp: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ChannelSelect {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -549,7 +568,7 @@ pub struct ChannelSelect {
|
|||
pub menu: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ChannelName {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -557,7 +576,7 @@ pub struct ChannelName {
|
|||
pub name: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Picker {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -565,10 +584,10 @@ pub struct Picker {
|
|||
pub input_editor: FieldEditor,
|
||||
pub empty_input_editor: FieldEditor,
|
||||
pub no_matches: ContainedLabel,
|
||||
pub item: Interactive<ContainedLabel>,
|
||||
pub item: Toggleable<Interactive<ContainedLabel>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
||||
pub struct ContainedText {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -576,7 +595,7 @@ pub struct ContainedText {
|
|||
pub text: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
||||
pub struct ContainedLabel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -584,7 +603,7 @@ pub struct ContainedLabel {
|
|||
pub label: LabelStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct ProjectDiagnostics {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -594,7 +613,7 @@ pub struct ProjectDiagnostics {
|
|||
pub tab_summary_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ContactNotification {
|
||||
pub header_avatar: ImageStyle,
|
||||
pub header_message: ContainedText,
|
||||
|
@ -604,21 +623,21 @@ pub struct ContactNotification {
|
|||
pub dismiss_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct UpdateNotification {
|
||||
pub message: ContainedText,
|
||||
pub action_message: Interactive<ContainedText>,
|
||||
pub dismiss_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct MessageNotification {
|
||||
pub message: ContainedText,
|
||||
pub action_message: Interactive<ContainedText>,
|
||||
pub dismiss_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct ProjectSharedNotification {
|
||||
pub window_height: f32,
|
||||
pub window_width: f32,
|
||||
|
@ -635,7 +654,7 @@ pub struct ProjectSharedNotification {
|
|||
pub dismiss_button: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, JsonSchema)]
|
||||
pub struct IncomingCallNotification {
|
||||
pub window_height: f32,
|
||||
pub window_width: f32,
|
||||
|
@ -652,7 +671,7 @@ pub struct IncomingCallNotification {
|
|||
pub decline_button: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Editor {
|
||||
pub text_color: Color,
|
||||
#[serde(default)]
|
||||
|
@ -693,7 +712,7 @@ pub struct Editor {
|
|||
pub whitespace: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Scrollbar {
|
||||
pub track: ContainerStyle,
|
||||
pub thumb: ContainerStyle,
|
||||
|
@ -702,14 +721,14 @@ pub struct Scrollbar {
|
|||
pub git: GitDiffColors,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct GitDiffColors {
|
||||
pub inserted: Color,
|
||||
pub modified: Color,
|
||||
pub deleted: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct DiagnosticPathHeader {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -718,7 +737,7 @@ pub struct DiagnosticPathHeader {
|
|||
pub text_scale_factor: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct DiagnosticHeader {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -729,7 +748,7 @@ pub struct DiagnosticHeader {
|
|||
pub icon_width_factor: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct DiagnosticStyle {
|
||||
pub message: LabelStyle,
|
||||
#[serde(default)]
|
||||
|
@ -737,7 +756,7 @@ pub struct DiagnosticStyle {
|
|||
pub text_scale_factor: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct AutocompleteStyle {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -747,13 +766,13 @@ pub struct AutocompleteStyle {
|
|||
pub match_highlight: HighlightStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Deserialize)]
|
||||
#[derive(Clone, Copy, Default, Deserialize, JsonSchema)]
|
||||
pub struct SelectionStyle {
|
||||
pub cursor: Color,
|
||||
pub selection: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct FieldEditor {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
|
@ -763,21 +782,21 @@ pub struct FieldEditor {
|
|||
pub selection: SelectionStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct InteractiveColor {
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct CodeActions {
|
||||
#[serde(default)]
|
||||
pub indicator: Interactive<InteractiveColor>,
|
||||
pub indicator: Toggleable<Interactive<InteractiveColor>>,
|
||||
pub vertical_scale: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Folds {
|
||||
pub indicator: Interactive<InteractiveColor>,
|
||||
pub indicator: Toggleable<Interactive<InteractiveColor>>,
|
||||
pub ellipses: FoldEllipses,
|
||||
pub fold_background: Color,
|
||||
pub icon_margin_scale: f32,
|
||||
|
@ -785,14 +804,14 @@ pub struct Folds {
|
|||
pub foldable_icon: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct FoldEllipses {
|
||||
pub text_color: Color,
|
||||
pub background: Interactive<InteractiveColor>,
|
||||
pub corner_radius_factor: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct DiffStyle {
|
||||
pub inserted: Color,
|
||||
pub modified: Color,
|
||||
|
@ -802,41 +821,49 @@ pub struct DiffStyle {
|
|||
pub corner_radius: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
#[derive(Debug, Default, Clone, Copy, JsonSchema)]
|
||||
pub struct Interactive<T> {
|
||||
pub default: T,
|
||||
pub hover: Option<T>,
|
||||
pub hover_and_active: Option<T>,
|
||||
pub hovered: Option<T>,
|
||||
pub clicked: Option<T>,
|
||||
pub click_and_active: Option<T>,
|
||||
pub active: Option<T>,
|
||||
pub disabled: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> Interactive<T> {
|
||||
pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T {
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
|
||||
pub struct Toggleable<T> {
|
||||
active: T,
|
||||
inactive: T,
|
||||
}
|
||||
|
||||
impl<T> Toggleable<T> {
|
||||
pub fn new(active: T, inactive: T) -> Self {
|
||||
Self { active, inactive }
|
||||
}
|
||||
pub fn in_state(&self, active: bool) -> &T {
|
||||
if active {
|
||||
if state.hovered() {
|
||||
self.hover_and_active
|
||||
.as_ref()
|
||||
.unwrap_or(self.active.as_ref().unwrap_or(&self.default))
|
||||
} else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some()
|
||||
{
|
||||
self.click_and_active
|
||||
.as_ref()
|
||||
.unwrap_or(self.active.as_ref().unwrap_or(&self.default))
|
||||
} else {
|
||||
self.active.as_ref().unwrap_or(&self.default)
|
||||
}
|
||||
} else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
|
||||
&self.active
|
||||
} else {
|
||||
&self.inactive
|
||||
}
|
||||
}
|
||||
pub fn active_state(&self) -> &T {
|
||||
self.in_state(true)
|
||||
}
|
||||
pub fn inactive_state(&self) -> &T {
|
||||
self.in_state(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Interactive<T> {
|
||||
pub fn style_for(&self, state: &mut MouseState) -> &T {
|
||||
if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
|
||||
self.clicked.as_ref().unwrap()
|
||||
} else if state.hovered() {
|
||||
self.hover.as_ref().unwrap_or(&self.default)
|
||||
self.hovered.as_ref().unwrap_or(&self.default)
|
||||
} else {
|
||||
&self.default
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disabled_style(&self) -> &T {
|
||||
self.disabled.as_ref().unwrap_or(&self.default)
|
||||
}
|
||||
|
@ -849,13 +876,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
|||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Helper {
|
||||
#[serde(flatten)]
|
||||
default: Value,
|
||||
hover: Option<Value>,
|
||||
hover_and_active: Option<Value>,
|
||||
hovered: Option<Value>,
|
||||
clicked: Option<Value>,
|
||||
click_and_active: Option<Value>,
|
||||
active: Option<Value>,
|
||||
disabled: Option<Value>,
|
||||
}
|
||||
|
||||
|
@ -880,21 +903,15 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
|
|||
}
|
||||
};
|
||||
|
||||
let hover = deserialize_state(json.hover)?;
|
||||
let hover_and_active = deserialize_state(json.hover_and_active)?;
|
||||
let hovered = deserialize_state(json.hovered)?;
|
||||
let clicked = deserialize_state(json.clicked)?;
|
||||
let click_and_active = deserialize_state(json.click_and_active)?;
|
||||
let active = deserialize_state(json.active)?;
|
||||
let disabled = deserialize_state(json.disabled)?;
|
||||
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
|
||||
|
||||
Ok(Interactive {
|
||||
default,
|
||||
hover,
|
||||
hover_and_active,
|
||||
hovered,
|
||||
clicked,
|
||||
click_and_active,
|
||||
active,
|
||||
disabled,
|
||||
})
|
||||
}
|
||||
|
@ -911,7 +928,7 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, JsonSchema)]
|
||||
pub struct SyntaxTheme {
|
||||
pub highlights: Vec<(String, HighlightStyle)>,
|
||||
}
|
||||
|
@ -945,7 +962,7 @@ impl<'de> Deserialize<'de> for SyntaxTheme {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct HoverPopover {
|
||||
pub container: ContainerStyle,
|
||||
pub info_container: ContainerStyle,
|
||||
|
@ -957,7 +974,7 @@ pub struct HoverPopover {
|
|||
pub highlight: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct TerminalStyle {
|
||||
pub black: Color,
|
||||
pub red: Color,
|
||||
|
@ -991,24 +1008,39 @@ pub struct TerminalStyle {
|
|||
pub dim_foreground: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct AssistantStyle {
|
||||
pub container: ContainerStyle,
|
||||
pub header: ContainerStyle,
|
||||
pub hamburger_button: Interactive<IconStyle>,
|
||||
pub split_button: Interactive<IconStyle>,
|
||||
pub assist_button: Interactive<IconStyle>,
|
||||
pub quote_button: Interactive<IconStyle>,
|
||||
pub zoom_in_button: Interactive<IconStyle>,
|
||||
pub zoom_out_button: Interactive<IconStyle>,
|
||||
pub plus_button: Interactive<IconStyle>,
|
||||
pub title: ContainedText,
|
||||
pub message_header: ContainerStyle,
|
||||
pub sent_at: ContainedText,
|
||||
pub user_sender: Interactive<ContainedText>,
|
||||
pub assistant_sender: Interactive<ContainedText>,
|
||||
pub system_sender: Interactive<ContainedText>,
|
||||
pub model_info_container: ContainerStyle,
|
||||
pub model: Interactive<ContainedText>,
|
||||
pub remaining_tokens: ContainedText,
|
||||
pub no_remaining_tokens: ContainedText,
|
||||
pub error_icon: Icon,
|
||||
pub api_key_editor: FieldEditor,
|
||||
pub api_key_prompt: ContainedText,
|
||||
pub saved_conversation: SavedConversation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct SavedConversation {
|
||||
pub container: Interactive<ContainerStyle>,
|
||||
pub saved_at: ContainedText,
|
||||
pub title: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct FeedbackStyle {
|
||||
pub submit_button: Interactive<ContainedText>,
|
||||
pub button_margin: f32,
|
||||
|
@ -1017,7 +1049,7 @@ pub struct FeedbackStyle {
|
|||
pub link_text_hover: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct WelcomeStyle {
|
||||
pub page_width: f32,
|
||||
pub logo: SvgStyle,
|
||||
|
@ -1031,7 +1063,7 @@ pub struct WelcomeStyle {
|
|||
pub checkbox_group: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct ColorScheme {
|
||||
pub name: String,
|
||||
pub is_light: bool,
|
||||
|
@ -1046,13 +1078,13 @@ pub struct ColorScheme {
|
|||
pub players: Vec<Player>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Player {
|
||||
pub cursor: Color,
|
||||
pub selection: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct RampSet {
|
||||
pub neutral: Vec<Color>,
|
||||
pub red: Vec<Color>,
|
||||
|
@ -1065,7 +1097,7 @@ pub struct RampSet {
|
|||
pub magenta: Vec<Color>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Layer {
|
||||
pub base: StyleSet,
|
||||
pub variant: StyleSet,
|
||||
|
@ -1076,7 +1108,7 @@ pub struct Layer {
|
|||
pub negative: StyleSet,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct StyleSet {
|
||||
pub default: Style,
|
||||
pub active: Style,
|
||||
|
@ -1086,7 +1118,7 @@ pub struct StyleSet {
|
|||
pub inverted: Style,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct Style {
|
||||
pub background: Color,
|
||||
pub border: Color,
|
||||
|
|
|
@ -14,12 +14,13 @@ use util::ResultExt as _;
|
|||
|
||||
const MIN_FONT_SIZE: f32 = 6.0;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, JsonSchema)]
|
||||
pub struct ThemeSettings {
|
||||
pub buffer_font_family_name: String,
|
||||
pub buffer_font_features: fonts::Features,
|
||||
pub buffer_font_family: FamilyId,
|
||||
pub(crate) buffer_font_size: f32,
|
||||
#[serde(skip)]
|
||||
pub theme: Arc<Theme>,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::{
|
||||
ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
|
||||
MouseEventHandler, ParentElement, Stack, Svg,
|
||||
ConstrainedBox, Container, ContainerStyle, Dimensions, Empty, Flex, KeystrokeLabel, Label,
|
||||
MouseEventHandler, ParentElement, Stack, Svg, SvgStyle,
|
||||
},
|
||||
fonts::TextStyle,
|
||||
geometry::vector::{vec2f, Vector2F},
|
||||
geometry::vector::Vector2F,
|
||||
platform,
|
||||
platform::MouseButton,
|
||||
scene::MouseClick,
|
||||
Action, Element, EventContext, MouseState, View, ViewContext,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{ContainedText, Interactive};
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct CheckboxStyle {
|
||||
pub icon: SvgStyle,
|
||||
pub label: ContainedText,
|
||||
|
@ -93,25 +93,6 @@ where
|
|||
.with_cursor_style(platform::CursorStyle::PointingHand)
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct SvgStyle {
|
||||
pub color: Color,
|
||||
pub asset: String,
|
||||
pub dimensions: Dimensions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Dimensions {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
impl Dimensions {
|
||||
pub fn to_vec(&self) -> Vector2F {
|
||||
vec2f(self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
|
||||
Svg::new(style.asset.clone())
|
||||
.with_color(style.color)
|
||||
|
@ -120,10 +101,10 @@ pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
|
|||
.with_height(style.dimensions.height)
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct IconStyle {
|
||||
icon: SvgStyle,
|
||||
container: ContainerStyle,
|
||||
pub icon: SvgStyle,
|
||||
pub container: ContainerStyle,
|
||||
}
|
||||
|
||||
pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
|
||||
|
@ -170,7 +151,7 @@ where
|
|||
F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
|
||||
{
|
||||
MouseEventHandler::<Tag, V>::new(0, cx, |state, _| {
|
||||
let style = style.style_for(state, false);
|
||||
let style = style.style_for(state);
|
||||
Label::new(label, style.text.to_owned())
|
||||
.aligned()
|
||||
.contained()
|
||||
|
@ -182,7 +163,7 @@ where
|
|||
.with_cursor_style(platform::CursorStyle::PointingHand)
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||
pub struct ModalStyle {
|
||||
close_icon: Interactive<IconStyle>,
|
||||
container: ContainerStyle,
|
||||
|
@ -220,13 +201,13 @@ where
|
|||
title,
|
||||
style
|
||||
.title_text
|
||||
.style_for(&mut MouseState::default(), false)
|
||||
.style_for(&mut MouseState::default())
|
||||
.clone(),
|
||||
))
|
||||
.with_child(
|
||||
// FIXME: Get a better tag type
|
||||
MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| {
|
||||
let style = style.close_icon.style_for(state, false);
|
||||
let style = style.close_icon.style_for(state);
|
||||
icon(style)
|
||||
})
|
||||
.on_click(platform::MouseButton::Left, move |_, _, cx| {
|
||||
|
|
|
@ -208,7 +208,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
|
|||
cx: &AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
|
||||
|
||||
let theme_match = &self.matches[ix];
|
||||
Label::new(theme_match.string.clone(), style.label.clone())
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
[package]
|
||||
name = "theme_testbench"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/theme_testbench.rs"
|
||||
doctest = false
|
||||
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
theme = { path = "../theme" }
|
||||
settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace" }
|
||||
project = { path = "../project" }
|
||||
|
||||
smallvec.workspace = true
|
|
@ -1,300 +0,0 @@
|
|||
use gpui::{
|
||||
actions,
|
||||
color::Color,
|
||||
elements::{
|
||||
AnyElement, Canvas, Container, ContainerStyle, Flex, Label, Margin, MouseEventHandler,
|
||||
Padding, ParentElement,
|
||||
},
|
||||
fonts::TextStyle,
|
||||
AppContext, Border, Element, Entity, ModelHandle, Quad, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use project::Project;
|
||||
use theme::{ColorScheme, Layer, Style, StyleSet, ThemeSettings};
|
||||
use workspace::{item::Item, register_deserializable_item, Pane, Workspace};
|
||||
|
||||
actions!(theme, [DeployThemeTestbench]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ThemeTestbench::deploy);
|
||||
|
||||
register_deserializable_item::<ThemeTestbench>(cx)
|
||||
}
|
||||
|
||||
pub struct ThemeTestbench {}
|
||||
|
||||
impl ThemeTestbench {
|
||||
pub fn deploy(
|
||||
workspace: &mut Workspace,
|
||||
_: &DeployThemeTestbench,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let view = cx.add_view(|_| ThemeTestbench {});
|
||||
workspace.add_item(Box::new(view), cx);
|
||||
}
|
||||
|
||||
fn render_ramps(color_scheme: &ColorScheme) -> Flex<Self> {
|
||||
fn display_ramp(ramp: &Vec<Color>) -> AnyElement<ThemeTestbench> {
|
||||
Flex::row()
|
||||
.with_children(ramp.iter().cloned().map(|color| {
|
||||
Canvas::new(move |scene, bounds, _, _, _| {
|
||||
scene.push_quad(Quad {
|
||||
bounds,
|
||||
background: Some(color),
|
||||
..Default::default()
|
||||
});
|
||||
})
|
||||
.flex(1.0, false)
|
||||
}))
|
||||
.flex(1.0, false)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(display_ramp(&color_scheme.ramps.neutral))
|
||||
.with_child(display_ramp(&color_scheme.ramps.red))
|
||||
.with_child(display_ramp(&color_scheme.ramps.orange))
|
||||
.with_child(display_ramp(&color_scheme.ramps.yellow))
|
||||
.with_child(display_ramp(&color_scheme.ramps.green))
|
||||
.with_child(display_ramp(&color_scheme.ramps.cyan))
|
||||
.with_child(display_ramp(&color_scheme.ramps.blue))
|
||||
.with_child(display_ramp(&color_scheme.ramps.violet))
|
||||
.with_child(display_ramp(&color_scheme.ramps.magenta))
|
||||
}
|
||||
|
||||
fn render_layer(
|
||||
layer_index: usize,
|
||||
layer: &Layer,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Container<Self> {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Self::render_button_set(0, layer_index, "base", &layer.base, cx).flex(1., false),
|
||||
)
|
||||
.with_child(
|
||||
Self::render_button_set(1, layer_index, "variant", &layer.variant, cx)
|
||||
.flex(1., false),
|
||||
)
|
||||
.with_child(
|
||||
Self::render_button_set(2, layer_index, "on", &layer.on, cx).flex(1., false),
|
||||
)
|
||||
.with_child(
|
||||
Self::render_button_set(3, layer_index, "accent", &layer.accent, cx)
|
||||
.flex(1., false),
|
||||
)
|
||||
.with_child(
|
||||
Self::render_button_set(4, layer_index, "positive", &layer.positive, cx)
|
||||
.flex(1., false),
|
||||
)
|
||||
.with_child(
|
||||
Self::render_button_set(5, layer_index, "warning", &layer.warning, cx)
|
||||
.flex(1., false),
|
||||
)
|
||||
.with_child(
|
||||
Self::render_button_set(6, layer_index, "negative", &layer.negative, cx)
|
||||
.flex(1., false),
|
||||
)
|
||||
.contained()
|
||||
.with_style(ContainerStyle {
|
||||
margin: Margin {
|
||||
top: 10.,
|
||||
bottom: 10.,
|
||||
left: 10.,
|
||||
right: 10.,
|
||||
},
|
||||
background_color: Some(layer.base.default.background),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn render_button_set(
|
||||
set_index: usize,
|
||||
layer_index: usize,
|
||||
set_name: &'static str,
|
||||
style_set: &StyleSet,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Flex<Self> {
|
||||
Flex::row()
|
||||
.with_child(Self::render_button(
|
||||
set_index * 6,
|
||||
layer_index,
|
||||
set_name,
|
||||
&style_set,
|
||||
None,
|
||||
cx,
|
||||
))
|
||||
.with_child(Self::render_button(
|
||||
set_index * 6 + 1,
|
||||
layer_index,
|
||||
"hovered",
|
||||
&style_set,
|
||||
Some(|style_set| &style_set.hovered),
|
||||
cx,
|
||||
))
|
||||
.with_child(Self::render_button(
|
||||
set_index * 6 + 2,
|
||||
layer_index,
|
||||
"pressed",
|
||||
&style_set,
|
||||
Some(|style_set| &style_set.pressed),
|
||||
cx,
|
||||
))
|
||||
.with_child(Self::render_button(
|
||||
set_index * 6 + 3,
|
||||
layer_index,
|
||||
"active",
|
||||
&style_set,
|
||||
Some(|style_set| &style_set.active),
|
||||
cx,
|
||||
))
|
||||
.with_child(Self::render_button(
|
||||
set_index * 6 + 4,
|
||||
layer_index,
|
||||
"disabled",
|
||||
&style_set,
|
||||
Some(|style_set| &style_set.disabled),
|
||||
cx,
|
||||
))
|
||||
.with_child(Self::render_button(
|
||||
set_index * 6 + 5,
|
||||
layer_index,
|
||||
"inverted",
|
||||
&style_set,
|
||||
Some(|style_set| &style_set.inverted),
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
fn render_button(
|
||||
button_index: usize,
|
||||
layer_index: usize,
|
||||
text: &'static str,
|
||||
style_set: &StyleSet,
|
||||
style_override: Option<fn(&StyleSet) -> &Style>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
enum TestBenchButton {}
|
||||
MouseEventHandler::<TestBenchButton, _>::new(layer_index + button_index, cx, |state, cx| {
|
||||
let style = if let Some(style_override) = style_override {
|
||||
style_override(&style_set)
|
||||
} else if state.clicked().is_some() {
|
||||
&style_set.pressed
|
||||
} else if state.hovered() {
|
||||
&style_set.hovered
|
||||
} else {
|
||||
&style_set.default
|
||||
};
|
||||
|
||||
Self::render_label(text.to_string(), style, cx)
|
||||
.contained()
|
||||
.with_style(ContainerStyle {
|
||||
margin: Margin {
|
||||
top: 4.,
|
||||
bottom: 4.,
|
||||
left: 4.,
|
||||
right: 4.,
|
||||
},
|
||||
padding: Padding {
|
||||
top: 4.,
|
||||
bottom: 4.,
|
||||
left: 4.,
|
||||
right: 4.,
|
||||
},
|
||||
background_color: Some(style.background),
|
||||
border: Border {
|
||||
width: 1.,
|
||||
color: style.border,
|
||||
overlay: false,
|
||||
top: true,
|
||||
bottom: true,
|
||||
left: true,
|
||||
right: true,
|
||||
},
|
||||
corner_radius: 2.,
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.flex(1., true)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_label(text: String, style: &Style, cx: &mut ViewContext<Self>) -> Label {
|
||||
let settings = settings::get::<ThemeSettings>(cx);
|
||||
let font_cache = cx.font_cache();
|
||||
let family_id = settings.buffer_font_family;
|
||||
let font_size = settings.buffer_font_size(cx);
|
||||
let font_id = font_cache
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: style.foreground,
|
||||
font_family_id: family_id,
|
||||
font_family_name: font_cache.family_name(family_id).unwrap(),
|
||||
font_id,
|
||||
font_size,
|
||||
font_properties: Default::default(),
|
||||
underline: Default::default(),
|
||||
};
|
||||
|
||||
Label::new(text, text_style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ThemeTestbench {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ThemeTestbench {
|
||||
fn ui_name() -> &'static str {
|
||||
"ThemeTestbench"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
|
||||
let color_scheme = &theme::current(cx).clone().color_scheme;
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Self::render_ramps(color_scheme)
|
||||
.contained()
|
||||
.with_margin_right(10.)
|
||||
.flex(0.1, false),
|
||||
)
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(Self::render_layer(100, &color_scheme.lowest, cx).flex(1., true))
|
||||
.with_child(Self::render_layer(200, &color_scheme.middle, cx).flex(1., true))
|
||||
.with_child(Self::render_layer(300, &color_scheme.highest, cx).flex(1., true))
|
||||
.flex(1., false),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for ThemeTestbench {
|
||||
fn tab_content<T: View>(
|
||||
&self,
|
||||
_: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
_: &AppContext,
|
||||
) -> AnyElement<T> {
|
||||
Label::new("Theme Testbench", style.label.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some("ThemeTestBench")
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
_project: ModelHandle<Project>,
|
||||
_workspace: WeakViewHandle<Workspace>,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
_item_id: workspace::ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<gpui::anyhow::Result<ViewHandle<Self>>> {
|
||||
Task::ready(Ok(cx.add_view(|_| Self {})))
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||
lazy_static::lazy_static! {
|
||||
pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory");
|
||||
pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed");
|
||||
pub static ref CONVERSATIONS_DIR: PathBuf = HOME.join(".config/zed/conversations");
|
||||
pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed");
|
||||
pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed");
|
||||
pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");
|
||||
|
|
|
@ -209,8 +209,9 @@ impl Motion {
|
|||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
times: usize,
|
||||
maybe_times: Option<usize>,
|
||||
) -> Option<(DisplayPoint, SelectionGoal)> {
|
||||
let times = maybe_times.unwrap_or(1);
|
||||
use Motion::*;
|
||||
let infallible = self.infallible();
|
||||
let (new_point, goal) = match self {
|
||||
|
@ -236,7 +237,10 @@ impl Motion {
|
|||
EndOfLine => (end_of_line(map, point), SelectionGoal::None),
|
||||
CurrentLine => (end_of_line(map, point), SelectionGoal::None),
|
||||
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
||||
EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
|
||||
EndOfDocument => (
|
||||
end_of_document(map, point, maybe_times),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
Matching => (matching(map, point), SelectionGoal::None),
|
||||
FindForward { before, text } => (
|
||||
find_forward(map, point, *before, text.clone(), times),
|
||||
|
@ -257,7 +261,7 @@ impl Motion {
|
|||
&self,
|
||||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
times: usize,
|
||||
times: Option<usize>,
|
||||
expand_to_surrounding_newline: bool,
|
||||
) -> bool {
|
||||
if let Some((new_head, goal)) =
|
||||
|
@ -473,14 +477,19 @@ fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) ->
|
|||
map.clip_point(new_point, Bias::Left)
|
||||
}
|
||||
|
||||
fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
|
||||
let mut new_point = if line == 1 {
|
||||
map.max_point()
|
||||
fn end_of_document(
|
||||
map: &DisplaySnapshot,
|
||||
point: DisplayPoint,
|
||||
line: Option<usize>,
|
||||
) -> DisplayPoint {
|
||||
let new_row = if let Some(line) = line {
|
||||
(line - 1) as u32
|
||||
} else {
|
||||
Point::new((line - 1) as u32, 0).to_display_point(map)
|
||||
map.max_buffer_row()
|
||||
};
|
||||
*new_point.column_mut() = point.column();
|
||||
map.clip_point(new_point, Bias::Left)
|
||||
|
||||
let new_point = Point::new(new_row, point.column());
|
||||
map.clip_point(new_point.to_display_point(map), Bias::Left)
|
||||
}
|
||||
|
||||
fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
mod case;
|
||||
mod change;
|
||||
mod delete;
|
||||
mod scroll;
|
||||
mod substitute;
|
||||
mod yank;
|
||||
|
||||
use std::{borrow::Cow, cmp::Ordering, sync::Arc};
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
motion::Motion,
|
||||
|
@ -12,25 +15,22 @@ use crate::{
|
|||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
display_map::ToDisplayPoint,
|
||||
scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
|
||||
Anchor, Bias, ClipboardSelection, DisplayPoint, Editor,
|
||||
display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, Bias, ClipboardSelection,
|
||||
DisplayPoint,
|
||||
};
|
||||
use gpui::{actions, impl_actions, AppContext, ViewContext, WindowContext};
|
||||
use gpui::{actions, AppContext, ViewContext, WindowContext};
|
||||
use language::{AutoindentMode, Point, SelectionGoal};
|
||||
use log::error;
|
||||
use serde::Deserialize;
|
||||
use workspace::Workspace;
|
||||
|
||||
use self::{
|
||||
case::change_case,
|
||||
change::{change_motion, change_object},
|
||||
delete::{delete_motion, delete_object},
|
||||
substitute::substitute,
|
||||
yank::{yank_motion, yank_object},
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize)]
|
||||
struct Scroll(ScrollAmount);
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[
|
||||
|
@ -45,17 +45,24 @@ actions!(
|
|||
DeleteToEndOfLine,
|
||||
Paste,
|
||||
Yank,
|
||||
Substitute,
|
||||
ChangeCase,
|
||||
]
|
||||
);
|
||||
|
||||
impl_actions!(vim, [Scroll]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(insert_after);
|
||||
cx.add_action(insert_first_non_whitespace);
|
||||
cx.add_action(insert_end_of_line);
|
||||
cx.add_action(insert_line_above);
|
||||
cx.add_action(insert_line_below);
|
||||
cx.add_action(change_case);
|
||||
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let times = vim.pop_number_operator(cx);
|
||||
substitute(vim, times, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let times = vim.pop_number_operator(cx);
|
||||
|
@ -81,19 +88,14 @@ pub fn init(cx: &mut AppContext) {
|
|||
})
|
||||
});
|
||||
cx.add_action(paste);
|
||||
cx.add_action(|_: &mut Workspace, Scroll(amount): &Scroll, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
scroll(editor, amount, cx);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
scroll::init(cx);
|
||||
}
|
||||
|
||||
pub fn normal_motion(
|
||||
motion: Motion,
|
||||
operator: Option<Operator>,
|
||||
times: usize,
|
||||
times: Option<usize>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
|
@ -129,7 +131,7 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) {
|
|||
})
|
||||
}
|
||||
|
||||
fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
|
||||
fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, goal| {
|
||||
|
@ -147,7 +149,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
|
|||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::Right.move_point(map, cursor, goal, 1)
|
||||
Motion::Right.move_point(map, cursor, goal, None)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -164,7 +166,7 @@ fn insert_first_non_whitespace(
|
|||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::FirstNonWhitespace.move_point(map, cursor, goal, 1)
|
||||
Motion::FirstNonWhitespace.move_point(map, cursor, goal, None)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -177,7 +179,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
|||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::EndOfLine.move_point(map, cursor, goal, 1)
|
||||
Motion::EndOfLine.move_point(map, cursor, goal, None)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -237,7 +239,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
|||
});
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::EndOfLine.move_point(map, cursor, goal, 1)
|
||||
Motion::EndOfLine.move_point(map, cursor, goal, None)
|
||||
});
|
||||
});
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
|
@ -384,46 +386,6 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
|
|||
});
|
||||
}
|
||||
|
||||
fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
|
||||
let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
|
||||
editor.scroll_screen(amount, cx);
|
||||
if should_move_cursor {
|
||||
let selection_ordering = editor.newest_selection_on_screen(cx);
|
||||
if selection_ordering.is_eq() {
|
||||
return;
|
||||
}
|
||||
|
||||
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
|
||||
visible_rows as u32
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
|
||||
let top_anchor = editor.scroll_manager.anchor().anchor;
|
||||
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.replace_cursors_with(|snapshot| {
|
||||
let mut new_point = top_anchor.to_display_point(&snapshot);
|
||||
|
||||
match selection_ordering {
|
||||
Ordering::Less => {
|
||||
*new_point.row_mut() += scroll_margin_rows;
|
||||
new_point = snapshot.clip_point(new_point, Bias::Right);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
*new_point.row_mut() += visible_rows - scroll_margin_rows as u32;
|
||||
new_point = snapshot.clip_point(new_point, Bias::Left);
|
||||
}
|
||||
Ordering::Equal => unreachable!(),
|
||||
}
|
||||
|
||||
vec![new_point]
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
|
|
64
crates/vim/src/normal/case.rs
Normal file
64
crates/vim/src/normal/case.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use gpui::ViewContext;
|
||||
use language::Point;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{motion::Motion, normal::ChangeCase, Vim};
|
||||
|
||||
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let count = vim.pop_number_operator(cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.start == selection.end {
|
||||
Motion::Right.expand_selection(map, selection, count, true);
|
||||
}
|
||||
})
|
||||
});
|
||||
let selections = editor.selections.all::<Point>(cx);
|
||||
for selection in selections.into_iter().rev() {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
let range = selection.start..selection.end;
|
||||
let text = snapshot
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.flat_map(|s| s.chars())
|
||||
.flat_map(|c| {
|
||||
if c.is_lowercase() {
|
||||
c.to_uppercase().collect::<Vec<char>>()
|
||||
} else {
|
||||
c.to_lowercase().collect::<Vec<char>>()
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
buffer.edit([(range, text)], None, cx)
|
||||
})
|
||||
}
|
||||
});
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_case(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state(indoc! {"ˇabC\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["~"]);
|
||||
cx.assert_editor_state("AˇbC\n");
|
||||
cx.simulate_keystrokes(["2", "~"]);
|
||||
cx.assert_editor_state("ABcˇ\n");
|
||||
|
||||
cx.set_state(indoc! {"a😀C«dÉ1*fˇ»\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["~"]);
|
||||
cx.assert_editor_state("a😀CDé1*Fˇ\n");
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use editor::{
|
|||
use gpui::WindowContext;
|
||||
use language::Selection;
|
||||
|
||||
pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
|
||||
pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
// Some motions ignore failure when switching to normal mode
|
||||
let mut motion_succeeded = matches!(
|
||||
motion,
|
||||
|
@ -78,10 +78,10 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
|
|||
fn expand_changed_word_selection(
|
||||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
times: usize,
|
||||
times: Option<usize>,
|
||||
ignore_punctuation: bool,
|
||||
) -> bool {
|
||||
if times == 1 {
|
||||
if times.is_none() || times.unwrap() == 1 {
|
||||
let in_word = map
|
||||
.chars_at(selection.head())
|
||||
.next()
|
||||
|
@ -97,7 +97,8 @@ fn expand_changed_word_selection(
|
|||
});
|
||||
true
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, 1, false)
|
||||
Motion::NextWordStart { ignore_punctuation }
|
||||
.expand_selection(map, selection, None, false)
|
||||
}
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
|
||||
|
|
|
@ -3,7 +3,7 @@ use collections::{HashMap, HashSet};
|
|||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
|
||||
use gpui::WindowContext;
|
||||
|
||||
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
|
||||
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
|
120
crates/vim/src/normal/scroll.rs
Normal file
120
crates/vim/src/normal/scroll.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use crate::Vim;
|
||||
use editor::{display_map::ToDisplayPoint, scroll::scroll_amount::ScrollAmount, Editor};
|
||||
use gpui::{actions, AppContext, ViewContext};
|
||||
use language::Bias;
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
[LineUp, LineDown, ScrollUp, ScrollDown, PageUp, PageDown,]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &LineDown, cx| {
|
||||
scroll(cx, |c| ScrollAmount::Line(c.unwrap_or(1.)))
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &LineUp, cx| {
|
||||
scroll(cx, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &PageDown, cx| {
|
||||
scroll(cx, |c| ScrollAmount::Page(c.unwrap_or(1.)))
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &PageUp, cx| {
|
||||
scroll(cx, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &ScrollDown, cx| {
|
||||
scroll(cx, |c| {
|
||||
if let Some(c) = c {
|
||||
ScrollAmount::Line(c)
|
||||
} else {
|
||||
ScrollAmount::Page(0.5)
|
||||
}
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &ScrollUp, cx| {
|
||||
scroll(cx, |c| {
|
||||
if let Some(c) = c {
|
||||
ScrollAmount::Line(-c)
|
||||
} else {
|
||||
ScrollAmount::Page(-0.5)
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn scroll(cx: &mut ViewContext<Workspace>, by: fn(c: Option<f32>) -> ScrollAmount) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let amount = by(vim.pop_number_operator(cx).map(|c| c as f32));
|
||||
vim.update_active_editor(cx, |editor, cx| scroll_editor(editor, &amount, cx));
|
||||
})
|
||||
}
|
||||
|
||||
fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
|
||||
let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
|
||||
editor.scroll_screen(amount, cx);
|
||||
if should_move_cursor {
|
||||
let selection_ordering = editor.newest_selection_on_screen(cx);
|
||||
if selection_ordering.is_eq() {
|
||||
return;
|
||||
}
|
||||
|
||||
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
|
||||
visible_rows as u32
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let top_anchor = editor.scroll_manager.anchor().anchor;
|
||||
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.replace_cursors_with(|snapshot| {
|
||||
let mut new_point = top_anchor.to_display_point(&snapshot);
|
||||
|
||||
match selection_ordering {
|
||||
Ordering::Less => {
|
||||
new_point = snapshot.clip_point(new_point, Bias::Right);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
*new_point.row_mut() += visible_rows - 1;
|
||||
new_point = snapshot.clip_point(new_point, Bias::Left);
|
||||
}
|
||||
Ordering::Equal => unreachable!(),
|
||||
}
|
||||
|
||||
vec![new_point]
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
use gpui::geometry::vector::vec2f;
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_scroll(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(indoc! {"ˇa\nb\nc\nd\ne\n"}, Mode::Normal);
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
|
||||
});
|
||||
cx.simulate_keystrokes(["ctrl-e"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.))
|
||||
});
|
||||
cx.simulate_keystrokes(["2", "ctrl-e"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.))
|
||||
});
|
||||
cx.simulate_keystrokes(["ctrl-y"]);
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.))
|
||||
});
|
||||
}
|
||||
}
|
73
crates/vim/src/normal/substitute.rs
Normal file
73
crates/vim/src/normal/substitute.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use gpui::WindowContext;
|
||||
use language::Point;
|
||||
|
||||
use crate::{motion::Motion, Mode, Vim};
|
||||
|
||||
pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.start == selection.end {
|
||||
Motion::Right.expand_selection(map, selection, count, true);
|
||||
}
|
||||
})
|
||||
});
|
||||
let selections = editor.selections.all::<Point>(cx);
|
||||
for selection in selections.into_iter().rev() {
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(selection.start..selection.end, "")], None, cx)
|
||||
})
|
||||
}
|
||||
});
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
});
|
||||
vim.switch_mode(Mode::Insert, true, cx)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_substitute(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// supports a single cursor
|
||||
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["s", "x"]);
|
||||
cx.assert_editor_state("xˇbc\n");
|
||||
|
||||
// supports a selection
|
||||
cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual { line: false });
|
||||
cx.assert_editor_state("a«bcˇ»\n");
|
||||
cx.simulate_keystrokes(["s", "x"]);
|
||||
cx.assert_editor_state("axˇ\n");
|
||||
|
||||
// supports counts
|
||||
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["2", "s", "x"]);
|
||||
cx.assert_editor_state("xˇc\n");
|
||||
|
||||
// supports multiple cursors
|
||||
cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["2", "s", "x"]);
|
||||
cx.assert_editor_state("axˇdexˇg\n");
|
||||
|
||||
// does not read beyond end of line
|
||||
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["5", "s", "x"]);
|
||||
cx.assert_editor_state("xˇ\n");
|
||||
|
||||
// it handles multibyte characters
|
||||
cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["4", "s"]);
|
||||
cx.assert_editor_state("ˇ\n");
|
||||
|
||||
// should transactionally undo selection changes
|
||||
cx.simulate_keystrokes(["escape", "u"]);
|
||||
cx.assert_editor_state("ˇcàfé\n");
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}
|
|||
use collections::HashMap;
|
||||
use gpui::WindowContext;
|
||||
|
||||
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
|
||||
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
|
|
@ -98,3 +98,44 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
|
|||
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_count_down(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(indoc! {"aˇa\nbb\ncc\ndd\nee"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["2", "down"]);
|
||||
cx.assert_editor_state("aa\nbb\ncˇc\ndd\nee");
|
||||
cx.simulate_keystrokes(["9", "down"]);
|
||||
cx.assert_editor_state("aa\nbb\ncc\ndd\neˇe");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_end_of_document_710(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// goes to end by default
|
||||
cx.set_state(indoc! {"aˇa\nbb\ncc"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["shift-g"]);
|
||||
cx.assert_editor_state("aa\nbb\ncˇc");
|
||||
|
||||
// can go to line 1 (https://github.com/zed-industries/community/issues/710)
|
||||
cx.simulate_keystrokes(["1", "shift-g"]);
|
||||
cx.assert_editor_state("aˇa\nbb\ncc");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// works in normal mode
|
||||
cx.set_state(indoc! {"aa\nbˇb\ncc"}, Mode::Normal);
|
||||
cx.simulate_keystrokes([">", ">"]);
|
||||
cx.assert_editor_state("aa\n bˇb\ncc");
|
||||
cx.simulate_keystrokes(["<", "<"]);
|
||||
cx.assert_editor_state("aa\nbˇb\ncc");
|
||||
|
||||
// works in visuial mode
|
||||
cx.simulate_keystrokes(["shift-v", "down", ">", ">"]);
|
||||
cx.assert_editor_state("aa\n b«b\n cˇ»c");
|
||||
}
|
||||
|
|
|
@ -238,13 +238,12 @@ impl Vim {
|
|||
popped_operator
|
||||
}
|
||||
|
||||
fn pop_number_operator(&mut self, cx: &mut WindowContext) -> usize {
|
||||
let mut times = 1;
|
||||
fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
|
||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||
times = number;
|
||||
self.pop_operator(cx);
|
||||
return Some(number);
|
||||
}
|
||||
times
|
||||
None
|
||||
}
|
||||
|
||||
fn clear_operator(&mut self, cx: &mut WindowContext) {
|
||||
|
|
|
@ -25,7 +25,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
cx.add_action(paste);
|
||||
}
|
||||
|
||||
pub fn visual_motion(motion: Motion, times: usize, cx: &mut WindowContext) {
|
||||
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue