
Closes #ISSUE Adds system GPU collection to crash reporting. Currently this is Linux only. The system GPUs are determined by reading the `/sys/class/drm` directory structure, rather than using the exisiting `gpui::Window::gpu_specs()` method in order to gather more information, and so that the GPU context is not dependent on Vulkan context initialization (i.e. we still get GPU info when Zed fails to start because Vulkan failed to initialize). Unfortunately, the `blade` APIs do not support querying which GPU _will_ be used, so we do not know which GPU was attempted to be used when Vulkan context initialization fails, however, when Vulkan initialization succeeds, we send a message to the crash handler containing the result of `gpui::Window::gpu_specs()` to include the "Active" gpu in any crash report that may occur Release Notes: - N/A *or* Added/Fixed/Improved ...
1446 lines
50 KiB
Rust
1446 lines
50 KiB
Rust
mod reliability;
|
|
mod zed;
|
|
|
|
use agent_ui::AgentPanel;
|
|
use anyhow::{Context as _, Result};
|
|
use clap::{Parser, command};
|
|
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
|
use client::{Client, ProxySettings, UserStore, parse_zed_link};
|
|
use collab_ui::channel_view::ChannelView;
|
|
use collections::HashMap;
|
|
use crashes::InitCrashHandler;
|
|
use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
|
|
use editor::Editor;
|
|
use extension::ExtensionHostProxy;
|
|
use extension_host::ExtensionStore;
|
|
use fs::{Fs, RealFs};
|
|
use futures::{StreamExt, channel::oneshot, future};
|
|
use git::GitHostingProviderRegistry;
|
|
use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, UpdateGlobal as _};
|
|
|
|
use gpui_tokio::Tokio;
|
|
use http_client::{Url, read_proxy_from_env};
|
|
use language::LanguageRegistry;
|
|
use onboarding::{FIRST_OPEN, show_onboarding_view};
|
|
use prompt_store::PromptBuilder;
|
|
use reqwest_client::ReqwestClient;
|
|
|
|
use assets::Assets;
|
|
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
|
use parking_lot::Mutex;
|
|
use project::project_settings::ProjectSettings;
|
|
use recent_projects::{SshSettings, open_ssh_project};
|
|
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
|
use session::{AppSession, Session};
|
|
use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
|
|
use std::{
|
|
env,
|
|
io::{self, IsTerminal},
|
|
path::{Path, PathBuf},
|
|
process,
|
|
sync::Arc,
|
|
};
|
|
use theme::{
|
|
ActiveTheme, IconThemeNotFoundError, SystemAppearance, ThemeNotFoundError, ThemeRegistry,
|
|
ThemeSettings,
|
|
};
|
|
use util::{ResultExt, TryFutureExt, maybe};
|
|
use uuid::Uuid;
|
|
use workspace::{
|
|
AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore,
|
|
notifications::NotificationId,
|
|
};
|
|
use zed::{
|
|
OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options,
|
|
derive_paths_with_position, edit_prediction_registry, handle_cli_connection,
|
|
handle_keymap_file_changes, handle_settings_changed, handle_settings_file_changes,
|
|
initialize_workspace, open_paths_with_positions,
|
|
};
|
|
|
|
use crate::zed::OpenRequestKind;
|
|
|
|
#[cfg(feature = "mimalloc")]
|
|
#[global_allocator]
|
|
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
|
|
fn files_not_created_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) {
|
|
let message = "Zed failed to launch";
|
|
let error_details = errors
|
|
.into_iter()
|
|
.flat_map(|(kind, paths)| {
|
|
#[allow(unused_mut)] // for non-unix platforms
|
|
let mut error_kind_details = match paths.len() {
|
|
0 => return None,
|
|
1 => format!(
|
|
"{kind} when creating directory {:?}",
|
|
paths.first().expect("match arm checks for a single entry")
|
|
),
|
|
_many => format!("{kind} when creating directories {paths:?}"),
|
|
};
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
if kind == io::ErrorKind::PermissionDenied {
|
|
error_kind_details.push_str("\n\nConsider using chown and chmod tools for altering the directories permissions if your user has corresponding rights.\
|
|
\nFor example, `sudo chown $(whoami):staff ~/.config` and `chmod +uwrx ~/.config`");
|
|
}
|
|
}
|
|
|
|
Some(error_kind_details)
|
|
})
|
|
.collect::<Vec<_>>().join("\n\n");
|
|
|
|
eprintln!("{message}: {error_details}");
|
|
Application::new().run(move |cx| {
|
|
if let Ok(window) = cx.open_window(gpui::WindowOptions::default(), |_, cx| {
|
|
cx.new(|_| gpui::Empty)
|
|
}) {
|
|
window
|
|
.update(cx, |_, window, cx| {
|
|
let response = window.prompt(
|
|
gpui::PromptLevel::Critical,
|
|
message,
|
|
Some(&error_details),
|
|
&["Exit"],
|
|
cx,
|
|
);
|
|
|
|
cx.spawn_in(window, async move |_, cx| {
|
|
response.await?;
|
|
cx.update(|_, cx| cx.quit())
|
|
})
|
|
.detach_and_log_err(cx);
|
|
})
|
|
.log_err();
|
|
} else {
|
|
fail_to_open_window(anyhow::anyhow!("{message}: {error_details}"), cx)
|
|
}
|
|
})
|
|
}
|
|
|
|
fn fail_to_open_window_async(e: anyhow::Error, cx: &mut AsyncApp) {
|
|
cx.update(|cx| fail_to_open_window(e, cx)).log_err();
|
|
}
|
|
|
|
fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) {
|
|
eprintln!(
|
|
"Zed failed to open a window: {e:?}. See https://zed.dev/docs/linux for troubleshooting steps."
|
|
);
|
|
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
|
{
|
|
process::exit(1);
|
|
}
|
|
|
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
{
|
|
use ashpd::desktop::notification::{Notification, NotificationProxy, Priority};
|
|
_cx.spawn(async move |_cx| {
|
|
let Ok(proxy) = NotificationProxy::new().await else {
|
|
process::exit(1);
|
|
};
|
|
|
|
let notification_id = "dev.zed.Oops";
|
|
proxy
|
|
.add_notification(
|
|
notification_id,
|
|
Notification::new("Zed failed to launch")
|
|
.body(Some(
|
|
format!(
|
|
"{e:?}. See https://zed.dev/docs/linux for troubleshooting steps."
|
|
)
|
|
.as_str(),
|
|
))
|
|
.priority(Priority::High)
|
|
.icon(ashpd::desktop::Icon::with_names(&[
|
|
"dialog-question-symbolic",
|
|
])),
|
|
)
|
|
.await
|
|
.ok();
|
|
|
|
process::exit(1);
|
|
})
|
|
.detach();
|
|
}
|
|
}
|
|
|
|
pub fn main() {
|
|
#[cfg(unix)]
|
|
util::prevent_root_execution();
|
|
|
|
let args = Args::parse();
|
|
|
|
// `zed --crash-handler` Makes zed operate in minidump crash handler mode
|
|
if let Some(socket) = &args.crash_handler {
|
|
crashes::crash_server(socket.as_path());
|
|
return;
|
|
}
|
|
|
|
// `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
|
|
if let Some(socket) = &args.askpass {
|
|
askpass::main(socket);
|
|
return;
|
|
}
|
|
|
|
// `zed --nc` Makes zed operate in nc/netcat mode for use with MCP
|
|
if let Some(socket) = &args.nc {
|
|
match nc::main(socket) {
|
|
Ok(()) => return,
|
|
Err(err) => {
|
|
eprintln!("Error: {}", err);
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// `zed --printenv` Outputs environment variables as JSON to stdout
|
|
if args.printenv {
|
|
util::shell_env::print_env();
|
|
return;
|
|
}
|
|
|
|
if args.dump_all_actions {
|
|
dump_all_gpui_actions();
|
|
return;
|
|
}
|
|
|
|
// Set custom data directory.
|
|
if let Some(dir) = &args.user_data_dir {
|
|
paths::set_custom_data_dir(dir);
|
|
}
|
|
|
|
#[cfg(all(not(debug_assertions), target_os = "windows"))]
|
|
unsafe {
|
|
use windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
|
|
|
|
if args.foreground {
|
|
let _ = AttachConsole(ATTACH_PARENT_PROCESS);
|
|
}
|
|
}
|
|
|
|
let file_errors = init_paths();
|
|
if !file_errors.is_empty() {
|
|
files_not_created_on_launch(file_errors);
|
|
return;
|
|
}
|
|
|
|
zlog::init();
|
|
if stdout_is_a_pty() {
|
|
zlog::init_output_stdout();
|
|
} else {
|
|
let result = zlog::init_output_file(paths::log_file(), Some(paths::old_log_file()));
|
|
if let Err(err) = result {
|
|
eprintln!("Could not open log file: {}... Defaulting to stdout", err);
|
|
zlog::init_output_stdout();
|
|
};
|
|
}
|
|
|
|
let app_version = AppVersion::load(env!("CARGO_PKG_VERSION"));
|
|
let app_commit_sha =
|
|
option_env!("ZED_COMMIT_SHA").map(|commit_sha| AppCommitSha::new(commit_sha.to_string()));
|
|
|
|
if args.system_specs {
|
|
let system_specs = system_specs::SystemSpecs::new_stateless(
|
|
app_version,
|
|
app_commit_sha,
|
|
*release_channel::RELEASE_CHANNEL,
|
|
);
|
|
println!("Zed System Specs (from CLI):\n{}", system_specs);
|
|
return;
|
|
}
|
|
|
|
log::info!(
|
|
"========== starting zed version {}, sha {} ==========",
|
|
app_version,
|
|
app_commit_sha
|
|
.as_ref()
|
|
.map(|sha| sha.short())
|
|
.as_deref()
|
|
.unwrap_or("unknown"),
|
|
);
|
|
|
|
let app = Application::new().with_assets(Assets);
|
|
|
|
let system_id = app.background_executor().block(system_id()).ok();
|
|
let installation_id = app.background_executor().block(installation_id()).ok();
|
|
let session_id = Uuid::new_v4().to_string();
|
|
let session = app.background_executor().block(Session::new());
|
|
|
|
app.background_executor()
|
|
.spawn(crashes::init(InitCrashHandler {
|
|
session_id: session_id.clone(),
|
|
zed_version: app_version.to_string(),
|
|
release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(),
|
|
commit_sha: app_commit_sha
|
|
.as_ref()
|
|
.map(|sha| sha.full())
|
|
.unwrap_or_else(|| "no sha".to_owned()),
|
|
}))
|
|
.detach();
|
|
reliability::init_panic_hook(
|
|
app_version,
|
|
app_commit_sha.clone(),
|
|
system_id.as_ref().map(|id| id.to_string()),
|
|
installation_id.as_ref().map(|id| id.to_string()),
|
|
session_id.clone(),
|
|
);
|
|
|
|
let (open_listener, mut open_rx) = OpenListener::new();
|
|
|
|
let failed_single_instance_check = if *db::ZED_STATELESS
|
|
|| *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev
|
|
{
|
|
false
|
|
} else {
|
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
{
|
|
crate::zed::listen_for_cli_connections(open_listener.clone()).is_err()
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
!crate::zed::windows_only_instance::handle_single_instance(open_listener.clone(), &args)
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
use zed::mac_only_instance::*;
|
|
ensure_only_instance() != IsOnlyInstance::Yes
|
|
}
|
|
};
|
|
if failed_single_instance_check {
|
|
println!("zed is already running");
|
|
return;
|
|
}
|
|
|
|
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
|
|
let git_binary_path =
|
|
if cfg!(target_os = "macos") && option_env!("ZED_BUNDLE").as_deref() == Some("true") {
|
|
app.path_for_auxiliary_executable("git")
|
|
.context("could not find git binary path")
|
|
.log_err()
|
|
} else {
|
|
None
|
|
};
|
|
log::info!("Using git binary path: {:?}", git_binary_path);
|
|
|
|
let fs = Arc::new(RealFs::new(git_binary_path, app.background_executor()));
|
|
let user_settings_file_rx = watch_config_file(
|
|
&app.background_executor(),
|
|
fs.clone(),
|
|
paths::settings_file().clone(),
|
|
);
|
|
let global_settings_file_rx = watch_config_file(
|
|
&app.background_executor(),
|
|
fs.clone(),
|
|
paths::global_settings_file().clone(),
|
|
);
|
|
let user_keymap_file_rx = watch_config_file(
|
|
&app.background_executor(),
|
|
fs.clone(),
|
|
paths::keymap_file().clone(),
|
|
);
|
|
|
|
let (shell_env_loaded_tx, shell_env_loaded_rx) = oneshot::channel();
|
|
if !stdout_is_a_pty() {
|
|
app.background_executor()
|
|
.spawn(async {
|
|
#[cfg(unix)]
|
|
util::load_login_shell_environment().log_err();
|
|
shell_env_loaded_tx.send(()).ok();
|
|
})
|
|
.detach()
|
|
} else {
|
|
drop(shell_env_loaded_tx)
|
|
}
|
|
|
|
app.on_open_urls({
|
|
let open_listener = open_listener.clone();
|
|
move |urls| {
|
|
open_listener.open(RawOpenRequest {
|
|
urls,
|
|
diff_paths: Vec::new(),
|
|
})
|
|
}
|
|
});
|
|
app.on_reopen(move |cx| {
|
|
if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade())
|
|
{
|
|
cx.spawn({
|
|
let app_state = app_state;
|
|
async move |cx| {
|
|
if let Err(e) = restore_or_create_workspace(app_state, cx).await {
|
|
fail_to_open_window_async(e, cx)
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
});
|
|
|
|
app.run(move |cx| {
|
|
menu::init();
|
|
zed_actions::init();
|
|
|
|
release_channel::init(app_version, cx);
|
|
gpui_tokio::init(cx);
|
|
if let Some(app_commit_sha) = app_commit_sha {
|
|
AppCommitSha::set_global(app_commit_sha, cx);
|
|
}
|
|
settings::init(cx);
|
|
zlog_settings::init(cx);
|
|
handle_settings_file_changes(
|
|
user_settings_file_rx,
|
|
global_settings_file_rx,
|
|
cx,
|
|
handle_settings_changed,
|
|
);
|
|
handle_keymap_file_changes(user_keymap_file_rx, cx);
|
|
client::init_settings(cx);
|
|
let user_agent = format!(
|
|
"Zed/{} ({}; {})",
|
|
AppVersion::global(cx),
|
|
std::env::consts::OS,
|
|
std::env::consts::ARCH
|
|
);
|
|
let proxy_str = ProxySettings::get_global(cx).proxy.to_owned();
|
|
let proxy_url = proxy_str
|
|
.as_ref()
|
|
.and_then(|input| {
|
|
input
|
|
.parse::<Url>()
|
|
.inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
|
|
.ok()
|
|
})
|
|
.or_else(read_proxy_from_env);
|
|
let http = {
|
|
let _guard = Tokio::handle(cx).enter();
|
|
|
|
ReqwestClient::proxy_and_user_agent(proxy_url, &user_agent)
|
|
.expect("could not start HTTP client")
|
|
};
|
|
cx.set_http_client(Arc::new(http));
|
|
|
|
<dyn Fs>::set_global(fs.clone(), cx);
|
|
|
|
GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
|
|
git_hosting_providers::init(cx);
|
|
|
|
OpenListener::set_global(cx, open_listener.clone());
|
|
|
|
extension::init(cx);
|
|
let extension_host_proxy = ExtensionHostProxy::global(cx);
|
|
|
|
let client = Client::production(cx);
|
|
cx.set_http_client(client.http_client());
|
|
let mut languages = LanguageRegistry::new(cx.background_executor().clone());
|
|
languages.set_language_server_download_dir(paths::languages_dir().clone());
|
|
let languages = Arc::new(languages);
|
|
let (mut tx, rx) = watch::channel(None);
|
|
cx.observe_global::<SettingsStore>(move |cx| {
|
|
let settings = &ProjectSettings::get_global(cx).node;
|
|
let options = NodeBinaryOptions {
|
|
allow_path_lookup: !settings.ignore_system_version,
|
|
// TODO: Expose this setting
|
|
allow_binary_download: true,
|
|
use_paths: settings.path.as_ref().map(|node_path| {
|
|
let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref());
|
|
let npm_path = settings
|
|
.npm_path
|
|
.as_ref()
|
|
.map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref()));
|
|
(
|
|
node_path.clone(),
|
|
npm_path.unwrap_or_else(|| {
|
|
let base_path = PathBuf::new();
|
|
node_path.parent().unwrap_or(&base_path).join("npm")
|
|
}),
|
|
)
|
|
}),
|
|
};
|
|
tx.send(Some(options)).log_err();
|
|
})
|
|
.detach();
|
|
let node_runtime = NodeRuntime::new(client.http_client(), Some(shell_env_loaded_rx), rx);
|
|
|
|
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
|
|
language::init(cx);
|
|
languages::init(languages.clone(), node_runtime.clone(), cx);
|
|
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
|
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
|
|
|
|
language_extension::init(
|
|
language_extension::LspAccess::ViaWorkspaces({
|
|
let workspace_store = workspace_store.clone();
|
|
Arc::new(move |cx: &mut App| {
|
|
workspace_store.update(cx, |workspace_store, cx| {
|
|
workspace_store
|
|
.workspaces()
|
|
.iter()
|
|
.map(|workspace| {
|
|
workspace.update(cx, |workspace, _, cx| {
|
|
workspace.project().read(cx).lsp_store()
|
|
})
|
|
})
|
|
.collect()
|
|
})
|
|
})
|
|
}),
|
|
extension_host_proxy.clone(),
|
|
languages.clone(),
|
|
);
|
|
|
|
Client::set_global(client.clone(), cx);
|
|
|
|
zed::init(cx);
|
|
project::Project::init(&client, cx);
|
|
debugger_ui::init(cx);
|
|
debugger_tools::init(cx);
|
|
client::init(&client, cx);
|
|
let telemetry = client.telemetry();
|
|
telemetry.start(
|
|
system_id.as_ref().map(|id| id.to_string()),
|
|
installation_id.as_ref().map(|id| id.to_string()),
|
|
session_id.clone(),
|
|
cx,
|
|
);
|
|
|
|
// We should rename these in the future to `first app open`, `first app open for release channel`, and `app open`
|
|
if let (Some(system_id), Some(installation_id)) = (&system_id, &installation_id) {
|
|
match (&system_id, &installation_id) {
|
|
(IdType::New(_), IdType::New(_)) => {
|
|
telemetry::event!("App First Opened");
|
|
telemetry::event!("App First Opened For Release Channel");
|
|
}
|
|
(IdType::Existing(_), IdType::New(_)) => {
|
|
telemetry::event!("App First Opened For Release Channel");
|
|
}
|
|
(_, IdType::Existing(_)) => {
|
|
telemetry::event!("App Opened");
|
|
}
|
|
}
|
|
}
|
|
let app_session = cx.new(|cx| AppSession::new(session, cx));
|
|
|
|
let app_state = Arc::new(AppState {
|
|
languages,
|
|
client: client.clone(),
|
|
user_store,
|
|
fs: fs.clone(),
|
|
build_window_options,
|
|
workspace_store,
|
|
node_runtime,
|
|
session: app_session,
|
|
});
|
|
AppState::set_global(Arc::downgrade(&app_state), cx);
|
|
|
|
auto_update::init(client.http_client(), cx);
|
|
dap_adapters::init(cx);
|
|
auto_update_ui::init(cx);
|
|
reliability::init(
|
|
client.http_client(),
|
|
system_id.as_ref().map(|id| id.to_string()),
|
|
installation_id.clone().map(|id| id.to_string()),
|
|
session_id.clone(),
|
|
cx,
|
|
);
|
|
|
|
SystemAppearance::init(cx);
|
|
theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
|
|
theme_extension::init(
|
|
extension_host_proxy.clone(),
|
|
ThemeRegistry::global(cx),
|
|
cx.background_executor().clone(),
|
|
);
|
|
command_palette::init(cx);
|
|
let copilot_language_server_id = app_state.languages.next_language_server_id();
|
|
copilot::init(
|
|
copilot_language_server_id,
|
|
app_state.fs.clone(),
|
|
app_state.client.http_client(),
|
|
app_state.node_runtime.clone(),
|
|
cx,
|
|
);
|
|
supermaven::init(app_state.client.clone(), cx);
|
|
language_model::init(app_state.client.clone(), cx);
|
|
language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
|
|
agent_settings::init(cx);
|
|
agent_servers::init(cx);
|
|
web_search::init(cx);
|
|
web_search_providers::init(app_state.client.clone(), cx);
|
|
snippet_provider::init(cx);
|
|
edit_prediction_registry::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
|
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx);
|
|
agent_ui::init(
|
|
app_state.fs.clone(),
|
|
app_state.client.clone(),
|
|
prompt_builder.clone(),
|
|
app_state.languages.clone(),
|
|
false,
|
|
cx,
|
|
);
|
|
assistant_tools::init(app_state.client.http_client(), cx);
|
|
repl::init(app_state.fs.clone(), cx);
|
|
extension_host::init(
|
|
extension_host_proxy,
|
|
app_state.fs.clone(),
|
|
app_state.client.clone(),
|
|
app_state.node_runtime.clone(),
|
|
cx,
|
|
);
|
|
recent_projects::init(cx);
|
|
|
|
load_embedded_fonts(cx);
|
|
|
|
app_state.languages.set_theme(cx.theme().clone());
|
|
editor::init(cx);
|
|
image_viewer::init(cx);
|
|
repl::notebook::init(cx);
|
|
diagnostics::init(cx);
|
|
|
|
audio::init(cx);
|
|
workspace::init(app_state.clone(), cx);
|
|
ui_prompt::init(cx);
|
|
|
|
go_to_line::init(cx);
|
|
file_finder::init(cx);
|
|
tab_switcher::init(cx);
|
|
outline::init(cx);
|
|
project_symbols::init(cx);
|
|
project_panel::init(cx);
|
|
outline_panel::init(cx);
|
|
tasks_ui::init(cx);
|
|
snippets_ui::init(cx);
|
|
channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
|
|
search::init(cx);
|
|
vim::init(cx);
|
|
terminal_view::init(cx);
|
|
journal::init(app_state.clone(), cx);
|
|
language_selector::init(cx);
|
|
toolchain_selector::init(cx);
|
|
theme_selector::init(cx);
|
|
settings_profile_selector::init(cx);
|
|
language_tools::init(cx);
|
|
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
|
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
|
collab_ui::init(&app_state, cx);
|
|
git_ui::init(cx);
|
|
jj_ui::init(cx);
|
|
feedback::init(cx);
|
|
markdown_preview::init(cx);
|
|
svg_preview::init(cx);
|
|
onboarding::init(cx);
|
|
settings_ui::init(cx);
|
|
extensions_ui::init(cx);
|
|
zeta::init(cx);
|
|
inspector_ui::init(app_state.clone(), cx);
|
|
|
|
cx.observe_global::<SettingsStore>({
|
|
let fs = fs.clone();
|
|
let languages = app_state.languages.clone();
|
|
let http = app_state.client.http_client();
|
|
let client = app_state.client.clone();
|
|
move |cx| {
|
|
for &mut window in cx.windows().iter_mut() {
|
|
let background_appearance = cx.theme().window_background_appearance();
|
|
window
|
|
.update(cx, |_, window, _| {
|
|
window.set_background_appearance(background_appearance)
|
|
})
|
|
.ok();
|
|
}
|
|
|
|
eager_load_active_theme_and_icon_theme(fs.clone(), cx);
|
|
|
|
languages.set_theme(cx.theme().clone());
|
|
let new_host = &client::ClientSettings::get_global(cx).server_url;
|
|
if &http.base_url() != new_host {
|
|
http.set_base_url(new_host);
|
|
if client.status().borrow().is_connected() {
|
|
client.reconnect(&cx.to_async());
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
telemetry::event!(
|
|
"Settings Changed",
|
|
setting = "theme",
|
|
value = cx.theme().name.to_string()
|
|
);
|
|
telemetry::event!(
|
|
"Settings Changed",
|
|
setting = "keymap",
|
|
value = BaseKeymap::get_global(cx).to_string()
|
|
);
|
|
telemetry.flush_events().detach();
|
|
|
|
let fs = app_state.fs.clone();
|
|
load_user_themes_in_background(fs.clone(), cx);
|
|
watch_themes(fs.clone(), cx);
|
|
watch_languages(fs.clone(), app_state.languages.clone(), cx);
|
|
|
|
cx.set_menus(app_menus());
|
|
initialize_workspace(app_state.clone(), prompt_builder, cx);
|
|
|
|
cx.activate(true);
|
|
|
|
cx.spawn({
|
|
let client = app_state.client.clone();
|
|
async move |cx| authenticate(client, cx).await
|
|
})
|
|
.detach_and_log_err(cx);
|
|
|
|
let urls: Vec<_> = args
|
|
.paths_or_urls
|
|
.iter()
|
|
.filter_map(|arg| parse_url_arg(arg, cx).log_err())
|
|
.collect();
|
|
|
|
let diff_paths: Vec<[String; 2]> = args
|
|
.diff
|
|
.chunks(2)
|
|
.map(|chunk| [chunk[0].clone(), chunk[1].clone()])
|
|
.collect();
|
|
|
|
if !urls.is_empty() || !diff_paths.is_empty() {
|
|
open_listener.open(RawOpenRequest { urls, diff_paths })
|
|
}
|
|
|
|
match open_rx
|
|
.try_next()
|
|
.ok()
|
|
.flatten()
|
|
.and_then(|request| OpenRequest::parse(request, cx).log_err())
|
|
{
|
|
Some(request) => {
|
|
handle_open_request(request, app_state.clone(), cx);
|
|
}
|
|
None => {
|
|
cx.spawn({
|
|
let app_state = app_state.clone();
|
|
async move |cx| {
|
|
if let Err(e) = restore_or_create_workspace(app_state, cx).await {
|
|
fail_to_open_window_async(e, cx)
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
}
|
|
|
|
let app_state = app_state.clone();
|
|
|
|
crate::zed::component_preview::init(app_state.clone(), cx);
|
|
|
|
cx.spawn(async move |cx| {
|
|
while let Some(urls) = open_rx.next().await {
|
|
cx.update(|cx| {
|
|
if let Some(request) = OpenRequest::parse(urls, cx).log_err() {
|
|
handle_open_request(request, app_state.clone(), cx);
|
|
}
|
|
})
|
|
.ok();
|
|
}
|
|
})
|
|
.detach();
|
|
});
|
|
}
|
|
|
|
fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut App) {
|
|
if let Some(kind) = request.kind {
|
|
match kind {
|
|
OpenRequestKind::CliConnection(connection) => {
|
|
cx.spawn(async move |cx| handle_cli_connection(connection, app_state, cx).await)
|
|
.detach();
|
|
}
|
|
OpenRequestKind::Extension { extension_id } => {
|
|
cx.spawn(async move |cx| {
|
|
let workspace =
|
|
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
|
|
workspace.update(cx, |_, window, cx| {
|
|
window.dispatch_action(
|
|
Box::new(zed_actions::Extensions {
|
|
category_filter: None,
|
|
id: Some(extension_id),
|
|
}),
|
|
cx,
|
|
);
|
|
})
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
OpenRequestKind::AgentPanel => {
|
|
cx.spawn(async move |cx| {
|
|
let workspace =
|
|
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
|
|
workspace.update(cx, |workspace, window, cx| {
|
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
|
panel.focus_handle(cx).focus(window);
|
|
}
|
|
})
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
OpenRequestKind::DockMenuAction { index } => {
|
|
cx.perform_dock_menu_action(index);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if let Some(connection_options) = request.ssh_connection {
|
|
cx.spawn(async move |cx| {
|
|
let paths: Vec<PathBuf> = request.open_paths.into_iter().map(PathBuf::from).collect();
|
|
open_ssh_project(
|
|
connection_options,
|
|
paths,
|
|
app_state,
|
|
workspace::OpenOptions::default(),
|
|
cx,
|
|
)
|
|
.await
|
|
})
|
|
.detach_and_log_err(cx);
|
|
return;
|
|
}
|
|
|
|
let mut task = None;
|
|
if !request.open_paths.is_empty() || !request.diff_paths.is_empty() {
|
|
let app_state = app_state.clone();
|
|
task = Some(cx.spawn(async move |cx| {
|
|
let paths_with_position =
|
|
derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await;
|
|
let (_window, results) = open_paths_with_positions(
|
|
&paths_with_position,
|
|
&request.diff_paths,
|
|
app_state,
|
|
workspace::OpenOptions::default(),
|
|
cx,
|
|
)
|
|
.await?;
|
|
for result in results.into_iter().flatten() {
|
|
if let Err(err) = result {
|
|
log::error!("Error opening path: {err}",);
|
|
}
|
|
}
|
|
anyhow::Ok(())
|
|
}));
|
|
}
|
|
|
|
if !request.open_channel_notes.is_empty() || request.join_channel.is_some() {
|
|
cx.spawn(async move |cx| {
|
|
let result = maybe!(async {
|
|
if let Some(task) = task {
|
|
task.await?;
|
|
}
|
|
let client = app_state.client.clone();
|
|
// we continue even if authentication fails as join_channel/ open channel notes will
|
|
// show a visible error message.
|
|
authenticate(client, cx).await.log_err();
|
|
|
|
if let Some(channel_id) = request.join_channel {
|
|
cx.update(|cx| {
|
|
workspace::join_channel(
|
|
client::ChannelId(channel_id),
|
|
app_state.clone(),
|
|
None,
|
|
cx,
|
|
)
|
|
})?
|
|
.await?;
|
|
}
|
|
|
|
let workspace_window =
|
|
workspace::get_any_active_workspace(app_state, cx.clone()).await?;
|
|
let workspace = workspace_window.entity(cx)?;
|
|
|
|
let mut promises = Vec::new();
|
|
for (channel_id, heading) in request.open_channel_notes {
|
|
promises.push(cx.update_window(workspace_window.into(), |_, window, cx| {
|
|
ChannelView::open(
|
|
client::ChannelId(channel_id),
|
|
heading,
|
|
workspace.clone(),
|
|
window,
|
|
cx,
|
|
)
|
|
.log_err()
|
|
})?)
|
|
}
|
|
future::join_all(promises).await;
|
|
anyhow::Ok(())
|
|
})
|
|
.await;
|
|
if let Err(err) = result {
|
|
fail_to_open_window_async(err, cx);
|
|
}
|
|
})
|
|
.detach()
|
|
} else if let Some(task) = task {
|
|
cx.spawn(async move |cx| {
|
|
if let Err(err) = task.await {
|
|
fail_to_open_window_async(err, cx);
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
}
|
|
|
|
async fn authenticate(client: Arc<Client>, cx: &AsyncApp) -> Result<()> {
|
|
if stdout_is_a_pty() {
|
|
if client::IMPERSONATE_LOGIN.is_some() {
|
|
client.sign_in_with_optional_connect(false, cx).await?;
|
|
} else if client.has_credentials(cx).await {
|
|
client.sign_in_with_optional_connect(true, cx).await?;
|
|
}
|
|
} else if client.has_credentials(cx).await {
|
|
client.sign_in_with_optional_connect(true, cx).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn system_id() -> Result<IdType> {
|
|
let key_name = "system_id".to_string();
|
|
|
|
if let Ok(Some(system_id)) = GLOBAL_KEY_VALUE_STORE.read_kvp(&key_name) {
|
|
return Ok(IdType::Existing(system_id));
|
|
}
|
|
|
|
let system_id = Uuid::new_v4().to_string();
|
|
|
|
GLOBAL_KEY_VALUE_STORE
|
|
.write_kvp(key_name, system_id.clone())
|
|
.await?;
|
|
|
|
Ok(IdType::New(system_id))
|
|
}
|
|
|
|
async fn installation_id() -> Result<IdType> {
|
|
let legacy_key_name = "device_id".to_string();
|
|
let key_name = "installation_id".to_string();
|
|
|
|
// Migrate legacy key to new key
|
|
if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(&legacy_key_name) {
|
|
KEY_VALUE_STORE
|
|
.write_kvp(key_name, installation_id.clone())
|
|
.await?;
|
|
KEY_VALUE_STORE.delete_kvp(legacy_key_name).await?;
|
|
return Ok(IdType::Existing(installation_id));
|
|
}
|
|
|
|
if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(&key_name) {
|
|
return Ok(IdType::Existing(installation_id));
|
|
}
|
|
|
|
let installation_id = Uuid::new_v4().to_string();
|
|
|
|
KEY_VALUE_STORE
|
|
.write_kvp(key_name, installation_id.clone())
|
|
.await?;
|
|
|
|
Ok(IdType::New(installation_id))
|
|
}
|
|
|
|
async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp) -> Result<()> {
|
|
if let Some(locations) = restorable_workspace_locations(cx, &app_state).await {
|
|
let mut tasks = Vec::new();
|
|
|
|
for location in locations {
|
|
match location {
|
|
SerializedWorkspaceLocation::Local(location, _) => {
|
|
let app_state = app_state.clone();
|
|
let paths = location.paths().to_vec();
|
|
let task = cx.spawn(async move |cx| {
|
|
let open_task = cx.update(|cx| {
|
|
workspace::open_paths(
|
|
&paths,
|
|
app_state,
|
|
workspace::OpenOptions::default(),
|
|
cx,
|
|
)
|
|
})?;
|
|
open_task.await.map(|_| ())
|
|
});
|
|
tasks.push(task);
|
|
}
|
|
SerializedWorkspaceLocation::Ssh(ssh) => {
|
|
let app_state = app_state.clone();
|
|
let ssh_host = ssh.host.clone();
|
|
let task = cx.spawn(async move |cx| {
|
|
let connection_options = cx.update(|cx| {
|
|
SshSettings::get_global(cx)
|
|
.connection_options_for(ssh.host, ssh.port, ssh.user)
|
|
});
|
|
|
|
match connection_options {
|
|
Ok(connection_options) => recent_projects::open_ssh_project(
|
|
connection_options,
|
|
ssh.paths.into_iter().map(PathBuf::from).collect(),
|
|
app_state,
|
|
workspace::OpenOptions::default(),
|
|
cx,
|
|
)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!(e)),
|
|
Err(e) => Err(anyhow::anyhow!(
|
|
"Failed to get SSH connection options for {}: {}",
|
|
ssh_host,
|
|
e
|
|
)),
|
|
}
|
|
});
|
|
tasks.push(task);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wait for all workspaces to open concurrently
|
|
let results = future::join_all(tasks).await;
|
|
|
|
// Show notifications for any errors that occurred
|
|
let mut error_count = 0;
|
|
for result in results {
|
|
if let Err(e) = result {
|
|
log::error!("Failed to restore workspace: {}", e);
|
|
error_count += 1;
|
|
}
|
|
}
|
|
|
|
if error_count > 0 {
|
|
let message = if error_count == 1 {
|
|
"Failed to restore 1 workspace. Check logs for details.".to_string()
|
|
} else {
|
|
format!(
|
|
"Failed to restore {} workspaces. Check logs for details.",
|
|
error_count
|
|
)
|
|
};
|
|
|
|
// Try to find an active workspace to show the toast
|
|
let toast_shown = cx
|
|
.update(|cx| {
|
|
if let Some(window) = cx.active_window()
|
|
&& let Some(workspace) = window.downcast::<Workspace>()
|
|
{
|
|
workspace
|
|
.update(cx, |workspace, _, cx| {
|
|
workspace.show_toast(
|
|
Toast::new(NotificationId::unique::<()>(), message),
|
|
cx,
|
|
)
|
|
})
|
|
.ok();
|
|
return true;
|
|
}
|
|
false
|
|
})
|
|
.unwrap_or(false);
|
|
|
|
// If we couldn't show a toast (no windows opened successfully),
|
|
// we've already logged the errors above, so the user can check logs
|
|
if !toast_shown {
|
|
log::error!(
|
|
"Failed to show notification for window restoration errors, because no workspace windows were available."
|
|
);
|
|
}
|
|
}
|
|
} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
|
|
cx.update(|cx| show_onboarding_view(app_state, cx))?.await?;
|
|
} else {
|
|
cx.update(|cx| {
|
|
workspace::open_new(
|
|
Default::default(),
|
|
app_state,
|
|
cx,
|
|
|workspace, window, cx| {
|
|
Editor::new_file(workspace, &Default::default(), window, cx)
|
|
},
|
|
)
|
|
})?
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn restorable_workspace_locations(
|
|
cx: &mut AsyncApp,
|
|
app_state: &Arc<AppState>,
|
|
) -> Option<Vec<SerializedWorkspaceLocation>> {
|
|
let mut restore_behavior = cx
|
|
.update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup)
|
|
.ok()?;
|
|
|
|
let session_handle = app_state.session.clone();
|
|
let (last_session_id, last_session_window_stack) = cx
|
|
.update(|cx| {
|
|
let session = session_handle.read(cx);
|
|
|
|
(
|
|
session.last_session_id().map(|id| id.to_string()),
|
|
session.last_session_window_stack(),
|
|
)
|
|
})
|
|
.ok()?;
|
|
|
|
if last_session_id.is_none()
|
|
&& matches!(
|
|
restore_behavior,
|
|
workspace::RestoreOnStartupBehavior::LastSession
|
|
)
|
|
{
|
|
restore_behavior = workspace::RestoreOnStartupBehavior::LastWorkspace;
|
|
}
|
|
|
|
match restore_behavior {
|
|
workspace::RestoreOnStartupBehavior::LastWorkspace => {
|
|
workspace::last_opened_workspace_location()
|
|
.await
|
|
.map(|location| vec![location])
|
|
}
|
|
workspace::RestoreOnStartupBehavior::LastSession => {
|
|
if let Some(last_session_id) = last_session_id {
|
|
let ordered = last_session_window_stack.is_some();
|
|
|
|
let mut locations = workspace::last_session_workspace_locations(
|
|
&last_session_id,
|
|
last_session_window_stack,
|
|
)
|
|
.filter(|locations| !locations.is_empty());
|
|
|
|
// Since last_session_window_order returns the windows ordered front-to-back
|
|
// we need to open the window that was frontmost last.
|
|
if ordered && let Some(locations) = locations.as_mut() {
|
|
locations.reverse();
|
|
}
|
|
|
|
locations
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn init_paths() -> HashMap<io::ErrorKind, Vec<&'static Path>> {
|
|
[
|
|
paths::config_dir(),
|
|
paths::extensions_dir(),
|
|
paths::languages_dir(),
|
|
paths::debug_adapters_dir(),
|
|
paths::database_dir(),
|
|
paths::logs_dir(),
|
|
paths::temp_dir(),
|
|
]
|
|
.into_iter()
|
|
.fold(HashMap::default(), |mut errors, path| {
|
|
if let Err(e) = std::fs::create_dir_all(path) {
|
|
errors.entry(e.kind()).or_insert_with(Vec::new).push(path);
|
|
}
|
|
errors
|
|
})
|
|
}
|
|
|
|
pub fn stdout_is_a_pty() -> bool {
|
|
std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && io::stdout().is_terminal()
|
|
}
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(name = "zed", disable_version_flag = true)]
|
|
struct Args {
|
|
/// A sequence of space-separated paths or urls that you want to open.
|
|
///
|
|
/// Use `path:line:row` syntax to open a file at a specific location.
|
|
/// Non-existing paths and directories will ignore `:line:row` suffix.
|
|
///
|
|
/// URLs can either be `file://` or `zed://` scheme, or relative to <https://zed.dev>.
|
|
paths_or_urls: Vec<String>,
|
|
|
|
/// Pairs of file paths to diff. Can be specified multiple times.
|
|
#[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])]
|
|
diff: Vec<String>,
|
|
|
|
/// Sets a custom directory for all user data (e.g., database, extensions, logs).
|
|
/// This overrides the default platform-specific data directory location.
|
|
/// On macOS, the default is `~/Library/Application Support/Zed`.
|
|
/// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`.
|
|
/// On Windows, the default is `%LOCALAPPDATA%\Zed`.
|
|
#[arg(long, value_name = "DIR")]
|
|
user_data_dir: Option<String>,
|
|
|
|
/// Instructs zed to run as a dev server on this machine. (not implemented)
|
|
#[arg(long)]
|
|
dev_server_token: Option<String>,
|
|
|
|
/// Prints system specs. Useful for submitting issues on GitHub when encountering a bug
|
|
/// that prevents Zed from starting, so you can't run `zed: copy system specs to clipboard`
|
|
#[arg(long)]
|
|
system_specs: bool,
|
|
|
|
/// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
|
|
/// by having Zed act like netcat communicating over a Unix socket.
|
|
#[arg(long, hide = true)]
|
|
askpass: Option<String>,
|
|
|
|
/// Used for the MCP Server, to remove the need for netcat as a dependency,
|
|
/// by having Zed act like netcat communicating over a Unix socket.
|
|
#[arg(long, hide = true)]
|
|
nc: Option<String>,
|
|
|
|
/// Used for recording minidumps on crashes by having Zed run a separate
|
|
/// process communicating over a socket.
|
|
#[arg(long, hide = true)]
|
|
crash_handler: Option<PathBuf>,
|
|
|
|
/// Run zed in the foreground, only used on Windows, to match the behavior on macOS.
|
|
#[arg(long)]
|
|
#[cfg(target_os = "windows")]
|
|
#[arg(hide = true)]
|
|
foreground: bool,
|
|
|
|
/// The dock action to perform. This is used on Windows only.
|
|
#[arg(long)]
|
|
#[cfg(target_os = "windows")]
|
|
#[arg(hide = true)]
|
|
dock_action: Option<usize>,
|
|
|
|
#[arg(long, hide = true)]
|
|
dump_all_actions: bool,
|
|
|
|
/// Output current environment variables as JSON to stdout
|
|
#[arg(long, hide = true)]
|
|
printenv: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum IdType {
|
|
New(String),
|
|
Existing(String),
|
|
}
|
|
|
|
impl ToString for IdType {
|
|
fn to_string(&self) -> String {
|
|
match self {
|
|
IdType::New(id) | IdType::Existing(id) => id.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_url_arg(arg: &str, cx: &App) -> Result<String> {
|
|
match std::fs::canonicalize(Path::new(&arg)) {
|
|
Ok(path) => Ok(format!("file://{}", path.display())),
|
|
Err(error) => {
|
|
if arg.starts_with("file://")
|
|
|| arg.starts_with("zed-cli://")
|
|
|| arg.starts_with("ssh://")
|
|
|| parse_zed_link(arg, cx).is_some()
|
|
{
|
|
Ok(arg.into())
|
|
} else {
|
|
anyhow::bail!("error parsing path argument: {error}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn load_embedded_fonts(cx: &App) {
|
|
let asset_source = cx.asset_source();
|
|
let font_paths = asset_source.list("fonts").unwrap();
|
|
let embedded_fonts = Mutex::new(Vec::new());
|
|
let executor = cx.background_executor();
|
|
|
|
executor.block(executor.scoped(|scope| {
|
|
for font_path in &font_paths {
|
|
if !font_path.ends_with(".ttf") {
|
|
continue;
|
|
}
|
|
|
|
scope.spawn(async {
|
|
let font_bytes = asset_source.load(font_path).unwrap().unwrap();
|
|
embedded_fonts.lock().push(font_bytes);
|
|
});
|
|
}
|
|
}));
|
|
|
|
cx.text_system()
|
|
.add_fonts(embedded_fonts.into_inner())
|
|
.unwrap();
|
|
}
|
|
|
|
/// Eagerly loads the active theme and icon theme based on the selections in the
|
|
/// theme settings.
|
|
///
|
|
/// This fast path exists to load these themes as soon as possible so the user
|
|
/// doesn't see the default themes while waiting on extensions to load.
|
|
fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &App) {
|
|
let extension_store = ExtensionStore::global(cx);
|
|
let theme_registry = ThemeRegistry::global(cx);
|
|
let theme_settings = ThemeSettings::get_global(cx);
|
|
let appearance = SystemAppearance::global(cx).0;
|
|
|
|
if let Some(theme_selection) = theme_settings.theme_selection.as_ref() {
|
|
let theme_name = theme_selection.theme(appearance);
|
|
if matches!(theme_registry.get(theme_name), Err(ThemeNotFoundError(_)))
|
|
&& let Some(theme_path) = extension_store.read(cx).path_to_extension_theme(theme_name)
|
|
{
|
|
cx.spawn({
|
|
let theme_registry = theme_registry.clone();
|
|
let fs = fs.clone();
|
|
async move |cx| {
|
|
theme_registry.load_user_theme(&theme_path, fs).await?;
|
|
|
|
cx.update(|cx| {
|
|
ThemeSettings::reload_current_theme(cx);
|
|
})
|
|
}
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
}
|
|
|
|
if let Some(icon_theme_selection) = theme_settings.icon_theme_selection.as_ref() {
|
|
let icon_theme_name = icon_theme_selection.icon_theme(appearance);
|
|
if matches!(
|
|
theme_registry.get_icon_theme(icon_theme_name),
|
|
Err(IconThemeNotFoundError(_))
|
|
) && let Some((icon_theme_path, icons_root_path)) = extension_store
|
|
.read(cx)
|
|
.path_to_extension_icon_theme(icon_theme_name)
|
|
{
|
|
cx.spawn({
|
|
let fs = fs.clone();
|
|
async move |cx| {
|
|
theme_registry
|
|
.load_icon_theme(&icon_theme_path, &icons_root_path, fs)
|
|
.await?;
|
|
|
|
cx.update(|cx| {
|
|
ThemeSettings::reload_current_icon_theme(cx);
|
|
})
|
|
}
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Spawns a background task to load the user themes from the themes directory.
|
|
fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut App) {
|
|
cx.spawn({
|
|
let fs = fs.clone();
|
|
async move |cx| {
|
|
if let Some(theme_registry) = cx.update(|cx| ThemeRegistry::global(cx)).log_err() {
|
|
let themes_dir = paths::themes_dir().as_ref();
|
|
match fs
|
|
.metadata(themes_dir)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.map(|m| m.is_dir)
|
|
{
|
|
Some(is_dir) => {
|
|
anyhow::ensure!(is_dir, "Themes dir path {themes_dir:?} is not a directory")
|
|
}
|
|
None => {
|
|
fs.create_dir(themes_dir).await.with_context(|| {
|
|
format!("Failed to create themes dir at path {themes_dir:?}")
|
|
})?;
|
|
}
|
|
}
|
|
theme_registry.load_user_themes(themes_dir, fs).await?;
|
|
cx.update(ThemeSettings::reload_current_theme)?;
|
|
}
|
|
anyhow::Ok(())
|
|
}
|
|
})
|
|
.detach_and_log_err(cx);
|
|
}
|
|
|
|
/// Spawns a background task to watch the themes directory for changes.
|
|
fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut App) {
|
|
use std::time::Duration;
|
|
cx.spawn(async move |cx| {
|
|
let (mut events, _) = fs
|
|
.watch(paths::themes_dir(), Duration::from_millis(100))
|
|
.await;
|
|
|
|
while let Some(paths) = events.next().await {
|
|
for event in paths {
|
|
if fs.metadata(&event.path).await.ok().flatten().is_some()
|
|
&& let Some(theme_registry) =
|
|
cx.update(|cx| ThemeRegistry::global(cx)).log_err()
|
|
&& let Some(()) = theme_registry
|
|
.load_user_theme(&event.path, fs.clone())
|
|
.await
|
|
.log_err()
|
|
{
|
|
cx.update(ThemeSettings::reload_current_theme).log_err();
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.detach()
|
|
}
|
|
|
|
#[cfg(debug_assertions)]
|
|
fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>, cx: &mut App) {
|
|
use std::time::Duration;
|
|
|
|
let path = {
|
|
let p = Path::new("crates/languages/src");
|
|
let Ok(full_path) = p.canonicalize() else {
|
|
return;
|
|
};
|
|
full_path
|
|
};
|
|
|
|
cx.spawn(async move |_| {
|
|
let (mut events, _) = fs.watch(path.as_path(), Duration::from_millis(100)).await;
|
|
while let Some(event) = events.next().await {
|
|
let has_language_file = event.iter().any(|event| {
|
|
event
|
|
.path
|
|
.extension()
|
|
.map(|ext| ext.to_string_lossy().as_ref() == "scm")
|
|
.unwrap_or(false)
|
|
});
|
|
if has_language_file {
|
|
languages.reload();
|
|
}
|
|
}
|
|
})
|
|
.detach()
|
|
}
|
|
|
|
#[cfg(not(debug_assertions))]
|
|
fn watch_languages(_fs: Arc<dyn fs::Fs>, _languages: Arc<LanguageRegistry>, _cx: &mut App) {}
|
|
|
|
fn dump_all_gpui_actions() {
|
|
#[derive(Debug, serde::Serialize)]
|
|
struct ActionDef {
|
|
name: &'static str,
|
|
human_name: String,
|
|
aliases: &'static [&'static str],
|
|
documentation: Option<&'static str>,
|
|
}
|
|
let mut actions = gpui::generate_list_of_all_registered_actions()
|
|
.map(|action| ActionDef {
|
|
name: action.name,
|
|
human_name: command_palette::humanize_action_name(action.name),
|
|
aliases: action.deprecated_aliases,
|
|
documentation: action.documentation,
|
|
})
|
|
.collect::<Vec<ActionDef>>();
|
|
|
|
actions.sort_by_key(|a| a.name);
|
|
|
|
io::Write::write(
|
|
&mut std::io::stdout(),
|
|
serde_json::to_string_pretty(&actions).unwrap().as_bytes(),
|
|
)
|
|
.unwrap();
|
|
}
|