Merge pull request #2199 from zed-industries/welcome-experience

Welcome experience
This commit is contained in:
Mikayla Maki
2023-03-10 10:48:30 -08:00
committed by GitHub
63 changed files with 2649 additions and 675 deletions
+3 -10
View File
@@ -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 {
+3 -27
View File
@@ -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)
+1 -1
View File
@@ -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()
+1
View File
@@ -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);
+1 -1
View File
@@ -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()
+3 -3
View File
@@ -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()
+1 -1
View File
@@ -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
View File
@@ -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};
+1 -9
View File
@@ -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| {
+15 -11
View File
@@ -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
+7 -21
View File
@@ -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
+1 -1
View File
@@ -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,
+20 -1
View File
@@ -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)
}
}
}
+3 -1
View File
@@ -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(
+35 -1
View File
@@ -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),
+41 -15
View File
@@ -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(
+18
View File
@@ -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" }
+55
View File
@@ -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"))
}
}
+1 -1
View File
@@ -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
+4 -1
View File
@@ -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)
+1 -1
View File
@@ -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();
+85 -66
View File
@@ -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);
+5 -1
View File
@@ -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
View File
@@ -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(),
);
}
}
+163 -43
View File
@@ -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();
+26 -5
View File
@@ -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();
+1 -1
View File
@@ -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 {
+1 -9
View File
@@ -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)
}
+30 -2
View File
@@ -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,
+149
View File
@@ -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)
}
+1 -6
View File
@@ -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 {
+27
View File
@@ -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" }
+175
View File
@@ -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()
}
}
+316
View File
@@ -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))
}
}
+1
View File
@@ -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" }
+45 -18
View File
@@ -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| {
+8
View File
@@ -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 {
+20 -8
View File
@@ -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
View File
@@ -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);
+12
View File
@@ -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
View File
@@ -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(&center_pane, move |this, _, event, cx| {
this.handle_pane_event(pane_id, event, cx)
@@ -541,7 +617,12 @@ impl Workspace {
.detach();
cx.focus(&center_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, &center_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| {
+3
View File
@@ -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
View File
@@ -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),
]
}
+3 -2
View File
@@ -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
View File
@@ -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));