Merge pull request #2199 from zed-industries/welcome-experience
Welcome experience
This commit is contained in:
commit
37d01c7fb3
63 changed files with 2649 additions and 675 deletions
|
@ -13,11 +13,12 @@ use client::{
|
|||
http::{self, HttpClient},
|
||||
UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
|
||||
};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
FutureExt, SinkExt, StreamExt,
|
||||
};
|
||||
use gpui::{App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
|
||||
use gpui::{Action, App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
|
||||
use isahc::{config::Configurable, Request};
|
||||
use language::LanguageRegistry;
|
||||
use log::LevelFilter;
|
||||
|
@ -35,17 +36,19 @@ use std::{
|
|||
path::PathBuf, sync::Arc, thread, time::Duration,
|
||||
};
|
||||
use terminal_view::{get_working_directory, TerminalView};
|
||||
use welcome::{show_welcome_experience, FIRST_OPEN};
|
||||
|
||||
use fs::RealFs;
|
||||
use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile};
|
||||
use settings::watched_json::WatchedJsonFile;
|
||||
use theme::ThemeRegistry;
|
||||
#[cfg(debug_assertions)]
|
||||
use util::StaffMode;
|
||||
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
self, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, OpenPaths, Workspace,
|
||||
self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
|
||||
OpenPaths, Workspace,
|
||||
};
|
||||
use zed::{self, build_window_options, initialize_workspace, languages, menus};
|
||||
use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings};
|
||||
|
||||
fn main() {
|
||||
let http = http::client();
|
||||
|
@ -119,7 +122,14 @@ fn main() {
|
|||
fs.clone(),
|
||||
));
|
||||
|
||||
watch_settings_file(default_settings, settings_file_content, themes.clone(), cx);
|
||||
settings::watch_files(
|
||||
default_settings,
|
||||
settings_file_content,
|
||||
themes.clone(),
|
||||
keymap_file,
|
||||
cx,
|
||||
);
|
||||
|
||||
if !stdout_is_a_pty() {
|
||||
upload_previous_panics(http.clone(), cx);
|
||||
}
|
||||
|
@ -132,8 +142,6 @@ fn main() {
|
|||
languages::init(languages.clone());
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
||||
|
||||
watch_keymap_file(keymap_file, cx);
|
||||
|
||||
cx.set_global(client.clone());
|
||||
|
||||
context_menu::init(cx);
|
||||
|
@ -179,6 +187,7 @@ fn main() {
|
|||
build_window_options,
|
||||
initialize_workspace,
|
||||
dock_default_item_factory,
|
||||
background_actions,
|
||||
});
|
||||
auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
|
||||
|
||||
|
@ -190,6 +199,7 @@ fn main() {
|
|||
zed::init(&app_state, cx);
|
||||
collab_ui::init(app_state.clone(), cx);
|
||||
feedback::init(app_state.clone(), cx);
|
||||
welcome::init(cx);
|
||||
|
||||
cx.set_menus(menus::menus());
|
||||
|
||||
|
@ -197,7 +207,7 @@ fn main() {
|
|||
cx.platform().activate(true);
|
||||
let paths = collect_path_args();
|
||||
if paths.is_empty() {
|
||||
cx.spawn(|cx| async move { restore_or_create_workspace(cx).await })
|
||||
cx.spawn(|cx| async move { restore_or_create_workspace(&app_state, cx).await })
|
||||
.detach()
|
||||
} else {
|
||||
cx.dispatch_global_action(OpenPaths { paths });
|
||||
|
@ -207,11 +217,14 @@ fn main() {
|
|||
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
|
||||
.detach();
|
||||
} else if let Ok(Some(paths)) = open_paths_rx.try_next() {
|
||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||
.detach();
|
||||
} else {
|
||||
cx.spawn(|cx| async move { restore_or_create_workspace(cx).await })
|
||||
.detach()
|
||||
cx.spawn({
|
||||
let app_state = app_state.clone();
|
||||
|cx| async move { restore_or_create_workspace(&app_state, cx).await }
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
|
||||
cx.spawn(|cx| {
|
||||
|
@ -228,8 +241,7 @@ fn main() {
|
|||
let app_state = app_state.clone();
|
||||
async move {
|
||||
while let Some(paths) = open_paths_rx.next().await {
|
||||
log::error!("OPEN PATHS FROM HANDLE");
|
||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
||||
cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
@ -251,13 +263,15 @@ fn main() {
|
|||
});
|
||||
}
|
||||
|
||||
async fn restore_or_create_workspace(mut cx: AsyncAppContext) {
|
||||
async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
|
||||
if let Some(location) = workspace::last_opened_workspace_paths().await {
|
||||
cx.update(|cx| {
|
||||
cx.dispatch_global_action(OpenPaths {
|
||||
paths: location.paths().as_ref().clone(),
|
||||
})
|
||||
});
|
||||
} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
|
||||
cx.update(|cx| show_welcome_experience(app_state, cx));
|
||||
} else {
|
||||
cx.update(|cx| {
|
||||
cx.dispatch_global_action(NewFile);
|
||||
|
@ -591,7 +605,7 @@ async fn handle_cli_connection(
|
|||
paths
|
||||
};
|
||||
let (workspace, items) = cx
|
||||
.update(|cx| workspace::open_paths(&paths, &app_state, cx))
|
||||
.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||
.await;
|
||||
|
||||
let mut errored = false;
|
||||
|
@ -692,3 +706,13 @@ pub fn dock_default_item_factory(
|
|||
|
||||
Some(Box::new(terminal_view))
|
||||
}
|
||||
|
||||
pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
|
||||
&[
|
||||
("Go to file", &file_finder::Toggle),
|
||||
("Open command palette", &command_palette::Toggle),
|
||||
("Focus the dock", &FocusDock),
|
||||
("Open recent projects", &recent_projects::OpenRecent),
|
||||
("Change your settings", &OpenSettings),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ pub fn menus() -> Vec<Menu<'static>> {
|
|||
MenuItem::action("Select Theme", theme_selector::Toggle),
|
||||
],
|
||||
}),
|
||||
MenuItem::action("Install CLI", super::InstallCommandLineInterface),
|
||||
MenuItem::action("Install CLI", install_cli::Install),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Hide Zed", super::Hide),
|
||||
MenuItem::action("Hide Others", super::HideOthers),
|
||||
|
@ -137,8 +137,9 @@ pub fn menus() -> Vec<Menu<'static>> {
|
|||
items: vec![
|
||||
MenuItem::action("Command Palette", command_palette::Toggle),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("View Telemetry Log", crate::OpenTelemetryLog),
|
||||
MenuItem::action("View Telemetry", crate::OpenTelemetryLog),
|
||||
MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
|
||||
MenuItem::action("Show Welcome", workspace::Welcome),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action(
|
||||
"Copy System Specs Into Clipboard",
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue