Merge pull request #2199 from zed-industries/welcome-experience
Welcome experience
This commit is contained in:
@@ -197,7 +197,8 @@ impl TestServer {
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _| unimplemented!(),
|
||||
dock_default_item_factory: |_, _| unimplemented!(),
|
||||
dock_default_item_factory: |_, _| None,
|
||||
background_actions: || &[],
|
||||
});
|
||||
|
||||
Project::init(&client);
|
||||
@@ -434,15 +435,7 @@ impl TestClient {
|
||||
cx: &mut TestAppContext,
|
||||
) -> ViewHandle<Workspace> {
|
||||
let (_, root_view) = cx.add_window(|_| EmptyView);
|
||||
cx.add_view(&root_view, |cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
|
||||
}
|
||||
|
||||
fn create_new_root_dir(&mut self) -> PathBuf {
|
||||
|
||||
@@ -1449,15 +1449,7 @@ async fn test_host_disconnect(
|
||||
deterministic.run_until_parked();
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||
|
||||
let (_, workspace_b) = cx_b.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project_b.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
|
||||
@@ -4706,15 +4698,7 @@ async fn test_collaborating_with_code_actions(
|
||||
|
||||
// Join the project as client B.
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project_b.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
@@ -4937,15 +4921,7 @@ async fn test_collaborating_with_renames(
|
||||
.unwrap();
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
|
||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project_b.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
|
||||
|
||||
@@ -821,7 +821,7 @@ impl CollabTitlebarItem {
|
||||
avatar_style: AvatarStyle,
|
||||
background_color: Color,
|
||||
) -> ElementBox {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(avatar_style.image)
|
||||
.aligned()
|
||||
.contained()
|
||||
|
||||
@@ -86,6 +86,7 @@ fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut Mutable
|
||||
0,
|
||||
project,
|
||||
app_state.dock_default_item_factory,
|
||||
app_state.background_actions,
|
||||
cx,
|
||||
);
|
||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||
|
||||
@@ -128,7 +128,7 @@ impl PickerDelegate for ContactFinder {
|
||||
.style_for(mouse_state, selected);
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_finder.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
|
||||
@@ -736,7 +736,7 @@ impl ContactList {
|
||||
) -> ElementBox {
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
@@ -1090,7 +1090,7 @@ impl ContactList {
|
||||
};
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
@@ -1183,7 +1183,7 @@ impl ContactList {
|
||||
|
||||
let mut row = Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
|
||||
@@ -108,7 +108,7 @@ impl IncomingCallNotification {
|
||||
.unwrap_or(&default_project);
|
||||
Flex::row()
|
||||
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.caller_avatar)
|
||||
.aligned()
|
||||
.boxed()
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.header_avatar)
|
||||
.aligned()
|
||||
.constrained()
|
||||
|
||||
@@ -108,7 +108,7 @@ impl ProjectSharedNotification {
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
Flex::row()
|
||||
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.owner_avatar)
|
||||
.aligned()
|
||||
.boxed()
|
||||
|
||||
@@ -352,9 +352,7 @@ mod tests {
|
||||
});
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let editor = cx.add_view(&workspace, |cx| {
|
||||
let mut editor = Editor::single_line(None, cx);
|
||||
editor.set_text("abc", cx);
|
||||
|
||||
+15
-3
@@ -4,6 +4,7 @@ pub mod query;
|
||||
// Re-export
|
||||
pub use anyhow;
|
||||
use anyhow::Context;
|
||||
use gpui::MutableAppContext;
|
||||
pub use indoc::indoc;
|
||||
pub use lazy_static;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
@@ -17,6 +18,7 @@ use sqlez::domain::Migrator;
|
||||
use sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
use sqlez_macros::sql;
|
||||
use std::fs::create_dir_all;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -39,6 +41,7 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
|
||||
const DB_FILE_NAME: &'static str = "db.sqlite";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
|
||||
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
||||
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
|
||||
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||
@@ -63,11 +66,11 @@ pub async fn open_db<M: Migrator + 'static>(
|
||||
let connection = async_iife!({
|
||||
// Note: This still has a race condition where 1 set of migrations succeeds
|
||||
// (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal))
|
||||
// This will cause the first connection to have the database taken out
|
||||
// This will cause the first connection to have the database taken out
|
||||
// from under it. This *should* be fine though. The second dabatase failure will
|
||||
// cause errors in the log and so should be observed by developers while writing
|
||||
// soon-to-be good migrations. If user databases are corrupted, we toss them out
|
||||
// and try again from a blank. As long as running all migrations from start to end
|
||||
// and try again from a blank. As long as running all migrations from start to end
|
||||
// on a blank database is ok, this race condition will never be triggered.
|
||||
//
|
||||
// Basically: Don't ever push invalid migrations to stable or everyone will have
|
||||
@@ -85,7 +88,7 @@ pub async fn open_db<M: Migrator + 'static>(
|
||||
};
|
||||
}
|
||||
|
||||
// Take a lock in the failure case so that we move the db once per process instead
|
||||
// Take a lock in the failure case so that we move the db once per process instead
|
||||
// of potentially multiple times from different threads. This shouldn't happen in the
|
||||
// normal path
|
||||
let _lock = DB_FILE_OPERATIONS.lock();
|
||||
@@ -236,6 +239,15 @@ macro_rules! define_connection {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write_and_log<F>(cx: &mut MutableAppContext, db_write: impl FnOnce() -> F + Send + 'static)
|
||||
where
|
||||
F: Future<Output = anyhow::Result<()>> + Send,
|
||||
{
|
||||
cx.background()
|
||||
.spawn(async move { db_write().await.log_err() })
|
||||
.detach()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs, thread};
|
||||
|
||||
@@ -805,15 +805,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
// Create some diagnostics
|
||||
project.update(cx, |project, cx| {
|
||||
|
||||
@@ -484,7 +484,7 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
cx.set_global(DragAndDrop::<Workspace>::default());
|
||||
use workspace::item::Item;
|
||||
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
|
||||
let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(0, None, || &[], cx));
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
|
||||
|
||||
cx.add_view(&pane, |cx| {
|
||||
@@ -2353,12 +2353,16 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
|
||||
e.paste(&Paste, cx);
|
||||
e.handle_input(") ", cx);
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
( one✅
|
||||
three
|
||||
five ) ˇtwo one✅ four three six five ( one✅
|
||||
three
|
||||
five ) ˇ"});
|
||||
cx.assert_editor_state(
|
||||
&([
|
||||
"( one✅ ",
|
||||
"three ",
|
||||
"five ) ˇtwo one✅ four three six five ( one✅ ",
|
||||
"three ",
|
||||
"five ) ˇ",
|
||||
]
|
||||
.join("\n")),
|
||||
);
|
||||
|
||||
// Cut with three selections, one of which is full-line.
|
||||
cx.set_state(indoc! {"
|
||||
@@ -5562,7 +5566,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
Settings::test_async(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
|
||||
let (_, pane) = cx.add_window(|cx| Pane::new(None, cx));
|
||||
let (_, pane) = cx.add_window(|cx| Pane::new(0, None, || &[], cx));
|
||||
|
||||
let leader = pane.update(cx, |_, cx| {
|
||||
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
@@ -5831,11 +5835,11 @@ async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppCon
|
||||
cx.assert_editor_state(
|
||||
&r#"
|
||||
ˇuse some::modified;
|
||||
|
||||
|
||||
|
||||
|
||||
fn main() {
|
||||
println!("hello there");
|
||||
|
||||
|
||||
println!("around the");
|
||||
println!("world");
|
||||
}
|
||||
|
||||
@@ -65,15 +65,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
|
||||
.await;
|
||||
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root", true, cx)
|
||||
@@ -134,7 +126,7 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
(let_chain)
|
||||
(await_expression)
|
||||
] @indent
|
||||
|
||||
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "<" ">" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
|
||||
@@ -329,9 +329,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
cx.dispatch_action(window_id, Toggle);
|
||||
|
||||
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
|
||||
@@ -385,9 +383,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
|
||||
@@ -461,9 +457,7 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
finder
|
||||
@@ -487,9 +481,7 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
|
||||
@@ -541,9 +533,7 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
@@ -585,9 +575,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
// When workspace has an active item, sort items which are closer to that item
|
||||
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
|
||||
@@ -624,9 +612,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let (_, finder) =
|
||||
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
|
||||
finder
|
||||
|
||||
@@ -5086,7 +5086,7 @@ impl<T: Entity> From<WeakModelHandle<T>> for AnyWeakModelHandle {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Copy)]
|
||||
pub struct WeakViewHandle<T> {
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::{borrow::Cow, cell::RefCell, collections::HashMap};
|
||||
use image::ImageFormat;
|
||||
use std::{borrow::Cow, cell::RefCell, collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::ImageData;
|
||||
|
||||
pub trait AssetSource: 'static + Send + Sync {
|
||||
fn load(&self, path: &str) -> Result<Cow<[u8]>>;
|
||||
@@ -22,6 +25,7 @@ impl AssetSource for () {
|
||||
pub struct AssetCache {
|
||||
source: Box<dyn AssetSource>,
|
||||
svgs: RefCell<HashMap<String, usvg::Tree>>,
|
||||
pngs: RefCell<HashMap<String, Arc<ImageData>>>,
|
||||
}
|
||||
|
||||
impl AssetCache {
|
||||
@@ -29,6 +33,7 @@ impl AssetCache {
|
||||
Self {
|
||||
source: Box::new(source),
|
||||
svgs: RefCell::new(HashMap::new()),
|
||||
pngs: RefCell::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,4 +48,18 @@ impl AssetCache {
|
||||
Ok(svg)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn png(&self, path: &str) -> Result<Arc<ImageData>> {
|
||||
let mut pngs = self.pngs.borrow_mut();
|
||||
if let Some(png) = pngs.get(path) {
|
||||
Ok(png.clone())
|
||||
} else {
|
||||
let bytes = self.source.load(path)?;
|
||||
let image = ImageData::new(
|
||||
image::load_from_memory_with_format(&bytes, ImageFormat::Png)?.into_bgra8(),
|
||||
);
|
||||
pngs.insert(path.to_string(), image.clone());
|
||||
Ok(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,9 @@ impl Element for ConstrainedBox {
|
||||
_: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
cx.paint_layer(Some(visible_bounds), |cx| {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct Flex {
|
||||
axis: Axis,
|
||||
children: Vec<ElementBox>,
|
||||
scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
|
||||
child_alignment: f32,
|
||||
}
|
||||
|
||||
impl Flex {
|
||||
@@ -30,6 +31,7 @@ impl Flex {
|
||||
axis,
|
||||
children: Default::default(),
|
||||
scroll_state: None,
|
||||
child_alignment: -1.,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +43,15 @@ impl Flex {
|
||||
Self::new(Axis::Vertical)
|
||||
}
|
||||
|
||||
/// Render children centered relative to the cross-axis of the parent flex.
|
||||
///
|
||||
/// If this is a flex row, children will be centered vertically. If this is a
|
||||
/// flex column, children will be centered horizontally.
|
||||
pub fn align_children_center(mut self) -> Self {
|
||||
self.child_alignment = 0.;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scrollable<Tag, V>(
|
||||
mut self,
|
||||
element_id: usize,
|
||||
@@ -309,7 +320,30 @@ impl Element for Flex {
|
||||
}
|
||||
}
|
||||
|
||||
child.paint(child_origin, visible_bounds, cx);
|
||||
// We use the child_alignment f32 to determine a point along the cross axis of the
|
||||
// overall flex element and each child. We then align these points. So 0 would center
|
||||
// each child relative to the overall height/width of the flex. -1 puts children at
|
||||
// the start. 1 puts children at the end.
|
||||
let aligned_child_origin = {
|
||||
let cross_axis = self.axis.invert();
|
||||
let my_center = bounds.size().along(cross_axis) / 2.;
|
||||
let my_target = my_center + my_center * self.child_alignment;
|
||||
|
||||
let child_center = child.size().along(cross_axis) / 2.;
|
||||
let child_target = child_center + child_center * self.child_alignment;
|
||||
|
||||
let mut aligned_child_origin = child_origin;
|
||||
match self.axis {
|
||||
Axis::Horizontal => aligned_child_origin
|
||||
.set_y(aligned_child_origin.y() - (child_target - my_target)),
|
||||
Axis::Vertical => aligned_child_origin
|
||||
.set_x(aligned_child_origin.x() - (child_target - my_target)),
|
||||
}
|
||||
|
||||
aligned_child_origin
|
||||
};
|
||||
|
||||
child.paint(aligned_child_origin, visible_bounds, cx);
|
||||
|
||||
match self.axis {
|
||||
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
|
||||
|
||||
@@ -11,8 +11,13 @@ use crate::{
|
||||
use serde::Deserialize;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
enum ImageSource {
|
||||
Path(&'static str),
|
||||
Data(Arc<ImageData>),
|
||||
}
|
||||
|
||||
pub struct Image {
|
||||
data: Arc<ImageData>,
|
||||
source: ImageSource,
|
||||
style: ImageStyle,
|
||||
}
|
||||
|
||||
@@ -31,9 +36,16 @@ pub struct ImageStyle {
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn new(data: Arc<ImageData>) -> Self {
|
||||
pub fn new(asset_path: &'static str) -> Self {
|
||||
Self {
|
||||
data,
|
||||
source: ImageSource::Path(asset_path),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_data(data: Arc<ImageData>) -> Self {
|
||||
Self {
|
||||
source: ImageSource::Data(data),
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -45,39 +57,53 @@ impl Image {
|
||||
}
|
||||
|
||||
impl Element for Image {
|
||||
type LayoutState = ();
|
||||
type LayoutState = Option<Arc<ImageData>>;
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_: &mut LayoutContext,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let data = match &self.source {
|
||||
ImageSource::Path(path) => match cx.asset_cache.png(path) {
|
||||
Ok(data) => data,
|
||||
Err(error) => {
|
||||
log::error!("could not load image: {}", error);
|
||||
return (Vector2F::zero(), None);
|
||||
}
|
||||
},
|
||||
ImageSource::Data(data) => data.clone(),
|
||||
};
|
||||
|
||||
let desired_size = vec2f(
|
||||
self.style.width.unwrap_or_else(|| constraint.max.x()),
|
||||
self.style.height.unwrap_or_else(|| constraint.max.y()),
|
||||
);
|
||||
let size = constrain_size_preserving_aspect_ratio(
|
||||
constraint.constrain(desired_size),
|
||||
self.data.size().to_f32(),
|
||||
data.size().to_f32(),
|
||||
);
|
||||
(size, ())
|
||||
|
||||
(size, Some(data))
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
layout: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
cx.scene.push_image(scene::Image {
|
||||
bounds,
|
||||
border: self.style.border,
|
||||
corner_radius: self.style.corner_radius,
|
||||
grayscale: self.style.grayscale,
|
||||
data: self.data.clone(),
|
||||
});
|
||||
if let Some(data) = layout {
|
||||
cx.scene.push_image(scene::Image {
|
||||
bounds,
|
||||
border: self.style.border,
|
||||
corner_radius: self.style.corner_radius,
|
||||
grayscale: self.style.grayscale,
|
||||
data: data.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "install_cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/install_cli.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
smol = "1.2.5"
|
||||
anyhow = "1.0.38"
|
||||
log = "0.4"
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
@@ -0,0 +1,55 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{actions, AsyncAppContext};
|
||||
use util::ResultExt;
|
||||
|
||||
actions!(cli, [Install]);
|
||||
|
||||
pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
|
||||
let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
|
||||
let link_path = Path::new("/usr/local/bin/zed");
|
||||
let bin_dir_path = link_path.parent().unwrap();
|
||||
|
||||
// Don't re-create symlink if it points to the same CLI binary.
|
||||
if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If the symlink is not there or is outdated, first try replacing it
|
||||
// without escalating.
|
||||
smol::fs::remove_file(link_path).await.log_err();
|
||||
if smol::fs::unix::symlink(&cli_path, link_path)
|
||||
.await
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// The symlink could not be created, so use osascript with admin privileges
|
||||
// to create it.
|
||||
let status = smol::process::Command::new("osascript")
|
||||
.args([
|
||||
"-e",
|
||||
&format!(
|
||||
"do shell script \" \
|
||||
mkdir -p \'{}\' && \
|
||||
ln -sf \'{}\' \'{}\' \
|
||||
\" with administrator privileges",
|
||||
bin_dir_path.to_string_lossy(),
|
||||
cli_path.to_string_lossy(),
|
||||
link_path.to_string_lossy(),
|
||||
),
|
||||
])
|
||||
.stdout(smol::process::Stdio::inherit())
|
||||
.stderr(smol::process::Stdio::inherit())
|
||||
.output()
|
||||
.await?
|
||||
.status;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("error running osascript"))
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
async move {
|
||||
let (journal_dir, entry_path) = create_entry.await?;
|
||||
let (workspace, _) = cx
|
||||
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, cx))
|
||||
.update(|cx| workspace::open_paths(&[journal_dir], &app_state, None, cx))
|
||||
.await;
|
||||
|
||||
let opened = workspace
|
||||
|
||||
@@ -102,7 +102,10 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||
.read(cx)
|
||||
.render_match(ix, state, ix == selected_ix, cx)
|
||||
})
|
||||
.on_down(MouseButton::Left, move |_, cx| {
|
||||
// Capture mouse events
|
||||
.on_down(MouseButton::Left, |_, _| {})
|
||||
.on_up(MouseButton::Left, |_, _| {})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(SelectIndex(ix))
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
|
||||
@@ -867,7 +867,7 @@ impl LocalWorktree {
|
||||
let old_path = self.entry_for_id(entry_id)?.path.clone();
|
||||
let new_path = new_path.into();
|
||||
let abs_old_path = self.absolutize(&old_path);
|
||||
let abs_new_path = self.absolutize(&new_path);
|
||||
let abs_new_path = self.absolutize(new_path.as_ref());
|
||||
let rename = cx.background().spawn({
|
||||
let fs = self.fs.clone();
|
||||
let abs_new_path = abs_new_path.clone();
|
||||
|
||||
@@ -1262,54 +1262,89 @@ impl View for ProjectPanel {
|
||||
let padding = std::mem::take(&mut container_style.padding);
|
||||
let last_worktree_root_id = self.last_worktree_root_id;
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ProjectPanel>::new(0, cx, |_, cx| {
|
||||
UniformList::new(
|
||||
self.list.clone(),
|
||||
self.visible_entries
|
||||
.iter()
|
||||
.map(|(_, worktree_entries)| worktree_entries.len())
|
||||
.sum(),
|
||||
cx,
|
||||
move |this, range, items, cx| {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let mut dragged_entry_destination =
|
||||
this.dragged_entry_destination.clone();
|
||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(Self::render_entry(
|
||||
id,
|
||||
details,
|
||||
&this.filename_editor,
|
||||
&mut dragged_entry_destination,
|
||||
&theme.project_panel,
|
||||
cx,
|
||||
));
|
||||
});
|
||||
this.dragged_entry_destination = dragged_entry_destination;
|
||||
},
|
||||
)
|
||||
.with_padding_top(padding.top)
|
||||
.with_padding_bottom(padding.bottom)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.expanded()
|
||||
.boxed()
|
||||
})
|
||||
.on_down(MouseButton::Right, move |e, cx| {
|
||||
// When deploying the context menu anywhere below the last project entry,
|
||||
// act as if the user clicked the root of the last worktree.
|
||||
if let Some(entry_id) = last_worktree_root_id {
|
||||
cx.dispatch_action(DeployContextMenu {
|
||||
entry_id,
|
||||
position: e.position,
|
||||
})
|
||||
}
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(ChildView::new(&self.context_menu, cx).boxed())
|
||||
.boxed()
|
||||
let has_worktree = self.visible_entries.len() != 0;
|
||||
|
||||
if has_worktree {
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ProjectPanel>::new(0, cx, |_, cx| {
|
||||
UniformList::new(
|
||||
self.list.clone(),
|
||||
self.visible_entries
|
||||
.iter()
|
||||
.map(|(_, worktree_entries)| worktree_entries.len())
|
||||
.sum(),
|
||||
cx,
|
||||
move |this, range, items, cx| {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let mut dragged_entry_destination =
|
||||
this.dragged_entry_destination.clone();
|
||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(Self::render_entry(
|
||||
id,
|
||||
details,
|
||||
&this.filename_editor,
|
||||
&mut dragged_entry_destination,
|
||||
&theme.project_panel,
|
||||
cx,
|
||||
));
|
||||
});
|
||||
this.dragged_entry_destination = dragged_entry_destination;
|
||||
},
|
||||
)
|
||||
.with_padding_top(padding.top)
|
||||
.with_padding_bottom(padding.bottom)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.expanded()
|
||||
.boxed()
|
||||
})
|
||||
.on_down(MouseButton::Right, move |e, cx| {
|
||||
// When deploying the context menu anywhere below the last project entry,
|
||||
// act as if the user clicked the root of the last worktree.
|
||||
if let Some(entry_id) = last_worktree_root_id {
|
||||
cx.dispatch_action(DeployContextMenu {
|
||||
entry_id,
|
||||
position: e.position,
|
||||
})
|
||||
}
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(ChildView::new(&self.context_menu, cx).boxed())
|
||||
.boxed()
|
||||
} else {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::<Self>::new(2, cx, {
|
||||
let button_style = theme.open_project_button.clone();
|
||||
let context_menu_item_style =
|
||||
cx.global::<Settings>().theme.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();
|
||||
|
||||
theme::ui::keystroke_label(
|
||||
"Open a project",
|
||||
&button_style,
|
||||
&context_menu_item.keystroke,
|
||||
Box::new(workspace::Open),
|
||||
cx,
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(workspace::Open)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.boxed(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
@@ -1404,15 +1439,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
@@ -1504,15 +1531,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
||||
|
||||
select_path(&panel, "root1", cx);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::parse_json_with_comments;
|
||||
use crate::{parse_json_with_comments, Settings};
|
||||
use anyhow::{Context, Result};
|
||||
use assets::Assets;
|
||||
use collections::BTreeMap;
|
||||
@@ -45,6 +45,10 @@ impl KeymapFileContent {
|
||||
for path in ["keymaps/default.json", "keymaps/vim.json"] {
|
||||
Self::load(path, cx).unwrap();
|
||||
}
|
||||
|
||||
if let Some(asset_path) = cx.global::<Settings>().base_keymap.asset_path() {
|
||||
Self::load(asset_path, cx).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(asset_path: &str, cx: &mut MutableAppContext) -> Result<()> {
|
||||
|
||||
+437
-136
@@ -18,12 +18,13 @@ use sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
statement::Statement,
|
||||
};
|
||||
use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc};
|
||||
use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc};
|
||||
use theme::{Theme, ThemeRegistry};
|
||||
use tree_sitter::Query;
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
||||
pub use watched_json::watch_files;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Settings {
|
||||
@@ -54,6 +55,46 @@ pub struct Settings {
|
||||
pub telemetry_defaults: TelemetrySettings,
|
||||
pub telemetry_overrides: TelemetrySettings,
|
||||
pub auto_update: bool,
|
||||
pub base_keymap: BaseKeymap,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
|
||||
pub enum BaseKeymap {
|
||||
#[default]
|
||||
VSCode,
|
||||
JetBrains,
|
||||
Sublime,
|
||||
Atom,
|
||||
}
|
||||
|
||||
impl BaseKeymap {
|
||||
pub const OPTIONS: [(&'static str, Self); 4] = [
|
||||
("VSCode (Default)", Self::VSCode),
|
||||
("Atom", Self::Atom),
|
||||
("JetBrains", Self::JetBrains),
|
||||
("Sublime", Self::Sublime),
|
||||
];
|
||||
|
||||
pub fn asset_path(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
|
||||
BaseKeymap::Sublime => Some("keymaps/sublime_text.json"),
|
||||
BaseKeymap::Atom => Some("keymaps/atom.json"),
|
||||
BaseKeymap::VSCode => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn names() -> impl Iterator<Item = &'static str> {
|
||||
Self::OPTIONS.iter().map(|(name, _)| *name)
|
||||
}
|
||||
|
||||
pub fn from_names(option: &str) -> BaseKeymap {
|
||||
Self::OPTIONS
|
||||
.iter()
|
||||
.copied()
|
||||
.find_map(|(name, value)| (name == option).then(|| value))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -66,9 +107,18 @@ impl TelemetrySettings {
|
||||
pub fn metrics(&self) -> bool {
|
||||
self.metrics.unwrap()
|
||||
}
|
||||
|
||||
pub fn diagnostics(&self) -> bool {
|
||||
self.diagnostics.unwrap()
|
||||
}
|
||||
|
||||
pub fn set_metrics(&mut self, value: bool) {
|
||||
self.metrics = Some(value);
|
||||
}
|
||||
|
||||
pub fn set_diagnostics(&mut self, value: bool) {
|
||||
self.diagnostics = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -317,6 +367,8 @@ pub struct SettingsFileContent {
|
||||
pub telemetry: TelemetrySettings,
|
||||
#[serde(default)]
|
||||
pub auto_update: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub base_keymap: Option<BaseKeymap>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -387,6 +439,7 @@ impl Settings {
|
||||
telemetry_defaults: defaults.telemetry,
|
||||
telemetry_overrides: Default::default(),
|
||||
auto_update: defaults.auto_update.unwrap(),
|
||||
base_keymap: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,6 +477,7 @@ impl Settings {
|
||||
merge(&mut self.vim_mode, data.vim_mode);
|
||||
merge(&mut self.autosave, data.autosave);
|
||||
merge(&mut self.default_dock_anchor, data.default_dock_anchor);
|
||||
merge(&mut self.base_keymap, data.base_keymap);
|
||||
|
||||
// Ensure terminal font is loaded, so we can request it in terminal_element layout
|
||||
if let Some(terminal_font) = &data.terminal.font_family {
|
||||
@@ -601,6 +655,7 @@ impl Settings {
|
||||
},
|
||||
telemetry_overrides: Default::default(),
|
||||
auto_update: true,
|
||||
base_keymap: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,13 +732,19 @@ pub fn settings_file_json_schema(
|
||||
serde_json::to_value(root_schema).unwrap()
|
||||
}
|
||||
|
||||
/// Expects the key to be unquoted, and the value to be valid JSON
|
||||
/// (e.g. values should be unquoted for numbers and bools, quoted for strings)
|
||||
pub fn write_top_level_setting(
|
||||
mut settings_content: String,
|
||||
top_level_key: &str,
|
||||
new_val: &str,
|
||||
) -> String {
|
||||
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
|
||||
Ok(serde_json::from_reader(
|
||||
json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
|
||||
)?)
|
||||
}
|
||||
|
||||
fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_value: &Value) {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
parser.set_language(tree_sitter_json::language()).unwrap();
|
||||
let tree = parser.parse(&settings_content, None).unwrap();
|
||||
@@ -693,56 +754,64 @@ pub fn write_top_level_setting(
|
||||
let query = Query::new(
|
||||
tree_sitter_json::language(),
|
||||
"
|
||||
(document
|
||||
(object
|
||||
(pair
|
||||
key: (string) @key
|
||||
value: (_) @value)))
|
||||
",
|
||||
(pair
|
||||
key: (string) @key
|
||||
value: (_) @value)
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut depth = 0;
|
||||
let mut first_key_start = None;
|
||||
let mut existing_value_range = None;
|
||||
let mut existing_value_range = 0..settings_content.len();
|
||||
let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes());
|
||||
for mat in matches {
|
||||
if mat.captures.len() != 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = mat.captures[0];
|
||||
let value = mat.captures[1];
|
||||
let key_range = mat.captures[0].node.byte_range();
|
||||
let value_range = mat.captures[1].node.byte_range();
|
||||
|
||||
first_key_start.get_or_insert_with(|| key.node.start_byte());
|
||||
if key_range.start > existing_value_range.end {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(key_text) = settings_content.get(key.node.byte_range()) {
|
||||
if key_text == format!("\"{top_level_key}\"") {
|
||||
existing_value_range = Some(value.node.byte_range());
|
||||
first_key_start.get_or_insert_with(|| key_range.start);
|
||||
|
||||
let found_key = settings_content
|
||||
.get(key_range.clone())
|
||||
.map(|key_text| key_text == format!("\"{}\"", key_path[depth]))
|
||||
.unwrap_or(false);
|
||||
|
||||
if found_key {
|
||||
existing_value_range = value_range;
|
||||
depth += 1;
|
||||
|
||||
if depth == key_path.len() {
|
||||
break;
|
||||
} else {
|
||||
first_key_start = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (first_key_start, existing_value_range) {
|
||||
(None, None) => {
|
||||
// No document, create a new object and overwrite
|
||||
settings_content.clear();
|
||||
write!(
|
||||
settings_content,
|
||||
"{{\n \"{}\": {new_val}\n}}\n",
|
||||
top_level_key
|
||||
)
|
||||
.unwrap();
|
||||
// We found the exact key we want, insert the new value
|
||||
if depth == key_path.len() {
|
||||
let new_val = serde_json::to_string_pretty(new_value)
|
||||
.expect("Could not serialize new json field to string");
|
||||
settings_content.replace_range(existing_value_range, &new_val);
|
||||
} else {
|
||||
// We have key paths, construct the sub objects
|
||||
let new_key = key_path[depth];
|
||||
|
||||
// We don't have the key, construct the nested objects
|
||||
let mut new_value = serde_json::to_value(new_value).unwrap();
|
||||
for key in key_path[(depth + 1)..].iter().rev() {
|
||||
new_value = serde_json::json!({ key.to_string(): new_value });
|
||||
}
|
||||
|
||||
(_, Some(existing_value_range)) => {
|
||||
// Existing theme key, overwrite
|
||||
settings_content.replace_range(existing_value_range, &new_val);
|
||||
}
|
||||
|
||||
(Some(first_key_start), None) => {
|
||||
// No existing theme key, but other settings. Prepend new theme settings and
|
||||
// match style of first key
|
||||
if let Some(first_key_start) = first_key_start {
|
||||
let mut row = 0;
|
||||
let mut column = 0;
|
||||
for (ix, char) in settings_content.char_indices() {
|
||||
@@ -757,142 +826,374 @@ pub fn write_top_level_setting(
|
||||
}
|
||||
}
|
||||
|
||||
let content = format!(r#""{top_level_key}": {new_val},"#);
|
||||
settings_content.insert_str(first_key_start, &content);
|
||||
|
||||
if row > 0 {
|
||||
let new_val = to_pretty_json(&new_value, column, column);
|
||||
let content = format!(r#""{new_key}": {new_val},"#);
|
||||
settings_content.insert_str(first_key_start, &content);
|
||||
|
||||
settings_content.insert_str(
|
||||
first_key_start + content.len(),
|
||||
&format!("\n{:width$}", ' ', width = column),
|
||||
)
|
||||
} else {
|
||||
settings_content.insert_str(first_key_start + content.len(), " ")
|
||||
let new_val = serde_json::to_string(&new_value).unwrap();
|
||||
let mut content = format!(r#""{new_key}": {new_val},"#);
|
||||
content.push(' ');
|
||||
settings_content.insert_str(first_key_start, &content);
|
||||
}
|
||||
} else {
|
||||
new_value = serde_json::json!({ new_key.to_string(): new_value });
|
||||
let indent_prefix_len = 4 * depth;
|
||||
let new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
|
||||
|
||||
settings_content.replace_range(existing_value_range, &new_val);
|
||||
if depth == 0 {
|
||||
settings_content.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_pretty_json(
|
||||
value: &serde_json::Value,
|
||||
indent_size: usize,
|
||||
indent_prefix_len: usize,
|
||||
) -> String {
|
||||
const SPACES: [u8; 32] = [b' '; 32];
|
||||
|
||||
debug_assert!(indent_size <= SPACES.len());
|
||||
debug_assert!(indent_prefix_len <= SPACES.len());
|
||||
|
||||
let mut output = Vec::new();
|
||||
let mut ser = serde_json::Serializer::with_formatter(
|
||||
&mut output,
|
||||
serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
|
||||
);
|
||||
|
||||
value.serialize(&mut ser).unwrap();
|
||||
let text = String::from_utf8(output).unwrap();
|
||||
|
||||
let mut adjusted_text = String::new();
|
||||
for (i, line) in text.split('\n').enumerate() {
|
||||
if i > 0 {
|
||||
adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
|
||||
}
|
||||
adjusted_text.push_str(line);
|
||||
adjusted_text.push('\n');
|
||||
}
|
||||
adjusted_text.pop();
|
||||
adjusted_text
|
||||
}
|
||||
|
||||
pub fn update_settings_file(
|
||||
mut text: String,
|
||||
old_file_content: SettingsFileContent,
|
||||
update: impl FnOnce(&mut SettingsFileContent),
|
||||
) -> String {
|
||||
let mut new_file_content = old_file_content.clone();
|
||||
|
||||
update(&mut new_file_content);
|
||||
|
||||
let old_object = to_json_object(old_file_content);
|
||||
let new_object = to_json_object(new_file_content);
|
||||
|
||||
fn apply_changes_to_json_text(
|
||||
old_object: &serde_json::Map<String, Value>,
|
||||
new_object: &serde_json::Map<String, Value>,
|
||||
current_key_path: Vec<&str>,
|
||||
json_text: &mut String,
|
||||
) {
|
||||
for (key, old_value) in old_object.iter() {
|
||||
// We know that these two are from the same shape of object, so we can just unwrap
|
||||
let new_value = new_object.get(key).unwrap();
|
||||
if old_value != new_value {
|
||||
match new_value {
|
||||
Value::Bool(_) | Value::Number(_) | Value::String(_) => {
|
||||
let mut key_path = current_key_path.clone();
|
||||
key_path.push(key);
|
||||
write_settings_key(json_text, &key_path, &new_value);
|
||||
}
|
||||
Value::Object(new_sub_object) => {
|
||||
let mut key_path = current_key_path.clone();
|
||||
key_path.push(key);
|
||||
if let Value::Object(old_sub_object) = old_value {
|
||||
apply_changes_to_json_text(
|
||||
old_sub_object,
|
||||
new_sub_object,
|
||||
key_path,
|
||||
json_text,
|
||||
);
|
||||
} else {
|
||||
unimplemented!("This function doesn't support changing values from simple values to objects yet");
|
||||
}
|
||||
}
|
||||
Value::Null | Value::Array(_) => {
|
||||
unimplemented!("We only support objects and simple values");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settings_content
|
||||
apply_changes_to_json_text(&old_object, &new_object, vec![], &mut text);
|
||||
|
||||
text
|
||||
}
|
||||
|
||||
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map<String, Value> {
|
||||
let tmp = serde_json::to_value(settings_file).unwrap();
|
||||
match tmp {
|
||||
Value::Object(map) => map,
|
||||
_ => unreachable!("SettingsFileContent represents a JSON map"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
|
||||
Ok(serde_json::from_reader(
|
||||
json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
|
||||
)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::write_top_level_setting;
|
||||
use super::*;
|
||||
use unindent::Unindent;
|
||||
|
||||
fn assert_new_settings<S1: Into<String>, S2: Into<String>>(
|
||||
old_json: S1,
|
||||
update: fn(&mut SettingsFileContent),
|
||||
expected_new_json: S2,
|
||||
) {
|
||||
let old_json = old_json.into();
|
||||
let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
|
||||
let new_json = update_settings_file(old_json, old_content, update);
|
||||
assert_eq!(new_json, expected_new_json.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_telemetry_setting_multiple_fields() {
|
||||
assert_new_settings(
|
||||
r#"
|
||||
{
|
||||
"telemetry": {
|
||||
"metrics": false,
|
||||
"diagnostics": false
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
|settings| {
|
||||
settings.telemetry.set_diagnostics(true);
|
||||
settings.telemetry.set_metrics(true);
|
||||
},
|
||||
r#"
|
||||
{
|
||||
"telemetry": {
|
||||
"metrics": true,
|
||||
"diagnostics": true
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_telemetry_setting_weird_formatting() {
|
||||
assert_new_settings(
|
||||
r#"{
|
||||
"telemetry": { "metrics": false, "diagnostics": true }
|
||||
}"#
|
||||
.unindent(),
|
||||
|settings| settings.telemetry.set_diagnostics(false),
|
||||
r#"{
|
||||
"telemetry": { "metrics": false, "diagnostics": false }
|
||||
}"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_telemetry_setting_other_fields() {
|
||||
assert_new_settings(
|
||||
r#"
|
||||
{
|
||||
"telemetry": {
|
||||
"metrics": false,
|
||||
"diagnostics": true
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
|settings| settings.telemetry.set_diagnostics(false),
|
||||
r#"
|
||||
{
|
||||
"telemetry": {
|
||||
"metrics": false,
|
||||
"diagnostics": false
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_telemetry_setting_empty_telemetry() {
|
||||
assert_new_settings(
|
||||
r#"
|
||||
{
|
||||
"telemetry": {}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
|settings| settings.telemetry.set_diagnostics(false),
|
||||
r#"
|
||||
{
|
||||
"telemetry": {
|
||||
"diagnostics": false
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_telemetry_setting_pre_existing() {
|
||||
assert_new_settings(
|
||||
r#"
|
||||
{
|
||||
"telemetry": {
|
||||
"diagnostics": true
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
|settings| settings.telemetry.set_diagnostics(false),
|
||||
r#"
|
||||
{
|
||||
"telemetry": {
|
||||
"diagnostics": false
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_telemetry_setting() {
|
||||
assert_new_settings(
|
||||
"{}",
|
||||
|settings| settings.telemetry.set_diagnostics(true),
|
||||
r#"
|
||||
{
|
||||
"telemetry": {
|
||||
"diagnostics": true
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_object_empty_doc() {
|
||||
assert_new_settings(
|
||||
"",
|
||||
|settings| settings.telemetry.set_diagnostics(true),
|
||||
r#"
|
||||
{
|
||||
"telemetry": {
|
||||
"diagnostics": true
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_into_settings_with_theme() {
|
||||
let settings = r#"
|
||||
{
|
||||
"theme": "One Dark"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new_settings = r#"
|
||||
{
|
||||
"theme": "summerfruit-light"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
assert_new_settings(
|
||||
r#"
|
||||
{
|
||||
"theme": "One Dark"
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||
r#"
|
||||
{
|
||||
"theme": "summerfruit-light"
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_into_empty_settings() {
|
||||
let settings = r#"
|
||||
{
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new_settings = r#"
|
||||
{
|
||||
"theme": "summerfruit-light"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
assert_new_settings(
|
||||
r#"
|
||||
{
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||
r#"
|
||||
{
|
||||
"theme": "summerfruit-light"
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_into_no_settings() {
|
||||
let settings = "".to_string();
|
||||
|
||||
let new_settings = r#"
|
||||
{
|
||||
"theme": "summerfruit-light"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
fn write_key_no_document() {
|
||||
assert_new_settings(
|
||||
"",
|
||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||
r#"
|
||||
{
|
||||
"theme": "summerfruit-light"
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_into_single_line_settings_without_theme() {
|
||||
let settings = r#"{ "a": "", "ok": true }"#.to_string();
|
||||
let new_settings = r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#;
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
assert_new_settings(
|
||||
r#"{ "a": "", "ok": true }"#,
|
||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||
r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_pre_object_whitespace() {
|
||||
let settings = r#" { "a": "", "ok": true }"#.to_string();
|
||||
let new_settings = r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#;
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
assert_new_settings(
|
||||
r#" { "a": "", "ok": true }"#,
|
||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||
r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_into_multi_line_settings_without_theme() {
|
||||
let settings = r#"
|
||||
{
|
||||
"a": "b"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new_settings = r#"
|
||||
{
|
||||
"theme": "summerfruit-light",
|
||||
"a": "b"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
assert_new_settings(
|
||||
r#"
|
||||
{
|
||||
"a": "b"
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
|settings| settings.theme = Some("summerfruit-light".to_string()),
|
||||
r#"
|
||||
{
|
||||
"theme": "summerfruit-light",
|
||||
"a": "b"
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::{watched_json::WatchedJsonFile, write_top_level_setting, SettingsFileContent};
|
||||
use crate::{update_settings_file, watched_json::WatchedJsonFile, SettingsFileContent};
|
||||
use anyhow::Result;
|
||||
use fs::Fs;
|
||||
use gpui::MutableAppContext;
|
||||
use serde_json::Value;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
|
||||
@@ -27,57 +26,24 @@ impl SettingsFile {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(cx: &mut MutableAppContext, update: impl FnOnce(&mut SettingsFileContent)) {
|
||||
pub fn update(
|
||||
cx: &mut MutableAppContext,
|
||||
update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
|
||||
) {
|
||||
let this = cx.global::<SettingsFile>();
|
||||
|
||||
let current_file_content = this.settings_file_content.current();
|
||||
let mut new_file_content = current_file_content.clone();
|
||||
|
||||
update(&mut new_file_content);
|
||||
|
||||
let fs = this.fs.clone();
|
||||
let path = this.path.clone();
|
||||
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
// Unwrap safety: These values are all guarnteed to be well formed, and we know
|
||||
// that they will deserialize to our settings object. All of the following unwraps
|
||||
// are therefore safe.
|
||||
let tmp = serde_json::to_value(current_file_content).unwrap();
|
||||
let old_json = tmp.as_object().unwrap();
|
||||
let old_text = fs.load(path).await?;
|
||||
|
||||
let new_tmp = serde_json::to_value(new_file_content).unwrap();
|
||||
let new_json = new_tmp.as_object().unwrap();
|
||||
let new_text = update_settings_file(old_text, current_file_content, update);
|
||||
|
||||
// Find changed fields
|
||||
let mut diffs = vec![];
|
||||
for (key, old_value) in old_json.iter() {
|
||||
let new_value = new_json.get(key).unwrap();
|
||||
if old_value != new_value {
|
||||
if matches!(
|
||||
new_value,
|
||||
&Value::Null | &Value::Object(_) | &Value::Array(_)
|
||||
) {
|
||||
unimplemented!(
|
||||
"We only support updating basic values at the top level"
|
||||
);
|
||||
}
|
||||
|
||||
let new_json = serde_json::to_string_pretty(new_value)
|
||||
.expect("Could not serialize new json field to string");
|
||||
|
||||
diffs.push((key, new_json));
|
||||
}
|
||||
}
|
||||
|
||||
// Have diffs, rewrite the settings file now.
|
||||
let mut content = fs.load(path).await?;
|
||||
|
||||
for (key, new_value) in diffs {
|
||||
content = write_top_level_setting(content, key, &new_value)
|
||||
}
|
||||
|
||||
fs.atomic_write(path.to_path_buf(), content).await?;
|
||||
fs.atomic_write(path.to_path_buf(), new_text).await?;
|
||||
|
||||
Ok(()) as Result<()>
|
||||
})
|
||||
@@ -88,10 +54,164 @@ impl SettingsFile {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap};
|
||||
use crate::{
|
||||
watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap,
|
||||
};
|
||||
use fs::FakeFs;
|
||||
use gpui::{actions, Action};
|
||||
use theme::ThemeRegistry;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
|
||||
let executor = cx.background();
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
let font_cache = cx.font_cache();
|
||||
|
||||
actions!(test, [A, B]);
|
||||
// From the Atom keymap
|
||||
actions!(workspace, [ActivatePreviousPane]);
|
||||
// From the JetBrains keymap
|
||||
actions!(pane, [ActivatePrevItem]);
|
||||
|
||||
fs.save(
|
||||
"/settings.json".as_ref(),
|
||||
&r#"
|
||||
{
|
||||
"base_keymap": "Atom"
|
||||
}
|
||||
"#
|
||||
.into(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
fs.save(
|
||||
"/keymap.json".as_ref(),
|
||||
&r#"
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"backspace": "test::A"
|
||||
}
|
||||
}
|
||||
]
|
||||
"#
|
||||
.into(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let settings_file =
|
||||
WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
|
||||
let keymaps_file =
|
||||
WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await;
|
||||
|
||||
let default_settings = cx.read(Settings::test);
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.add_global_action(|_: &A, _cx| {});
|
||||
cx.add_global_action(|_: &B, _cx| {});
|
||||
cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
|
||||
cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
|
||||
watch_files(
|
||||
default_settings,
|
||||
settings_file,
|
||||
ThemeRegistry::new((), font_cache),
|
||||
keymaps_file,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
|
||||
// Test loading the keymap base at all
|
||||
cx.update(|cx| {
|
||||
assert_keybindings_for(
|
||||
cx,
|
||||
vec![("backspace", &A), ("k", &ActivatePreviousPane)],
|
||||
line!(),
|
||||
);
|
||||
});
|
||||
|
||||
// Test modifying the users keymap, while retaining the base keymap
|
||||
fs.save(
|
||||
"/keymap.json".as_ref(),
|
||||
&r#"
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"backspace": "test::B"
|
||||
}
|
||||
}
|
||||
]
|
||||
"#
|
||||
.into(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
|
||||
cx.update(|cx| {
|
||||
assert_keybindings_for(
|
||||
cx,
|
||||
vec![("backspace", &B), ("k", &ActivatePreviousPane)],
|
||||
line!(),
|
||||
);
|
||||
});
|
||||
|
||||
// Test modifying the base, while retaining the users keymap
|
||||
fs.save(
|
||||
"/settings.json".as_ref(),
|
||||
&r#"
|
||||
{
|
||||
"base_keymap": "JetBrains"
|
||||
}
|
||||
"#
|
||||
.into(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
|
||||
cx.update(|cx| {
|
||||
assert_keybindings_for(
|
||||
cx,
|
||||
vec![("backspace", &B), ("[", &ActivatePrevItem)],
|
||||
line!(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn assert_keybindings_for<'a>(
|
||||
cx: &mut MutableAppContext,
|
||||
actions: Vec<(&'static str, &'a dyn Action)>,
|
||||
line: u32,
|
||||
) {
|
||||
for (key, action) in actions {
|
||||
// assert that...
|
||||
assert!(
|
||||
cx.available_actions(0, 0).any(|(_, bound_action, b)| {
|
||||
// action names match...
|
||||
bound_action.name() == action.name()
|
||||
&& bound_action.namespace() == action.namespace()
|
||||
// and key strokes contain the given key
|
||||
&& b.iter()
|
||||
.any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
|
||||
}),
|
||||
"On {} Failed to find {} with keybinding {}",
|
||||
line,
|
||||
action.name(),
|
||||
key
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
|
||||
let executor = cx.background();
|
||||
|
||||
@@ -62,7 +62,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn watch_settings_file(
|
||||
pub fn watch_files(
|
||||
defaults: Settings,
|
||||
settings_file: WatchedJsonFile<SettingsFileContent>,
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
keymap_file: WatchedJsonFile<KeymapFileContent>,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
watch_settings_file(defaults, settings_file, theme_registry, cx);
|
||||
watch_keymap_file(keymap_file, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn watch_settings_file(
|
||||
defaults: Settings,
|
||||
mut file: WatchedJsonFile<SettingsFileContent>,
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
@@ -77,13 +88,13 @@ pub fn watch_settings_file(
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) {
|
||||
fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) {
|
||||
cx.clear_bindings();
|
||||
KeymapFileContent::load_defaults(cx);
|
||||
content.add_to_cx(cx).log_err();
|
||||
}
|
||||
|
||||
pub fn settings_updated(
|
||||
fn settings_updated(
|
||||
defaults: &Settings,
|
||||
content: SettingsFileContent,
|
||||
theme_registry: &Arc<ThemeRegistry>,
|
||||
@@ -95,10 +106,20 @@ pub fn settings_updated(
|
||||
cx.refresh_windows();
|
||||
}
|
||||
|
||||
pub fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut MutableAppContext) {
|
||||
fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut MutableAppContext) {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut settings_subscription = None;
|
||||
while let Some(content) = file.0.recv().await {
|
||||
cx.update(|cx| keymap_updated(content, cx));
|
||||
cx.update(|cx| {
|
||||
let old_base_keymap = cx.global::<Settings>().base_keymap;
|
||||
keymap_updated(content.clone(), cx);
|
||||
settings_subscription = Some(cx.observe_global::<Settings, _>(move |cx| {
|
||||
let settings = cx.global::<Settings>();
|
||||
if settings.base_keymap != old_base_keymap {
|
||||
keymap_updated(content.clone(), cx);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -720,7 +720,7 @@ impl Element for TerminalElement {
|
||||
cx.paint_layer(clip_bounds, |cx| {
|
||||
let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
|
||||
|
||||
//Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
|
||||
// Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
|
||||
self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.mode, cx);
|
||||
|
||||
cx.scene.push_cursor_region(gpui::CursorRegion {
|
||||
|
||||
@@ -970,15 +970,7 @@ mod tests {
|
||||
let params = cx.update(AppState::test);
|
||||
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
(project, workspace)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ use gpui::{
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use ui::{CheckboxStyle, IconStyle};
|
||||
|
||||
pub mod ui;
|
||||
|
||||
pub use theme_registry::*;
|
||||
|
||||
@@ -37,6 +40,7 @@ pub struct Theme {
|
||||
pub tooltip: TooltipStyle,
|
||||
pub terminal: TerminalStyle,
|
||||
pub feedback: FeedbackStyle,
|
||||
pub welcome: WelcomeStyle,
|
||||
pub color_scheme: ColorScheme,
|
||||
}
|
||||
|
||||
@@ -49,6 +53,7 @@ pub struct ThemeMeta {
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Workspace {
|
||||
pub background: Color,
|
||||
pub blank_pane: BlankPaneStyle,
|
||||
pub titlebar: Titlebar,
|
||||
pub tab_bar: TabBar,
|
||||
pub pane_divider: Border,
|
||||
@@ -68,6 +73,16 @@ pub struct Workspace {
|
||||
pub drop_target_overlay_color: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct BlankPaneStyle {
|
||||
pub logo: IconStyle,
|
||||
pub logo_shadow: IconStyle,
|
||||
pub logo_container: ContainerStyle,
|
||||
pub keyboard_hints: ContainerStyle,
|
||||
pub keyboard_hint: Interactive<ContainedText>,
|
||||
pub keyboard_hint_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Titlebar {
|
||||
#[serde(flatten)]
|
||||
@@ -345,6 +360,7 @@ pub struct ProjectPanel {
|
||||
pub cut_entry: Interactive<ProjectPanelEntry>,
|
||||
pub filename_editor: FieldEditor,
|
||||
pub indent_width: f32,
|
||||
pub open_project_button: Interactive<ContainedText>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
@@ -850,13 +866,25 @@ pub struct FeedbackStyle {
|
||||
pub link_text_hover: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct WelcomeStyle {
|
||||
pub page_width: f32,
|
||||
pub logo: IconStyle,
|
||||
pub logo_subheading: ContainedText,
|
||||
pub usage_note: ContainedText,
|
||||
pub checkbox: CheckboxStyle,
|
||||
pub checkbox_container: ContainerStyle,
|
||||
pub button: Interactive<ContainedText>,
|
||||
pub button_group: ContainerStyle,
|
||||
pub heading_group: ContainerStyle,
|
||||
pub checkbox_group: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct ColorScheme {
|
||||
pub name: String,
|
||||
pub is_light: bool,
|
||||
|
||||
pub ramps: RampSet,
|
||||
|
||||
pub lowest: Layer,
|
||||
pub middle: Layer,
|
||||
pub highest: Layer,
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::{
|
||||
ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
|
||||
MouseEventHandler, ParentElement, Svg,
|
||||
},
|
||||
Action, Element, ElementBox, EventContext, RenderContext, View,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::ContainedText;
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct CheckboxStyle {
|
||||
pub icon: IconStyle,
|
||||
pub label: ContainedText,
|
||||
pub default: ContainerStyle,
|
||||
pub checked: ContainerStyle,
|
||||
pub hovered: ContainerStyle,
|
||||
pub hovered_and_checked: ContainerStyle,
|
||||
}
|
||||
|
||||
pub fn checkbox<T: 'static, V: View>(
|
||||
label: &'static str,
|
||||
style: &CheckboxStyle,
|
||||
checked: bool,
|
||||
cx: &mut RenderContext<V>,
|
||||
change: fn(checked: bool, cx: &mut EventContext) -> (),
|
||||
) -> MouseEventHandler<T> {
|
||||
let label = Label::new(label, style.label.text.clone())
|
||||
.contained()
|
||||
.with_style(style.label.container)
|
||||
.boxed();
|
||||
|
||||
checkbox_with_label(label, style, checked, cx, change)
|
||||
}
|
||||
|
||||
pub fn checkbox_with_label<T: 'static, V: View>(
|
||||
label: ElementBox,
|
||||
style: &CheckboxStyle,
|
||||
checked: bool,
|
||||
cx: &mut RenderContext<V>,
|
||||
change: fn(checked: bool, cx: &mut EventContext) -> (),
|
||||
) -> MouseEventHandler<T> {
|
||||
MouseEventHandler::<T>::new(0, cx, |state, _| {
|
||||
let indicator = if checked {
|
||||
icon(&style.icon)
|
||||
} else {
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(style.icon.dimensions.width)
|
||||
.with_height(style.icon.dimensions.height)
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_children([
|
||||
indicator
|
||||
.contained()
|
||||
.with_style(if checked {
|
||||
if state.hovered() {
|
||||
style.hovered_and_checked
|
||||
} else {
|
||||
style.checked
|
||||
}
|
||||
} else {
|
||||
if state.hovered() {
|
||||
style.hovered
|
||||
} else {
|
||||
style.default
|
||||
}
|
||||
})
|
||||
.boxed(),
|
||||
label,
|
||||
])
|
||||
.align_children_center()
|
||||
.boxed()
|
||||
})
|
||||
.on_click(gpui::MouseButton::Left, move |_, cx| change(!checked, cx))
|
||||
.with_cursor_style(gpui::CursorStyle::PointingHand)
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct IconStyle {
|
||||
pub color: Color,
|
||||
pub icon: String,
|
||||
pub dimensions: Dimensions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Dimensions {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
pub fn icon(style: &IconStyle) -> ConstrainedBox {
|
||||
Svg::new(style.icon.clone())
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.dimensions.width)
|
||||
.with_height(style.dimensions.height)
|
||||
}
|
||||
|
||||
pub fn keystroke_label<V: View>(
|
||||
label_text: &'static str,
|
||||
label_style: &ContainedText,
|
||||
keystroke_style: &ContainedText,
|
||||
action: Box<dyn Action>,
|
||||
cx: &mut RenderContext<V>,
|
||||
) -> Container {
|
||||
// FIXME: Put the theme in it's own global so we can
|
||||
// query the keystroke style on our own
|
||||
keystroke_label_for(
|
||||
cx.window_id(),
|
||||
cx.handle().id(),
|
||||
label_text,
|
||||
label_style,
|
||||
keystroke_style,
|
||||
action,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn keystroke_label_for(
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
label_text: &'static str,
|
||||
label_style: &ContainedText,
|
||||
keystroke_style: &ContainedText,
|
||||
action: Box<dyn Action>,
|
||||
) -> Container {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(label_text, label_style.text.clone())
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child({
|
||||
KeystrokeLabel::new(
|
||||
window_id,
|
||||
view_id,
|
||||
action,
|
||||
keystroke_style.container,
|
||||
keystroke_style.text.clone(),
|
||||
)
|
||||
.flex_float()
|
||||
.boxed()
|
||||
})
|
||||
.contained()
|
||||
.with_style(label_style.container)
|
||||
}
|
||||
@@ -47,12 +47,7 @@ impl ThemeSelector {
|
||||
let mut theme_names = registry
|
||||
.list(**cx.default_global::<StaffMode>())
|
||||
.collect::<Vec<_>>();
|
||||
theme_names.sort_unstable_by(|a, b| {
|
||||
a.is_light
|
||||
.cmp(&b.is_light)
|
||||
.reverse()
|
||||
.then(a.name.cmp(&b.name))
|
||||
});
|
||||
theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
|
||||
let matches = theme_names
|
||||
.iter()
|
||||
.map(|meta| StringMatch {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "welcome"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/welcome.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.38"
|
||||
log = "0.4"
|
||||
editor = { path = "../editor" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
db = { path = "../db" }
|
||||
install_cli = { path = "../install_cli" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
theme_selector = { path = "../theme_selector" }
|
||||
util = { path = "../util" }
|
||||
picker = { path = "../picker" }
|
||||
workspace = { path = "../workspace" }
|
||||
@@ -0,0 +1,175 @@
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{ChildView, Element as _, Label},
|
||||
AnyViewHandle, Entity, MutableAppContext, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::{settings_file::SettingsFile, BaseKeymap, Settings};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct BaseKeymapSelector {
|
||||
matches: Vec<StringMatch>,
|
||||
picker: ViewHandle<Picker<Self>>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
actions!(welcome, [ToggleBaseKeymapSelector]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
Picker::<BaseKeymapSelector>::init(cx);
|
||||
cx.add_action({
|
||||
move |workspace, _: &ToggleBaseKeymapSelector, cx| BaseKeymapSelector::toggle(workspace, cx)
|
||||
});
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
impl BaseKeymapSelector {
|
||||
fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
let this = cx.add_view(|cx| Self::new(cx));
|
||||
cx.subscribe(&this, Self::on_event).detach();
|
||||
this
|
||||
});
|
||||
}
|
||||
|
||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let base = cx.global::<Settings>().base_keymap;
|
||||
let selected_index = BaseKeymap::OPTIONS
|
||||
.iter()
|
||||
.position(|(_, value)| *value == base)
|
||||
.unwrap_or(0);
|
||||
|
||||
let this = cx.weak_handle();
|
||||
Self {
|
||||
picker: cx.add_view(|cx| Picker::new("Select a base keymap", this, cx)),
|
||||
matches: Vec::new(),
|
||||
selected_index,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
workspace: &mut Workspace,
|
||||
_: ViewHandle<BaseKeymapSelector>,
|
||||
event: &Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
Event::Dismissed => {
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for BaseKeymapSelector {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for BaseKeymapSelector {
|
||||
fn ui_name() -> &'static str {
|
||||
"BaseKeymapSelector"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||
ChildView::new(self.picker.clone(), cx).boxed()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for BaseKeymapSelector {
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
|
||||
let background = cx.background().clone();
|
||||
let candidates = BaseKeymap::names()
|
||||
.enumerate()
|
||||
.map(|(id, name)| StringMatchCandidate {
|
||||
id,
|
||||
char_bag: name.into(),
|
||||
string: name.into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
background,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.matches = matches;
|
||||
this.selected_index = this
|
||||
.selected_index
|
||||
.min(this.matches.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(selection) = self.matches.get(self.selected_index) {
|
||||
let base_keymap = BaseKeymap::from_names(&selection.string);
|
||||
SettingsFile::update(cx, move |settings| settings.base_keymap = Some(base_keymap));
|
||||
}
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Dismissed)
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut gpui::MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> gpui::ElementBox {
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
let keymap_match = &self.matches[ix];
|
||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||
|
||||
Label::new(keymap_match.string.clone(), style.label.clone())
|
||||
.with_highlights(keymap_match.positions.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
mod base_keymap_picker;
|
||||
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
elements::{Flex, Label, MouseEventHandler, ParentElement},
|
||||
Action, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext,
|
||||
Subscription, View, ViewContext,
|
||||
};
|
||||
use settings::{settings_file::SettingsFile, Settings};
|
||||
|
||||
use workspace::{
|
||||
item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
|
||||
WorkspaceId,
|
||||
};
|
||||
|
||||
use crate::base_keymap_picker::ToggleBaseKeymapSelector;
|
||||
|
||||
pub const FIRST_OPEN: &str = "first_open";
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| {
|
||||
let welcome_page = cx.add_view(WelcomePage::new);
|
||||
workspace.add_item(Box::new(welcome_page), cx)
|
||||
});
|
||||
|
||||
base_keymap_picker::init(cx);
|
||||
}
|
||||
|
||||
pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
open_new(&app_state, cx, |workspace, cx| {
|
||||
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
||||
let welcome_page = cx.add_view(|cx| WelcomePage::new(cx));
|
||||
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
|
||||
cx.focus(welcome_page);
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
db::write_and_log(cx, || {
|
||||
KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
|
||||
});
|
||||
}
|
||||
|
||||
pub struct WelcomePage {
|
||||
_settings_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl Entity for WelcomePage {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for WelcomePage {
|
||||
fn ui_name() -> &'static str {
|
||||
"WelcomePage"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
|
||||
let self_handle = cx.handle();
|
||||
let settings = cx.global::<Settings>();
|
||||
let theme = settings.theme.clone();
|
||||
|
||||
let width = theme.welcome.page_width;
|
||||
|
||||
let (diagnostics, metrics) = {
|
||||
let telemetry = settings.telemetry();
|
||||
(telemetry.diagnostics(), telemetry.metrics())
|
||||
};
|
||||
|
||||
enum Metrics {}
|
||||
enum Diagnostics {}
|
||||
|
||||
PaneBackdrop::new(
|
||||
self_handle.id(),
|
||||
Flex::column()
|
||||
.with_children([
|
||||
Flex::column()
|
||||
.with_children([
|
||||
theme::ui::icon(&theme.welcome.logo)
|
||||
.aligned()
|
||||
.contained()
|
||||
.aligned()
|
||||
.boxed(),
|
||||
Label::new(
|
||||
"Code at the speed of thought",
|
||||
theme.welcome.logo_subheading.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.welcome.logo_subheading.container)
|
||||
.boxed(),
|
||||
])
|
||||
.contained()
|
||||
.with_style(theme.welcome.heading_group)
|
||||
.constrained()
|
||||
.with_width(width)
|
||||
.boxed(),
|
||||
Flex::column()
|
||||
.with_children([
|
||||
self.render_cta_button(
|
||||
"Choose a theme",
|
||||
theme_selector::Toggle,
|
||||
width,
|
||||
cx,
|
||||
),
|
||||
self.render_cta_button(
|
||||
"Choose a keymap",
|
||||
ToggleBaseKeymapSelector,
|
||||
width,
|
||||
cx,
|
||||
),
|
||||
self.render_cta_button(
|
||||
"Install the CLI",
|
||||
install_cli::Install,
|
||||
width,
|
||||
cx,
|
||||
),
|
||||
])
|
||||
.contained()
|
||||
.with_style(theme.welcome.button_group)
|
||||
.constrained()
|
||||
.with_width(width)
|
||||
.boxed(),
|
||||
Flex::column()
|
||||
.with_children([
|
||||
theme::ui::checkbox_with_label::<Metrics, Self>(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
Label::new(
|
||||
"Send anonymous usage data",
|
||||
theme.welcome.checkbox.label.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.welcome.checkbox.label.container)
|
||||
.boxed(),
|
||||
Label::new(
|
||||
"Help > View Telemetry",
|
||||
theme.welcome.usage_note.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.welcome.usage_note.container)
|
||||
.boxed(),
|
||||
])
|
||||
.boxed(),
|
||||
&theme.welcome.checkbox,
|
||||
metrics,
|
||||
cx,
|
||||
|checked, cx| {
|
||||
SettingsFile::update(cx, move |file| {
|
||||
file.telemetry.set_metrics(checked)
|
||||
})
|
||||
},
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.welcome.checkbox_container)
|
||||
.boxed(),
|
||||
theme::ui::checkbox::<Diagnostics, Self>(
|
||||
"Send crash reports",
|
||||
&theme.welcome.checkbox,
|
||||
diagnostics,
|
||||
cx,
|
||||
|checked, cx| {
|
||||
SettingsFile::update(cx, move |file| {
|
||||
file.telemetry.set_diagnostics(checked)
|
||||
})
|
||||
},
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.welcome.checkbox_container)
|
||||
.boxed(),
|
||||
])
|
||||
.contained()
|
||||
.with_style(theme.welcome.checkbox_group)
|
||||
.constrained()
|
||||
.with_width(width)
|
||||
.boxed(),
|
||||
])
|
||||
.constrained()
|
||||
.with_max_width(width)
|
||||
.contained()
|
||||
.with_uniform_padding(10.)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl WelcomePage {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let handle = cx.weak_handle();
|
||||
|
||||
let settings_subscription = cx.observe_global::<Settings, _>(move |cx| {
|
||||
if let Some(handle) = handle.upgrade(cx) {
|
||||
handle.update(cx, |_, cx| cx.notify())
|
||||
}
|
||||
});
|
||||
|
||||
WelcomePage {
|
||||
_settings_subscription: settings_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_cta_button<L, A>(
|
||||
&self,
|
||||
label: L,
|
||||
action: A,
|
||||
width: f32,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox
|
||||
where
|
||||
L: Into<Cow<'static, str>>,
|
||||
A: 'static + Action + Clone,
|
||||
{
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
MouseEventHandler::<A>::new(0, cx, |state, _| {
|
||||
let style = theme.welcome.button.style_for(state, false);
|
||||
Label::new(label, style.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_max_width(width)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(action.clone())
|
||||
})
|
||||
.with_cursor_style(gpui::CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
// fn render_settings_checkbox<T: 'static>(
|
||||
// &self,
|
||||
// label: &'static str,
|
||||
// style: &CheckboxStyle,
|
||||
// checked: bool,
|
||||
// cx: &mut RenderContext<Self>,
|
||||
// set_value: fn(&mut SettingsFileContent, checked: bool) -> (),
|
||||
// ) -> ElementBox {
|
||||
// MouseEventHandler::<T>::new(0, cx, |state, _| {
|
||||
// let indicator = if checked {
|
||||
// Svg::new(style.check_icon.clone())
|
||||
// .with_color(style.check_icon_color)
|
||||
// .constrained()
|
||||
// } else {
|
||||
// Empty::new().constrained()
|
||||
// };
|
||||
|
||||
// Flex::row()
|
||||
// .with_children([
|
||||
// indicator
|
||||
// .with_width(style.width)
|
||||
// .with_height(style.height)
|
||||
// .contained()
|
||||
// .with_style(if checked {
|
||||
// if state.hovered() {
|
||||
// style.hovered_and_checked
|
||||
// } else {
|
||||
// style.checked
|
||||
// }
|
||||
// } else {
|
||||
// if state.hovered() {
|
||||
// style.hovered
|
||||
// } else {
|
||||
// style.default
|
||||
// }
|
||||
// })
|
||||
// .boxed(),
|
||||
// Label::new(label, style.label.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.label.container)
|
||||
// .boxed(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .boxed()
|
||||
// })
|
||||
// .on_click(gpui::MouseButton::Left, move |_, cx| {
|
||||
// SettingsFile::update(cx, move |content| set_value(content, !checked))
|
||||
// })
|
||||
// .with_cursor_style(gpui::CursorStyle::PointingHand)
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .boxed()
|
||||
// }
|
||||
}
|
||||
|
||||
impl Item for WelcomePage {
|
||||
fn tab_content(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
_cx: &gpui::AppContext,
|
||||
) -> gpui::ElementBox {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new("Welcome to Zed!", style.label.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn show_toolbar(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Self> {
|
||||
Some(WelcomePage::new(cx))
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ context_menu = { path = "../context_menu" }
|
||||
drag_and_drop = { path = "../drag_and_drop" }
|
||||
fs = { path = "../fs" }
|
||||
gpui = { path = "../gpui" }
|
||||
install_cli = { path = "../install_cli" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
project = { path = "../project" }
|
||||
|
||||
@@ -13,7 +13,7 @@ use gpui::{
|
||||
use settings::{DockAnchor, Settings};
|
||||
use theme::Theme;
|
||||
|
||||
use crate::{sidebar::SidebarSide, ItemHandle, Pane, Workspace};
|
||||
use crate::{sidebar::SidebarSide, BackgroundActions, ItemHandle, Pane, Workspace};
|
||||
pub use toggle_dock_button::ToggleDockButton;
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize)]
|
||||
@@ -39,20 +39,24 @@ impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]);
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Dock::focus_dock);
|
||||
cx.add_action(Dock::hide_dock);
|
||||
cx.add_action(Dock::move_dock);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, &MoveDock(dock_anchor), cx: &mut ViewContext<Workspace>| {
|
||||
Dock::move_dock(workspace, dock_anchor, true, cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext<Workspace>| {
|
||||
Dock::move_dock(workspace, &MoveDock(DockAnchor::Right), cx)
|
||||
Dock::move_dock(workspace, DockAnchor::Right, true, cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext<Workspace>| {
|
||||
Dock::move_dock(workspace, &MoveDock(DockAnchor::Bottom), cx)
|
||||
Dock::move_dock(workspace, DockAnchor::Bottom, true, cx)
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext<Workspace>| {
|
||||
Dock::move_dock(workspace, &MoveDock(DockAnchor::Expanded), cx)
|
||||
Dock::move_dock(workspace, DockAnchor::Expanded, true, cx)
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
@@ -177,12 +181,21 @@ pub struct Dock {
|
||||
|
||||
impl Dock {
|
||||
pub fn new(
|
||||
workspace_id: usize,
|
||||
default_item_factory: DockDefaultItemFactory,
|
||||
background_actions: BackgroundActions,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Self {
|
||||
let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
|
||||
|
||||
let pane = cx.add_view(|cx| Pane::new(Some(position.anchor()), cx));
|
||||
let pane = cx.add_view(|cx| {
|
||||
Pane::new(
|
||||
workspace_id,
|
||||
Some(position.anchor()),
|
||||
background_actions,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.set_active(false, cx);
|
||||
});
|
||||
@@ -215,6 +228,7 @@ impl Dock {
|
||||
pub(crate) fn set_dock_position(
|
||||
workspace: &mut Workspace,
|
||||
new_position: DockPosition,
|
||||
focus: bool,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
workspace.dock.position = new_position;
|
||||
@@ -235,19 +249,23 @@ impl Dock {
|
||||
let pane = workspace.dock.pane.clone();
|
||||
if pane.read(cx).items().next().is_none() {
|
||||
if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) {
|
||||
Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
|
||||
Pane::add_item(workspace, &pane, item_to_add, focus, focus, None, cx);
|
||||
} else {
|
||||
workspace.dock.position = workspace.dock.position.hide();
|
||||
}
|
||||
} else {
|
||||
cx.focus(pane);
|
||||
if focus {
|
||||
cx.focus(pane);
|
||||
}
|
||||
}
|
||||
} else if let Some(last_active_center_pane) = workspace
|
||||
.last_active_center_pane
|
||||
.as_ref()
|
||||
.and_then(|pane| pane.upgrade(cx))
|
||||
{
|
||||
cx.focus(last_active_center_pane);
|
||||
if focus {
|
||||
cx.focus(last_active_center_pane);
|
||||
}
|
||||
}
|
||||
cx.emit(crate::Event::DockAnchorChanged);
|
||||
workspace.serialize_workspace(cx);
|
||||
@@ -255,11 +273,11 @@ impl Dock {
|
||||
}
|
||||
|
||||
pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
|
||||
Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
|
||||
}
|
||||
|
||||
pub fn show(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
|
||||
pub fn show(workspace: &mut Workspace, focus: bool, cx: &mut ViewContext<Workspace>) {
|
||||
Self::set_dock_position(workspace, workspace.dock.position.show(), focus, cx);
|
||||
}
|
||||
|
||||
pub fn hide_on_sidebar_shown(
|
||||
@@ -275,19 +293,20 @@ impl Dock {
|
||||
}
|
||||
|
||||
fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
|
||||
Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
|
||||
Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx);
|
||||
}
|
||||
|
||||
fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
|
||||
Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
|
||||
Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
|
||||
}
|
||||
|
||||
fn move_dock(
|
||||
pub fn move_dock(
|
||||
workspace: &mut Workspace,
|
||||
&MoveDock(new_anchor): &MoveDock,
|
||||
new_anchor: DockAnchor,
|
||||
focus: bool,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx);
|
||||
Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), focus, cx);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
@@ -482,6 +501,7 @@ mod tests {
|
||||
0,
|
||||
project.clone(),
|
||||
default_item_factory,
|
||||
|| &[],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -610,7 +630,14 @@ mod tests {
|
||||
cx.update(|cx| init(cx));
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, default_item_factory, cx)
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project,
|
||||
default_item_factory,
|
||||
|| &[],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
|
||||
@@ -151,6 +151,9 @@ pub trait Item: View {
|
||||
"deserialize() must be implemented if serialized_item_kind() returns Some(_)"
|
||||
)
|
||||
}
|
||||
fn show_toolbar(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ItemHandle: 'static + fmt::Debug {
|
||||
@@ -213,6 +216,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||
fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
|
||||
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>>;
|
||||
fn serialized_item_kind(&self) -> Option<&'static str>;
|
||||
fn show_toolbar(&self, cx: &AppContext) -> bool;
|
||||
}
|
||||
|
||||
pub trait WeakItemHandle {
|
||||
@@ -591,6 +595,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
fn serialized_item_kind(&self) -> Option<&'static str> {
|
||||
T::serialized_item_kind()
|
||||
}
|
||||
|
||||
fn show_toolbar(&self, cx: &AppContext) -> bool {
|
||||
self.read(cx).show_toolbar()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn ItemHandle>> for AnyViewHandle {
|
||||
|
||||
@@ -122,6 +122,8 @@ impl Workspace {
|
||||
|
||||
pub mod simple_message_notification {
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
|
||||
@@ -153,9 +155,9 @@ pub mod simple_message_notification {
|
||||
}
|
||||
|
||||
pub struct MessageNotification {
|
||||
message: String,
|
||||
message: Cow<'static, str>,
|
||||
click_action: Option<Box<dyn Action>>,
|
||||
click_message: Option<String>,
|
||||
click_message: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
pub enum MessageNotificationEvent {
|
||||
@@ -167,23 +169,23 @@ pub mod simple_message_notification {
|
||||
}
|
||||
|
||||
impl MessageNotification {
|
||||
pub fn new_message<S: AsRef<str>>(message: S) -> MessageNotification {
|
||||
pub fn new_message<S: Into<Cow<'static, str>>>(message: S) -> MessageNotification {
|
||||
Self {
|
||||
message: message.as_ref().to_string(),
|
||||
message: message.into(),
|
||||
click_action: None,
|
||||
click_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<S1: AsRef<str>, A: Action, S2: AsRef<str>>(
|
||||
pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>(
|
||||
message: S1,
|
||||
click_action: A,
|
||||
click_message: S2,
|
||||
) -> Self {
|
||||
Self {
|
||||
message: message.as_ref().to_string(),
|
||||
message: message.into(),
|
||||
click_action: Some(Box::new(click_action) as Box<dyn Action>),
|
||||
click_message: Some(click_message.as_ref().to_string()),
|
||||
click_message: Some(click_message.into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +212,8 @@ pub mod simple_message_notification {
|
||||
let click_message = self.click_message.as_ref().map(|message| message.clone());
|
||||
let message = self.message.clone();
|
||||
|
||||
let has_click_action = click_action.is_some();
|
||||
|
||||
MouseEventHandler::<MessageNotificationTag>::new(0, cx, |state, cx| {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
@@ -243,6 +247,7 @@ pub mod simple_message_notification {
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(CancelMessageNotification)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(
|
||||
@@ -272,12 +277,19 @@ pub mod simple_message_notification {
|
||||
.contained()
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
// Since we're not using a proper overlay, we have to capture these extra events
|
||||
.on_down(MouseButton::Left, |_, _| {})
|
||||
.on_up(MouseButton::Left, |_, _| {})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
if let Some(click_action) = click_action.as_ref() {
|
||||
cx.dispatch_any_action(click_action.boxed_clone())
|
||||
}
|
||||
})
|
||||
.with_cursor_style(if has_click_action {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::Arrow
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
+119
-22
@@ -24,8 +24,8 @@ use gpui::{
|
||||
keymap_matcher::KeymapContext,
|
||||
platform::{CursorStyle, NavigationDirection},
|
||||
Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
|
||||
ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
ModelHandle, MouseButton, MouseRegion, MutableAppContext, PromptLevel, Quad, RenderContext,
|
||||
Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use serde::Deserialize;
|
||||
@@ -110,6 +110,8 @@ impl_internal_actions!(
|
||||
|
||||
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
|
||||
|
||||
pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)];
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
|
||||
pane.activate_item(action.0, true, true, cx);
|
||||
@@ -215,6 +217,8 @@ pub struct Pane {
|
||||
toolbar: ViewHandle<Toolbar>,
|
||||
tab_bar_context_menu: ViewHandle<ContextMenu>,
|
||||
docked: Option<DockAnchor>,
|
||||
_background_actions: BackgroundActions,
|
||||
_workspace_id: usize,
|
||||
}
|
||||
|
||||
pub struct ItemNavHistory {
|
||||
@@ -271,7 +275,12 @@ enum ItemType {
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(
|
||||
workspace_id: usize,
|
||||
docked: Option<DockAnchor>,
|
||||
background_actions: BackgroundActions,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let handle = cx.weak_handle();
|
||||
let context_menu = cx.add_view(ContextMenu::new);
|
||||
Self {
|
||||
@@ -292,6 +301,8 @@ impl Pane {
|
||||
toolbar: cx.add_view(|_| Toolbar::new(handle)),
|
||||
tab_bar_context_menu: context_menu,
|
||||
docked,
|
||||
_background_actions: background_actions,
|
||||
_workspace_id: workspace_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1415,6 +1426,14 @@ impl Pane {
|
||||
.flex(1., false)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_blank_pane(&mut self, theme: &Theme, _cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let background = theme.workspace.background;
|
||||
Empty::new()
|
||||
.contained()
|
||||
.with_background_color(background)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Pane {
|
||||
@@ -1485,11 +1504,12 @@ impl View for Pane {
|
||||
cx,
|
||||
{
|
||||
let toolbar = self.toolbar.clone();
|
||||
let toolbar_hidden = toolbar.read(cx).hidden();
|
||||
move |_, cx| {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
ChildView::new(&toolbar, cx).expanded().boxed(),
|
||||
)
|
||||
.with_children((!toolbar_hidden).then(|| {
|
||||
ChildView::new(&toolbar, cx).expanded().boxed()
|
||||
}))
|
||||
.with_child(
|
||||
ChildView::new(active_item, cx)
|
||||
.flex(1., true)
|
||||
@@ -1507,11 +1527,8 @@ impl View for Pane {
|
||||
enum EmptyPane {}
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
dragged_item_receiver::<EmptyPane, _>(0, 0, false, None, cx, |_, _| {
|
||||
Empty::new()
|
||||
.contained()
|
||||
.with_background_color(theme.workspace.background)
|
||||
.boxed()
|
||||
dragged_item_receiver::<EmptyPane, _>(0, 0, false, None, cx, |_, cx| {
|
||||
self.render_blank_pane(&theme, cx)
|
||||
})
|
||||
.on_down(MouseButton::Left, |_, cx| {
|
||||
cx.focus_parent_view();
|
||||
@@ -1705,6 +1722,93 @@ impl NavHistory {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PaneBackdrop {
|
||||
child_view: usize,
|
||||
child: ElementBox,
|
||||
}
|
||||
impl PaneBackdrop {
|
||||
pub fn new(pane_item_view: usize, child: ElementBox) -> Self {
|
||||
PaneBackdrop {
|
||||
child,
|
||||
child_view: pane_item_view,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for PaneBackdrop {
|
||||
type LayoutState = ();
|
||||
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
cx: &mut gpui::LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let size = self.child.layout(constraint, cx);
|
||||
(size, ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
cx: &mut gpui::PaintContext,
|
||||
) -> Self::PaintState {
|
||||
let background = cx.global::<Settings>().theme.editor.background;
|
||||
|
||||
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
cx.scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::new(bounds.origin(), bounds.size()),
|
||||
background: Some(background),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let child_view_id = self.child_view;
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::new::<Self>(child_view_id, 0, visible_bounds).on_down(
|
||||
gpui::MouseButton::Left,
|
||||
move |_, cx| {
|
||||
let window_id = cx.window_id;
|
||||
cx.focus(window_id, Some(child_view_id))
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
cx.paint_layer(Some(bounds), |cx| {
|
||||
self.child.paint(bounds.origin(), visible_bounds, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: std::ops::Range<usize>,
|
||||
_bounds: RectF,
|
||||
_visible_bounds: RectF,
|
||||
_layout: &Self::LayoutState,
|
||||
_paint: &Self::PaintState,
|
||||
cx: &gpui::MeasurementContext,
|
||||
) -> Option<RectF> {
|
||||
self.child.rect_for_text_range(range_utf16, cx)
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
_bounds: RectF,
|
||||
_layout: &Self::LayoutState,
|
||||
_paint: &Self::PaintState,
|
||||
cx: &gpui::DebugContext,
|
||||
) -> serde_json::Value {
|
||||
gpui::json::json!({
|
||||
"type": "Pane Back Drop",
|
||||
"view": self.child_view,
|
||||
"child": self.child.debug(cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
@@ -1721,9 +1825,7 @@ mod tests {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// 1. Add with a destination index
|
||||
@@ -1811,9 +1913,7 @@ mod tests {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// 1. Add with a destination index
|
||||
@@ -1889,9 +1989,7 @@ mod tests {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// singleton view
|
||||
@@ -2000,8 +2098,7 @@ mod tests {
|
||||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(None, 0, project, |_, _| unimplemented!(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
add_labled_item(&workspace, &pane, "A", cx);
|
||||
|
||||
@@ -42,6 +42,7 @@ pub enum ToolbarItemLocation {
|
||||
|
||||
pub struct Toolbar {
|
||||
active_pane_item: Option<Box<dyn ItemHandle>>,
|
||||
hidden: bool,
|
||||
pane: WeakViewHandle<Pane>,
|
||||
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
||||
}
|
||||
@@ -211,6 +212,7 @@ impl Toolbar {
|
||||
active_pane_item: None,
|
||||
pane,
|
||||
items: Default::default(),
|
||||
hidden: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +245,12 @@ impl Toolbar {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.active_pane_item = pane_item.map(|item| item.boxed_clone());
|
||||
self.hidden = self
|
||||
.active_pane_item
|
||||
.as_ref()
|
||||
.map(|item| !item.show_toolbar(cx))
|
||||
.unwrap_or(false);
|
||||
|
||||
for (toolbar_item, current_location) in self.items.iter_mut() {
|
||||
let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
|
||||
if new_location != *current_location {
|
||||
@@ -257,6 +265,10 @@ impl Toolbar {
|
||||
.iter()
|
||||
.find_map(|(item, _)| item.to_any().downcast())
|
||||
}
|
||||
|
||||
pub fn hidden(&self) -> bool {
|
||||
self.hidden
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
|
||||
|
||||
+232
-115
@@ -17,7 +17,7 @@ mod toolbar;
|
||||
|
||||
pub use smallvec;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use call::ActiveCall;
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
@@ -44,7 +44,8 @@ use gpui::{
|
||||
platform::{CursorStyle, WindowOptions},
|
||||
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext,
|
||||
SizeConstraint, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowBounds,
|
||||
SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
WindowBounds,
|
||||
};
|
||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
|
||||
use language::LanguageRegistry;
|
||||
@@ -65,7 +66,7 @@ use crate::{
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use log::{error, warn};
|
||||
use notifications::NotificationHandle;
|
||||
use notifications::{NotificationHandle, NotifyResultExt};
|
||||
pub use pane::*;
|
||||
pub use pane_group::*;
|
||||
use persistence::{model::SerializedItem, DB};
|
||||
@@ -118,7 +119,8 @@ actions!(
|
||||
NewTerminal,
|
||||
NewSearch,
|
||||
Feedback,
|
||||
Restart
|
||||
Restart,
|
||||
Welcome
|
||||
]
|
||||
);
|
||||
|
||||
@@ -187,21 +189,66 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
dock::init(cx);
|
||||
notifications::init(cx);
|
||||
|
||||
cx.add_global_action(open);
|
||||
cx.add_global_action(|_: &Open, cx: &mut MutableAppContext| {
|
||||
let mut paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
if let Some(paths) = paths.recv().await.flatten() {
|
||||
cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
cx.add_action(|_, _: &Open, cx: &mut ViewContext<Workspace>| {
|
||||
let mut paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
let handle = cx.handle().downgrade();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if let Some(paths) = paths.recv().await.flatten() {
|
||||
cx.update(|cx| {
|
||||
cx.dispatch_action_at(handle.window_id(), handle.id(), OpenPaths { paths })
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
cx.add_global_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |action: &OpenPaths, cx: &mut MutableAppContext| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
open_paths(&action.paths, &app_state, cx).detach();
|
||||
open_paths(&action.paths, &app_state, None, cx).detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.add_global_action({
|
||||
cx.add_async_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_: &NewFile, cx: &mut MutableAppContext| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
open_new(&app_state, cx).detach();
|
||||
move |workspace, action: &OpenPaths, cx: &mut ViewContext<Workspace>| {
|
||||
if !workspace.project().read(cx).is_local() {
|
||||
cx.propagate_action();
|
||||
return None;
|
||||
}
|
||||
|
||||
let app_state = app_state.upgrade()?;
|
||||
let window_id = cx.window_id();
|
||||
let action = action.clone();
|
||||
let close = workspace.prepare_to_close(false, cx);
|
||||
|
||||
Some(cx.spawn_weak(|_, mut cx| async move {
|
||||
let can_close = close.await?;
|
||||
if can_close {
|
||||
cx.update(|cx| open_paths(&action.paths, &app_state, Some(window_id), cx))
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
@@ -209,7 +256,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_: &NewWindow, cx: &mut MutableAppContext| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
open_new(&app_state, cx).detach();
|
||||
open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)).detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -275,6 +322,31 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let err = install_cli::install_cli(&cx)
|
||||
.await
|
||||
.context("Failed to create CLI symlink");
|
||||
|
||||
cx.update(|cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if matches!(err, Err(_)) {
|
||||
err.notify_err(workspace, cx);
|
||||
} else {
|
||||
workspace.show_notification(1, cx, |cx| {
|
||||
cx.add_view(|_| {
|
||||
MessageNotification::new_message(
|
||||
"Successfully installed the `zed` binary",
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
let client = &app_state.client;
|
||||
client.add_view_request_handler(Workspace::handle_follow);
|
||||
client.add_view_message_handler(Workspace::handle_unfollow);
|
||||
@@ -360,6 +432,7 @@ pub struct AppState {
|
||||
fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
|
||||
pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
|
||||
pub dock_default_item_factory: DockDefaultItemFactory,
|
||||
pub background_actions: BackgroundActions,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -382,7 +455,8 @@ impl AppState {
|
||||
user_store,
|
||||
initialize_workspace: |_, _, _| {},
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
dock_default_item_factory: |_, _| unimplemented!(),
|
||||
dock_default_item_factory: |_, _| None,
|
||||
background_actions: || &[],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -470,6 +544,8 @@ pub struct Workspace {
|
||||
active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
|
||||
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
|
||||
database_id: WorkspaceId,
|
||||
background_actions: BackgroundActions,
|
||||
_window_subscriptions: [Subscription; 3],
|
||||
_apply_leader_updates: Task<Result<()>>,
|
||||
_observe_current_user: Task<()>,
|
||||
}
|
||||
@@ -499,12 +575,9 @@ impl Workspace {
|
||||
workspace_id: WorkspaceId,
|
||||
project: ModelHandle<Project>,
|
||||
dock_default_factory: DockDefaultItemFactory,
|
||||
background_actions: BackgroundActions,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
|
||||
|
||||
cx.observe_window_activation(Self::on_window_activation_changed)
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
cx.subscribe(&project, move |this, _, event, cx| {
|
||||
match event {
|
||||
@@ -533,7 +606,10 @@ impl Workspace {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let center_pane = cx.add_view(|cx| Pane::new(None, cx));
|
||||
let weak_handle = cx.weak_handle();
|
||||
|
||||
let center_pane =
|
||||
cx.add_view(|cx| Pane::new(weak_handle.id(), None, background_actions, cx));
|
||||
let pane_id = center_pane.id();
|
||||
cx.subscribe(¢er_pane, move |this, _, event, cx| {
|
||||
this.handle_pane_event(pane_id, event, cx)
|
||||
@@ -541,7 +617,12 @@ impl Workspace {
|
||||
.detach();
|
||||
cx.focus(¢er_pane);
|
||||
cx.emit(Event::PaneAdded(center_pane.clone()));
|
||||
let dock = Dock::new(dock_default_factory, cx);
|
||||
let dock = Dock::new(
|
||||
weak_handle.id(),
|
||||
dock_default_factory,
|
||||
background_actions,
|
||||
cx,
|
||||
);
|
||||
let dock_pane = dock.pane().clone();
|
||||
|
||||
let fs = project.read(cx).fs().clone();
|
||||
@@ -564,7 +645,6 @@ impl Workspace {
|
||||
}
|
||||
});
|
||||
let handle = cx.handle();
|
||||
let weak_handle = cx.weak_handle();
|
||||
|
||||
// All leader updates are enqueued and then processed in a single task, so
|
||||
// that each asynchronous operation can be run in order.
|
||||
@@ -611,6 +691,28 @@ impl Workspace {
|
||||
active_call = Some((call, subscriptions));
|
||||
}
|
||||
|
||||
let subscriptions = [
|
||||
cx.observe_fullscreen(|_, _, cx| cx.notify()),
|
||||
cx.observe_window_activation(Self::on_window_activation_changed),
|
||||
cx.observe_window_bounds(move |_, mut bounds, display, cx| {
|
||||
// Transform fixed bounds to be stored in terms of the containing display
|
||||
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
||||
if let Some(screen) = cx.platform().screen_by_id(display) {
|
||||
let screen_bounds = screen.bounds();
|
||||
window_bounds
|
||||
.set_origin_x(window_bounds.origin_x() - screen_bounds.origin_x());
|
||||
window_bounds
|
||||
.set_origin_y(window_bounds.origin_y() - screen_bounds.origin_y());
|
||||
bounds = WindowBounds::Fixed(window_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
cx.background()
|
||||
.spawn(DB.set_window_bounds(workspace_id, bounds, display))
|
||||
.detach_and_log_err(cx);
|
||||
}),
|
||||
];
|
||||
|
||||
let mut this = Workspace {
|
||||
modal: None,
|
||||
weak_self: weak_handle.clone(),
|
||||
@@ -639,9 +741,11 @@ impl Workspace {
|
||||
window_edited: false,
|
||||
active_call,
|
||||
database_id: workspace_id,
|
||||
background_actions,
|
||||
_observe_current_user,
|
||||
_apply_leader_updates,
|
||||
leader_updates_tx,
|
||||
_window_subscriptions: subscriptions,
|
||||
};
|
||||
this.project_remote_id_changed(project.read(cx).remote_id(), cx);
|
||||
cx.defer(|this, cx| this.update_window_title(cx));
|
||||
@@ -650,6 +754,10 @@ impl Workspace {
|
||||
cx.defer(move |_, cx| {
|
||||
Self::load_from_serialized_workspace(weak_handle, serialized_workspace, cx)
|
||||
});
|
||||
} else {
|
||||
if cx.global::<Settings>().default_dock_anchor != DockAnchor::Expanded {
|
||||
Dock::show(&mut this, false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
this
|
||||
@@ -658,6 +766,7 @@ impl Workspace {
|
||||
fn new_local(
|
||||
abs_paths: Vec<PathBuf>,
|
||||
app_state: Arc<AppState>,
|
||||
requesting_window_id: Option<usize>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<(
|
||||
ViewHandle<Workspace>,
|
||||
@@ -713,73 +822,65 @@ impl Workspace {
|
||||
))
|
||||
});
|
||||
|
||||
let (bounds, display) = if let Some(bounds) = window_bounds_override {
|
||||
(Some(bounds), None)
|
||||
} else {
|
||||
serialized_workspace
|
||||
.as_ref()
|
||||
.and_then(|serialized_workspace| {
|
||||
let display = serialized_workspace.display?;
|
||||
let mut bounds = serialized_workspace.bounds?;
|
||||
|
||||
// Stored bounds are relative to the containing display.
|
||||
// So convert back to global coordinates if that screen still exists
|
||||
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
||||
if let Some(screen) = cx.platform().screen_by_id(display) {
|
||||
let screen_bounds = screen.bounds();
|
||||
window_bounds.set_origin_x(
|
||||
window_bounds.origin_x() + screen_bounds.origin_x(),
|
||||
);
|
||||
window_bounds.set_origin_y(
|
||||
window_bounds.origin_y() + screen_bounds.origin_y(),
|
||||
);
|
||||
bounds = WindowBounds::Fixed(window_bounds);
|
||||
} else {
|
||||
// Screen no longer exists. Return none here.
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some((bounds, display))
|
||||
})
|
||||
.unzip()
|
||||
};
|
||||
|
||||
// Use the serialized workspace to construct the new window
|
||||
let (_, workspace) = cx.add_window(
|
||||
(app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
|
||||
|cx| {
|
||||
let build_workspace =
|
||||
|cx: &mut ViewContext<Workspace>,
|
||||
serialized_workspace: Option<SerializedWorkspace>| {
|
||||
let mut workspace = Workspace::new(
|
||||
serialized_workspace,
|
||||
workspace_id,
|
||||
project_handle,
|
||||
app_state.dock_default_item_factory,
|
||||
app_state.background_actions,
|
||||
cx,
|
||||
);
|
||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||
cx.observe_window_bounds(move |_, mut bounds, display, cx| {
|
||||
// Transform fixed bounds to be stored in terms of the containing display
|
||||
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
||||
if let Some(screen) = cx.platform().screen_by_id(display) {
|
||||
let screen_bounds = screen.bounds();
|
||||
window_bounds.set_origin_x(
|
||||
window_bounds.origin_x() - screen_bounds.origin_x(),
|
||||
);
|
||||
window_bounds.set_origin_y(
|
||||
window_bounds.origin_y() - screen_bounds.origin_y(),
|
||||
);
|
||||
bounds = WindowBounds::Fixed(window_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
cx.background()
|
||||
.spawn(DB.set_window_bounds(workspace_id, bounds, display))
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.detach();
|
||||
workspace
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
let workspace = if let Some(window_id) = requesting_window_id {
|
||||
cx.update(|cx| {
|
||||
cx.replace_root_view(window_id, |cx| build_workspace(cx, serialized_workspace))
|
||||
})
|
||||
} else {
|
||||
let (bounds, display) = if let Some(bounds) = window_bounds_override {
|
||||
(Some(bounds), None)
|
||||
} else {
|
||||
serialized_workspace
|
||||
.as_ref()
|
||||
.and_then(|serialized_workspace| {
|
||||
let display = serialized_workspace.display?;
|
||||
let mut bounds = serialized_workspace.bounds?;
|
||||
|
||||
// Stored bounds are relative to the containing display.
|
||||
// So convert back to global coordinates if that screen still exists
|
||||
if let WindowBounds::Fixed(mut window_bounds) = bounds {
|
||||
if let Some(screen) = cx.platform().screen_by_id(display) {
|
||||
let screen_bounds = screen.bounds();
|
||||
window_bounds.set_origin_x(
|
||||
window_bounds.origin_x() + screen_bounds.origin_x(),
|
||||
);
|
||||
window_bounds.set_origin_y(
|
||||
window_bounds.origin_y() + screen_bounds.origin_y(),
|
||||
);
|
||||
bounds = WindowBounds::Fixed(window_bounds);
|
||||
} else {
|
||||
// Screen no longer exists. Return none here.
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some((bounds, display))
|
||||
})
|
||||
.unzip()
|
||||
};
|
||||
|
||||
// Use the serialized workspace to construct the new window
|
||||
cx.add_window(
|
||||
(app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
|
||||
|cx| build_workspace(cx, serialized_workspace),
|
||||
)
|
||||
.1
|
||||
};
|
||||
|
||||
notify_if_database_failed(&workspace, &mut cx);
|
||||
|
||||
@@ -875,7 +976,7 @@ impl Workspace {
|
||||
if self.project.read(cx).is_local() {
|
||||
Task::Ready(Some(callback(self, cx)))
|
||||
} else {
|
||||
let task = Self::new_local(Vec::new(), app_state.clone(), cx);
|
||||
let task = Self::new_local(Vec::new(), app_state.clone(), None, cx);
|
||||
cx.spawn(|_vh, mut cx| async move {
|
||||
let (workspace, _) = task.await;
|
||||
workspace.update(&mut cx, callback)
|
||||
@@ -1344,7 +1445,8 @@ impl Workspace {
|
||||
}
|
||||
|
||||
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
|
||||
let pane = cx.add_view(|cx| Pane::new(None, cx));
|
||||
let pane =
|
||||
cx.add_view(|cx| Pane::new(self.weak_handle().id(), None, self.background_actions, cx));
|
||||
let pane_id = pane.id();
|
||||
cx.subscribe(&pane, move |this, _, event, cx| {
|
||||
this.handle_pane_event(pane_id, event, cx)
|
||||
@@ -1356,6 +1458,23 @@ impl Workspace {
|
||||
pane
|
||||
}
|
||||
|
||||
pub fn add_item_to_center(
|
||||
&mut self,
|
||||
item: Box<dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
if let Some(center_pane) = self.last_active_center_pane.clone() {
|
||||
if let Some(center_pane) = center_pane.upgrade(cx) {
|
||||
Pane::add_item(self, ¢er_pane, item, true, true, None, cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
let active_pane = self.active_pane().clone();
|
||||
Pane::add_item(self, &active_pane, item, true, true, None, cx);
|
||||
@@ -1513,7 +1632,7 @@ impl Workspace {
|
||||
self.active_item_path_changed(cx);
|
||||
|
||||
if &pane == self.dock_pane() {
|
||||
Dock::show(self, cx);
|
||||
Dock::show(self, true, cx);
|
||||
} else {
|
||||
self.last_active_center_pane = Some(pane.downgrade());
|
||||
if self.dock.is_anchored_at(DockAnchor::Expanded) {
|
||||
@@ -2526,7 +2645,12 @@ impl Workspace {
|
||||
// the focus the dock generates start generating alternating
|
||||
// focus due to the deferred execution each triggering each other
|
||||
cx.after_window_update(move |workspace, cx| {
|
||||
Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx);
|
||||
Dock::set_dock_position(
|
||||
workspace,
|
||||
serialized_workspace.dock_position,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
@@ -2538,6 +2662,11 @@ impl Workspace {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self::new(None, 0, project, |_, _| None, || &[], cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
|
||||
@@ -2769,20 +2898,6 @@ impl std::fmt::Debug for OpenPaths {
|
||||
}
|
||||
}
|
||||
|
||||
fn open(_: &Open, cx: &mut MutableAppContext) {
|
||||
let mut paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
});
|
||||
cx.spawn(|mut cx| async move {
|
||||
if let Some(paths) = paths.recv().await.flatten() {
|
||||
cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
|
||||
|
||||
pub fn activate_workspace_for_project(
|
||||
@@ -2809,6 +2924,7 @@ pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
|
||||
pub fn open_paths(
|
||||
abs_paths: &[PathBuf],
|
||||
app_state: &Arc<AppState>,
|
||||
requesting_window_id: Option<usize>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<(
|
||||
ViewHandle<Workspace>,
|
||||
@@ -2839,7 +2955,8 @@ pub fn open_paths(
|
||||
.contains(&false);
|
||||
|
||||
cx.update(|cx| {
|
||||
let task = Workspace::new_local(abs_paths, app_state.clone(), cx);
|
||||
let task =
|
||||
Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (workspace, items) = task.await;
|
||||
@@ -2858,14 +2975,18 @@ pub fn open_paths(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Task<()> {
|
||||
let task = Workspace::new_local(Vec::new(), app_state.clone(), cx);
|
||||
pub fn open_new(
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut MutableAppContext,
|
||||
init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static,
|
||||
) -> Task<()> {
|
||||
let task = Workspace::new_local(Vec::new(), app_state.clone(), None, cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (workspace, opened_paths) = task.await;
|
||||
|
||||
workspace.update(&mut cx, |_, cx| {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
if opened_paths.is_empty() {
|
||||
cx.dispatch_action(NewFile);
|
||||
init(workspace, cx)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -2886,17 +3007,10 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use gpui::{executor::Deterministic, TestAppContext, ViewContext};
|
||||
use gpui::{executor::Deterministic, TestAppContext};
|
||||
use project::{Project, ProjectEntryId};
|
||||
use serde_json::json;
|
||||
|
||||
pub fn default_item_factory(
|
||||
_workspace: &mut Workspace,
|
||||
_cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab_disambiguation(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
@@ -2909,7 +3023,8 @@ mod tests {
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
default_item_factory,
|
||||
|_, _| None,
|
||||
|| &[],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -2981,7 +3096,8 @@ mod tests {
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
default_item_factory,
|
||||
|_, _| None,
|
||||
|| &[],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -3081,7 +3197,8 @@ mod tests {
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
default_item_factory,
|
||||
|_, _| None,
|
||||
|| &[],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -3120,7 +3237,7 @@ mod tests {
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, default_item_factory, cx)
|
||||
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
|
||||
});
|
||||
|
||||
let item1 = cx.add_view(&workspace, |cx| {
|
||||
@@ -3229,7 +3346,7 @@ mod tests {
|
||||
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, default_item_factory, cx)
|
||||
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
|
||||
});
|
||||
|
||||
// Create several workspace items with single project entries, and two
|
||||
@@ -3338,7 +3455,7 @@ mod tests {
|
||||
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, default_item_factory, cx)
|
||||
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
|
||||
});
|
||||
|
||||
let item = cx.add_view(&workspace, |cx| {
|
||||
@@ -3457,7 +3574,7 @@ mod tests {
|
||||
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, default_item_factory, cx)
|
||||
Workspace::new(Default::default(), 0, project, |_, _| None, || &[], cx)
|
||||
});
|
||||
|
||||
let item = cx.add_view(&workspace, |cx| {
|
||||
|
||||
@@ -29,6 +29,7 @@ context_menu = { path = "../context_menu" }
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
diagnostics = { path = "../diagnostics" }
|
||||
db = { path = "../db" }
|
||||
editor = { path = "../editor" }
|
||||
feedback = { path = "../feedback" }
|
||||
file_finder = { path = "../file_finder" }
|
||||
@@ -38,6 +39,7 @@ fsevent = { path = "../fsevent" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
go_to_line = { path = "../go_to_line" }
|
||||
gpui = { path = "../gpui" }
|
||||
install_cli = { path = "../install_cli" }
|
||||
journal = { path = "../journal" }
|
||||
language = { path = "../language" }
|
||||
language_selector = { path = "../language_selector" }
|
||||
@@ -59,6 +61,7 @@ theme_testbench = { path = "../theme_testbench" }
|
||||
util = { path = "../util" }
|
||||
vim = { path = "../vim" }
|
||||
workspace = { path = "../workspace" }
|
||||
welcome = { path = "../welcome" }
|
||||
anyhow = "1.0.38"
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-tar = "0.4.2"
|
||||
|
||||
+39
-15
@@ -13,11 +13,12 @@ use client::{
|
||||
http::{self, HttpClient},
|
||||
UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
|
||||
};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
FutureExt, SinkExt, StreamExt,
|
||||
};
|
||||
use gpui::{App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
|
||||
use gpui::{Action, App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
|
||||
use isahc::{config::Configurable, Request};
|
||||
use language::LanguageRegistry;
|
||||
use log::LevelFilter;
|
||||
@@ -35,17 +36,19 @@ use std::{
|
||||
path::PathBuf, sync::Arc, thread, time::Duration,
|
||||
};
|
||||
use terminal_view::{get_working_directory, TerminalView};
|
||||
use welcome::{show_welcome_experience, FIRST_OPEN};
|
||||
|
||||
use fs::RealFs;
|
||||
use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile};
|
||||
use settings::watched_json::WatchedJsonFile;
|
||||
use theme::ThemeRegistry;
|
||||
#[cfg(debug_assertions)]
|
||||
use util::StaffMode;
|
||||
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
self, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, OpenPaths, Workspace,
|
||||
self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
|
||||
OpenPaths, Workspace,
|
||||
};
|
||||
use zed::{self, build_window_options, initialize_workspace, languages, menus};
|
||||
use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings};
|
||||
|
||||
fn main() {
|
||||
let http = http::client();
|
||||
@@ -119,7 +122,14 @@ fn main() {
|
||||
fs.clone(),
|
||||
));
|
||||
|
||||
watch_settings_file(default_settings, settings_file_content, themes.clone(), cx);
|
||||
settings::watch_files(
|
||||
default_settings,
|
||||
settings_file_content,
|
||||
themes.clone(),
|
||||
keymap_file,
|
||||
cx,
|
||||
);
|
||||
|
||||
if !stdout_is_a_pty() {
|
||||
upload_previous_panics(http.clone(), cx);
|
||||
}
|
||||
@@ -132,8 +142,6 @@ fn main() {
|
||||
languages::init(languages.clone());
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
||||
|
||||
watch_keymap_file(keymap_file, cx);
|
||||
|
||||
cx.set_global(client.clone());
|
||||
|
||||
context_menu::init(cx);
|
||||
@@ -179,6 +187,7 @@ fn main() {
|
||||
build_window_options,
|
||||
initialize_workspace,
|
||||
dock_default_item_factory,
|
||||
background_actions,
|
||||
});
|
||||
auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
|
||||
|
||||
@@ -190,6 +199,7 @@ fn main() {
|
||||
zed::init(&app_state, cx);
|
||||
collab_ui::init(app_state.clone(), cx);
|
||||
feedback::init(app_state.clone(), cx);
|
||||
welcome::init(cx);
|
||||
|
||||
cx.set_menus(menus::menus());
|
||||
|
||||
@@ -197,7 +207,7 @@ fn main() {
|
||||
cx.platform().activate(true);
|
||||
let paths = collect_path_args();
|
||||
if paths.is_empty() {
|
||||
cx.spawn(|cx| async move { restore_or_create_workspace(cx).await })
|
||||
cx.spawn(|cx| async move { restore_or_create_workspace(&app_state, cx).await })
|
||||
.detach()
|
||||
} else {
|
||||
cx.dispatch_global_action(OpenPaths { paths });
|
||||
@@ -207,11 +217,14 @@ fn main() {
|
||||
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
|
||||
.detach();
|
||||
} else if let Ok(Some(paths)) = open_paths_rx.try_next() {
|
||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||
.detach();
|
||||
} else {
|
||||
cx.spawn(|cx| async move { restore_or_create_workspace(cx).await })
|
||||
.detach()
|
||||
cx.spawn({
|
||||
let app_state = app_state.clone();
|
||||
|cx| async move { restore_or_create_workspace(&app_state, cx).await }
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
|
||||
cx.spawn(|cx| {
|
||||
@@ -228,8 +241,7 @@ fn main() {
|
||||
let app_state = app_state.clone();
|
||||
async move {
|
||||
while let Some(paths) = open_paths_rx.next().await {
|
||||
log::error!("OPEN PATHS FROM HANDLE");
|
||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
@@ -251,13 +263,15 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
async fn restore_or_create_workspace(mut cx: AsyncAppContext) {
|
||||
async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
|
||||
if let Some(location) = workspace::last_opened_workspace_paths().await {
|
||||
cx.update(|cx| {
|
||||
cx.dispatch_global_action(OpenPaths {
|
||||
paths: location.paths().as_ref().clone(),
|
||||
})
|
||||
});
|
||||
} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
|
||||
cx.update(|cx| show_welcome_experience(app_state, cx));
|
||||
} else {
|
||||
cx.update(|cx| {
|
||||
cx.dispatch_global_action(NewFile);
|
||||
@@ -591,7 +605,7 @@ async fn handle_cli_connection(
|
||||
paths
|
||||
};
|
||||
let (workspace, items) = cx
|
||||
.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
||||
.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||
.await;
|
||||
|
||||
let mut errored = false;
|
||||
@@ -692,3 +706,13 @@ pub fn dock_default_item_factory(
|
||||
|
||||
Some(Box::new(terminal_view))
|
||||
}
|
||||
|
||||
pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
|
||||
&[
|
||||
("Go to file", &file_finder::Toggle),
|
||||
("Open command palette", &command_palette::Toggle),
|
||||
("Focus the dock", &FocusDock),
|
||||
("Open recent projects", &recent_projects::OpenRecent),
|
||||
("Change your settings", &OpenSettings),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||
MenuItem::action("Select Theme", theme_selector::Toggle),
|
||||
],
|
||||
}),
|
||||
MenuItem::action("Install CLI", super::InstallCommandLineInterface),
|
||||
MenuItem::action("Install CLI", install_cli::Install),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Hide Zed", super::Hide),
|
||||
MenuItem::action("Hide Others", super::HideOthers),
|
||||
@@ -137,8 +137,9 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||
items: vec![
|
||||
MenuItem::action("Command Palette", command_palette::Toggle),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("View Telemetry Log", crate::OpenTelemetryLog),
|
||||
MenuItem::action("View Telemetry", crate::OpenTelemetryLog),
|
||||
MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
|
||||
MenuItem::action("Show Welcome", workspace::Welcome),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action(
|
||||
"Copy System Specs Into Clipboard",
|
||||
|
||||
+53
-95
@@ -2,7 +2,7 @@ pub mod languages;
|
||||
pub mod menus;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::Context;
|
||||
use assets::Assets;
|
||||
use breadcrumbs::Breadcrumbs;
|
||||
pub use client;
|
||||
@@ -20,7 +20,7 @@ use gpui::{
|
||||
geometry::vector::vec2f,
|
||||
impl_actions,
|
||||
platform::{WindowBounds, WindowOptions},
|
||||
AssetSource, AsyncAppContext, Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind,
|
||||
AssetSource, Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind,
|
||||
};
|
||||
use language::Rope;
|
||||
pub use lsp;
|
||||
@@ -65,7 +65,6 @@ actions!(
|
||||
IncreaseBufferFontSize,
|
||||
DecreaseBufferFontSize,
|
||||
ResetBufferFontSize,
|
||||
InstallCommandLineInterface,
|
||||
ResetDatabase,
|
||||
]
|
||||
);
|
||||
@@ -140,9 +139,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||
cx.refresh_windows();
|
||||
});
|
||||
});
|
||||
cx.add_global_action(move |_: &InstallCommandLineInterface, cx| {
|
||||
cx.spawn(|cx| async move { install_cli(&cx).await.context("error creating CLI symlink") })
|
||||
.detach_and_log_err(cx);
|
||||
cx.add_global_action(move |_: &install_cli::Install, cx| {
|
||||
cx.spawn(|cx| async move {
|
||||
install_cli::install_cli(&cx)
|
||||
.await
|
||||
.context("error creating CLI symlink")
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
cx.add_action({
|
||||
let app_state = app_state.clone();
|
||||
@@ -255,7 +258,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
|
||||
},
|
||||
);
|
||||
|
||||
activity_indicator::init(cx);
|
||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
settings::KeymapFileContent::load_defaults(cx);
|
||||
@@ -482,54 +484,6 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
|
||||
);
|
||||
}
|
||||
|
||||
async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
|
||||
let cli_path = cx.platform().path_for_auxiliary_executable("cli")?;
|
||||
let link_path = Path::new("/usr/local/bin/zed");
|
||||
let bin_dir_path = link_path.parent().unwrap();
|
||||
|
||||
// Don't re-create symlink if it points to the same CLI binary.
|
||||
if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If the symlink is not there or is outdated, first try replacing it
|
||||
// without escalating.
|
||||
smol::fs::remove_file(link_path).await.log_err();
|
||||
if smol::fs::unix::symlink(&cli_path, link_path)
|
||||
.await
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// The symlink could not be created, so use osascript with admin privileges
|
||||
// to create it.
|
||||
let status = smol::process::Command::new("osascript")
|
||||
.args([
|
||||
"-e",
|
||||
&format!(
|
||||
"do shell script \" \
|
||||
mkdir -p \'{}\' && \
|
||||
ln -sf \'{}\' \'{}\' \
|
||||
\" with administrator privileges",
|
||||
bin_dir_path.to_string_lossy(),
|
||||
cli_path.to_string_lossy(),
|
||||
link_path.to_string_lossy(),
|
||||
),
|
||||
])
|
||||
.stdout(smol::process::Stdio::inherit())
|
||||
.stderr(smol::process::Stdio::inherit())
|
||||
.output()
|
||||
.await?
|
||||
.status;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("error running osascript"))
|
||||
}
|
||||
}
|
||||
|
||||
fn open_config_file(
|
||||
path: &'static Path,
|
||||
app_state: Arc<AppState>,
|
||||
@@ -758,6 +712,10 @@ mod tests {
|
||||
"ca": null,
|
||||
"cb": null,
|
||||
},
|
||||
"d": {
|
||||
"da": null,
|
||||
"db": null,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@@ -766,13 +724,14 @@ mod tests {
|
||||
open_paths(
|
||||
&[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
|
||||
&app_state,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(cx.window_ids().len(), 1);
|
||||
|
||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
|
||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
|
||||
.await;
|
||||
assert_eq!(cx.window_ids().len(), 1);
|
||||
let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
|
||||
@@ -786,11 +745,37 @@ mod tests {
|
||||
open_paths(
|
||||
&[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
|
||||
&app_state,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(cx.window_ids().len(), 2);
|
||||
|
||||
// Replace existing windows
|
||||
let window_id = cx.window_ids()[0];
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
|
||||
&app_state,
|
||||
Some(window_id),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(cx.window_ids().len(), 2);
|
||||
let workspace_1 = cx.root_view::<Workspace>(window_id).unwrap();
|
||||
workspace_1.read_with(cx, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace
|
||||
.worktrees(cx)
|
||||
.map(|w| w.read(cx).abs_path())
|
||||
.collect::<Vec<_>>(),
|
||||
&[Path::new("/root/c").into(), Path::new("/root/d").into()]
|
||||
);
|
||||
assert!(workspace.left_sidebar().read(cx).is_open());
|
||||
assert!(workspace.active_pane().is_focused(cx));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -802,7 +787,7 @@ mod tests {
|
||||
.insert_tree("/root", json!({"a": "hey"}))
|
||||
.await;
|
||||
|
||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
|
||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
|
||||
.await;
|
||||
assert_eq!(cx.window_ids().len(), 1);
|
||||
|
||||
@@ -840,7 +825,7 @@ mod tests {
|
||||
assert!(!cx.is_window_edited(workspace.window_id()));
|
||||
|
||||
// Opening the buffer again doesn't impact the window's edited state.
|
||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
|
||||
cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
|
||||
.await;
|
||||
let editor = workspace.read_with(cx, |workspace, cx| {
|
||||
workspace
|
||||
@@ -870,7 +855,8 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
|
||||
let app_state = init(cx);
|
||||
cx.update(|cx| open_new(&app_state, cx)).await;
|
||||
cx.update(|cx| open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)))
|
||||
.await;
|
||||
|
||||
let window_id = *cx.window_ids().first().unwrap();
|
||||
let workspace = cx.root_view::<Workspace>(window_id).unwrap();
|
||||
@@ -915,9 +901,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
let file1 = entries[0].clone();
|
||||
@@ -1036,9 +1020,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
// Open a file within an existing worktree.
|
||||
cx.update(|cx| {
|
||||
@@ -1197,9 +1179,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
// Open a file within an existing worktree.
|
||||
cx.update(|cx| {
|
||||
@@ -1241,9 +1221,7 @@ mod tests {
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
|
||||
|
||||
// Create a new untitled buffer
|
||||
@@ -1332,9 +1310,7 @@ mod tests {
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
// Create a new untitled buffer
|
||||
cx.dispatch_action(window_id, NewFile);
|
||||
@@ -1387,9 +1363,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
let file1 = entries[0].clone();
|
||||
@@ -1463,15 +1437,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
let file1 = entries[0].clone();
|
||||
@@ -1735,15 +1701,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
let entries = cx.read(|cx| workspace.file_project_paths(cx));
|
||||
|
||||
Reference in New Issue
Block a user