1mod app_menus;
2pub mod component_preview;
3pub mod edit_prediction_registry;
4#[cfg(target_os = "macos")]
5pub(crate) mod mac_only_instance;
6mod migrate;
7mod open_listener;
8mod quick_action_bar;
9#[cfg(target_os = "windows")]
10pub(crate) mod windows_only_instance;
11
12use agent_ui::{AgentDiffToolbar, AgentPanelDelegate};
13use agent_ui_v2::agents_panel::AgentsPanel;
14use anyhow::Context as _;
15pub use app_menus::*;
16use assets::Assets;
17use audio::{AudioSettings, REPLAY_DURATION};
18use breadcrumbs::Breadcrumbs;
19use client::zed_urls;
20use collections::VecDeque;
21use debugger_ui::debugger_panel::DebugPanel;
22use editor::{Editor, MultiBuffer};
23use extension_host::ExtensionStore;
24use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag};
25use fs::Fs;
26use futures::FutureExt as _;
27use futures::future::Either;
28use futures::{StreamExt, channel::mpsc, select_biased};
29use git_ui::commit_view::CommitViewToolbar;
30use git_ui::git_panel::GitPanel;
31use git_ui::project_diff::ProjectDiffToolbar;
32use gpui::{
33 Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity,
34 Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString,
35 Task, TitlebarOptions, UpdateGlobal, WeakEntity, Window, WindowKind, WindowOptions, actions,
36 image_cache, point, px, retain_all,
37};
38use image_viewer::ImageInfo;
39use language::Capability;
40use language_onboarding::BasedPyrightBanner;
41use language_tools::lsp_button::{self, LspButton};
42use language_tools::lsp_log_view::LspLogToolbarItemView;
43use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
44use migrator::migrate_keymap;
45use onboarding::DOCS_URL;
46use onboarding::multibuffer_hint::MultibufferHint;
47pub use open_listener::*;
48use outline_panel::OutlinePanel;
49use paths::{
50 local_debug_file_relative_path, local_settings_file_relative_path,
51 local_tasks_file_relative_path,
52};
53use project::{DirectoryLister, DisableAiSettings, ProjectItem};
54use project_panel::ProjectPanel;
55use prompt_store::PromptBuilder;
56use quick_action_bar::QuickActionBar;
57use recent_projects::open_remote_project;
58use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
59use rope::Rope;
60use search::project_search::ProjectSearchBar;
61use settings::{
62 BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile,
63 KeymapFileLoadResult, MigrationStatus, Settings, SettingsStore, VIM_KEYMAP_PATH,
64 initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content,
65 update_settings_file,
66};
67use std::time::Duration;
68use std::{
69 borrow::Cow,
70 path::{Path, PathBuf},
71 sync::Arc,
72 sync::atomic::{self, AtomicBool},
73};
74use terminal_view::terminal_panel::{self, TerminalPanel};
75use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
76use ui::{PopoverMenuHandle, prelude::*};
77use util::markdown::MarkdownString;
78use util::rel_path::RelPath;
79use util::{ResultExt, asset_str};
80use uuid::Uuid;
81use vim_mode_setting::VimModeSetting;
82use workspace::notifications::{
83 NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification,
84};
85use workspace::utility_pane::utility_slot_for_dock_position;
86use workspace::{
87 AppState, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace, WorkspaceSettings,
88 create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
89 open_new,
90};
91use workspace::{
92 CloseIntent, CloseWindow, NotificationFrame, RestoreBanner, with_active_or_new_workspace,
93};
94use workspace::{Pane, notifications::DetachAndPromptErr};
95use zed_actions::{
96 OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettingsFile, OpenZedUrl,
97 Quit,
98};
99
100actions!(
101 zed,
102 [
103 /// Opens the element inspector for debugging UI.
104 DebugElements,
105 /// Hides the application window.
106 Hide,
107 /// Hides all other application windows.
108 HideOthers,
109 /// Minimizes the current window.
110 Minimize,
111 /// Opens the default settings file.
112 OpenDefaultSettings,
113 /// Opens project-specific settings file.
114 OpenProjectSettingsFile,
115 /// Opens the project tasks configuration.
116 OpenProjectTasks,
117 /// Opens the tasks panel.
118 OpenTasks,
119 /// Opens debug tasks configuration.
120 OpenDebugTasks,
121 /// Resets the application database.
122 ResetDatabase,
123 /// Shows all hidden windows.
124 ShowAll,
125 /// Toggles fullscreen mode.
126 ToggleFullScreen,
127 /// Zooms the window.
128 Zoom,
129 /// Triggers a test panic for debugging.
130 TestPanic,
131 /// Triggers a hard crash for debugging.
132 TestCrash,
133 ]
134);
135
136actions!(
137 dev,
138 [
139 /// Stores last 30s of audio from zed staff using the experimental rodio
140 /// audio system (including yourself) on the current call in a tar file
141 /// in the current working directory.
142 CaptureRecentAudio,
143 ]
144);
145
146pub fn init(cx: &mut App) {
147 #[cfg(target_os = "macos")]
148 cx.on_action(|_: &Hide, cx| cx.hide());
149 #[cfg(target_os = "macos")]
150 cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps());
151 #[cfg(target_os = "macos")]
152 cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps());
153 cx.on_action(quit);
154
155 cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx));
156 let flag = cx.wait_for_flag::<PanicFeatureFlag>();
157 cx.spawn(async |cx| {
158 if cx
159 .update(|cx| ReleaseChannel::global(cx) == ReleaseChannel::Dev)
160 .unwrap_or_default()
161 || flag.await
162 {
163 cx.update(|cx| {
164 cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action"))
165 .on_action(|_: &TestCrash, _| {
166 unsafe extern "C" {
167 fn puts(s: *const i8);
168 }
169 unsafe {
170 puts(0xabad1d3a as *const i8);
171 }
172 });
173 })
174 .ok();
175 };
176 })
177 .detach();
178 cx.on_action(|_: &OpenLog, cx| {
179 with_active_or_new_workspace(cx, |workspace, window, cx| {
180 open_log_file(workspace, window, cx);
181 });
182 })
183 .on_action(|_: &workspace::RevealLogInFileManager, cx| {
184 cx.reveal_path(paths::log_file().as_path());
185 })
186 .on_action(|_: &zed_actions::OpenLicenses, cx| {
187 with_active_or_new_workspace(cx, |workspace, window, cx| {
188 open_bundled_file(
189 workspace,
190 asset_str::<Assets>("licenses.md"),
191 "Open Source License Attribution",
192 "Markdown",
193 window,
194 cx,
195 );
196 });
197 })
198 .on_action(|_: &zed_actions::OpenTelemetryLog, cx| {
199 with_active_or_new_workspace(cx, |workspace, window, cx| {
200 open_telemetry_log_file(workspace, window, cx);
201 });
202 })
203 .on_action(|&zed_actions::OpenKeymapFile, cx| {
204 with_active_or_new_workspace(cx, |_, window, cx| {
205 open_settings_file(
206 paths::keymap_file(),
207 || settings::initial_keymap_content().as_ref().into(),
208 window,
209 cx,
210 );
211 });
212 })
213 .on_action(|_: &OpenSettingsFile, cx| {
214 with_active_or_new_workspace(cx, |_, window, cx| {
215 open_settings_file(
216 paths::settings_file(),
217 || settings::initial_user_settings_content().as_ref().into(),
218 window,
219 cx,
220 );
221 });
222 })
223 .on_action(|_: &OpenAccountSettings, cx| {
224 with_active_or_new_workspace(cx, |_, _, cx| {
225 cx.open_url(&zed_urls::account_url(cx));
226 });
227 })
228 .on_action(|_: &OpenTasks, cx| {
229 with_active_or_new_workspace(cx, |_, window, cx| {
230 open_settings_file(
231 paths::tasks_file(),
232 || settings::initial_tasks_content().as_ref().into(),
233 window,
234 cx,
235 );
236 });
237 })
238 .on_action(|_: &OpenDebugTasks, cx| {
239 with_active_or_new_workspace(cx, |_, window, cx| {
240 open_settings_file(
241 paths::debug_scenarios_file(),
242 || settings::initial_debug_tasks_content().as_ref().into(),
243 window,
244 cx,
245 );
246 });
247 })
248 .on_action(|_: &OpenDefaultSettings, cx| {
249 with_active_or_new_workspace(cx, |workspace, window, cx| {
250 open_bundled_file(
251 workspace,
252 settings::default_settings(),
253 "Default Settings",
254 "JSON",
255 window,
256 cx,
257 );
258 });
259 })
260 .on_action(|_: &zed_actions::OpenDefaultKeymap, cx| {
261 with_active_or_new_workspace(cx, |workspace, window, cx| {
262 open_bundled_file(
263 workspace,
264 settings::default_keymap(),
265 "Default Key Bindings",
266 "JSON",
267 window,
268 cx,
269 );
270 });
271 })
272 .on_action(|_: &zed_actions::About, cx| {
273 with_active_or_new_workspace(cx, |workspace, window, cx| {
274 about(workspace, window, cx);
275 });
276 });
277}
278
279fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {
280 #[cfg(target_os = "macos")]
281 {
282 WorkspaceSettings::get_global(cx)
283 .on_last_window_closed
284 .is_quit_app()
285 .then(|| {
286 cx.on_window_closed(|cx| {
287 if cx.windows().is_empty() {
288 cx.quit();
289 }
290 })
291 })
292 }
293 #[cfg(not(target_os = "macos"))]
294 {
295 Some(cx.on_window_closed(|cx| {
296 if cx.windows().is_empty() {
297 cx.quit();
298 }
299 }))
300 }
301}
302
303pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowOptions {
304 let display = display_uuid.and_then(|uuid| {
305 cx.displays()
306 .into_iter()
307 .find(|display| display.uuid().ok() == Some(uuid))
308 });
309 let app_id = ReleaseChannel::global(cx).app_id();
310 let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
311 Ok(val) if val == "server" => gpui::WindowDecorations::Server,
312 Ok(val) if val == "client" => gpui::WindowDecorations::Client,
313 _ => match WorkspaceSettings::get_global(cx).window_decorations {
314 settings::WindowDecorations::Server => gpui::WindowDecorations::Server,
315 settings::WindowDecorations::Client => gpui::WindowDecorations::Client,
316 },
317 };
318
319 let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs;
320
321 WindowOptions {
322 titlebar: Some(TitlebarOptions {
323 title: None,
324 appears_transparent: true,
325 traffic_light_position: Some(point(px(9.0), px(9.0))),
326 }),
327 window_bounds: None,
328 focus: false,
329 show: false,
330 kind: WindowKind::Normal,
331 is_movable: true,
332 display_id: display.map(|display| display.id()),
333 window_background: cx.theme().window_background_appearance(),
334 app_id: Some(app_id.to_owned()),
335 window_decorations: Some(window_decorations),
336 window_min_size: Some(gpui::Size {
337 width: px(360.0),
338 height: px(240.0),
339 }),
340 tabbing_identifier: if use_system_window_tabs {
341 Some(String::from("zed"))
342 } else {
343 None
344 },
345 ..Default::default()
346 }
347}
348
349pub fn initialize_workspace(
350 app_state: Arc<AppState>,
351 prompt_builder: Arc<PromptBuilder>,
352 cx: &mut App,
353) {
354 let mut _on_close_subscription = bind_on_window_closed(cx);
355 cx.observe_global::<SettingsStore>(move |cx| {
356 // A 1.92 regression causes unused-assignment to trigger on this variable.
357 _ = _on_close_subscription.is_some();
358 _on_close_subscription = bind_on_window_closed(cx);
359 })
360 .detach();
361
362 cx.observe_new(move |workspace: &mut Workspace, window, cx| {
363 let Some(window) = window else {
364 return;
365 };
366
367 let workspace_handle = cx.entity();
368 let center_pane = workspace.active_pane().clone();
369 initialize_pane(workspace, ¢er_pane, window, cx);
370
371 cx.subscribe_in(&workspace_handle, window, {
372 move |workspace, _, event, window, cx| match event {
373 workspace::Event::PaneAdded(pane) => {
374 initialize_pane(workspace, pane, window, cx);
375 }
376 workspace::Event::OpenBundledFile {
377 text,
378 title,
379 language,
380 } => open_bundled_file(workspace, text.clone(), title, language, window, cx),
381 _ => {}
382 }
383 })
384 .detach();
385
386 #[cfg(not(target_os = "macos"))]
387 initialize_file_watcher(window, cx);
388
389 if let Some(specs) = window.gpu_specs() {
390 log::info!("Using GPU: {:?}", specs);
391 show_software_emulation_warning_if_needed(specs.clone(), window, cx);
392 if let Some((crash_server, message)) = crashes::CRASH_HANDLER
393 .get()
394 .zip(bincode::serialize(&specs).ok())
395 && let Err(err) = crash_server.send_message(3, message)
396 {
397 log::warn!(
398 "Failed to store active gpu info for crash reporting: {}",
399 err
400 );
401 }
402 }
403
404 #[cfg(target_os = "windows")]
405 unstable_version_notification(cx);
406
407 let edit_prediction_menu_handle = PopoverMenuHandle::default();
408 let edit_prediction_ui = cx.new(|cx| {
409 edit_prediction_ui::EditPredictionButton::new(
410 app_state.fs.clone(),
411 app_state.user_store.clone(),
412 edit_prediction_menu_handle.clone(),
413 app_state.client.clone(),
414 cx,
415 )
416 });
417 workspace.register_action({
418 move |_, _: &edit_prediction_ui::ToggleMenu, window, cx| {
419 edit_prediction_menu_handle.toggle(window, cx);
420 }
421 });
422
423 let search_button = cx.new(|_| search::search_status_button::SearchButton::new());
424 let diagnostic_summary =
425 cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
426 let activity_indicator = activity_indicator::ActivityIndicator::new(
427 workspace,
428 workspace.project().read(cx).languages().clone(),
429 window,
430 cx,
431 );
432 let active_buffer_language =
433 cx.new(|_| language_selector::ActiveBufferLanguage::new(workspace));
434 let active_toolchain_language =
435 cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx));
436 let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
437 let image_info = cx.new(|_cx| ImageInfo::new(workspace));
438
439 let lsp_button_menu_handle = PopoverMenuHandle::default();
440 let lsp_button =
441 cx.new(|cx| LspButton::new(workspace, lsp_button_menu_handle.clone(), window, cx));
442 workspace.register_action({
443 move |_, _: &lsp_button::ToggleMenu, window, cx| {
444 lsp_button_menu_handle.toggle(window, cx);
445 }
446 });
447
448 let cursor_position =
449 cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
450 let line_ending_indicator =
451 cx.new(|_| line_ending_selector::LineEndingIndicator::default());
452 workspace.status_bar().update(cx, |status_bar, cx| {
453 status_bar.add_left_item(search_button, window, cx);
454 status_bar.add_left_item(lsp_button, window, cx);
455 status_bar.add_left_item(diagnostic_summary, window, cx);
456 status_bar.add_left_item(activity_indicator, window, cx);
457 status_bar.add_right_item(edit_prediction_ui, window, cx);
458 status_bar.add_right_item(active_buffer_language, window, cx);
459 status_bar.add_right_item(active_toolchain_language, window, cx);
460 status_bar.add_right_item(line_ending_indicator, window, cx);
461 status_bar.add_right_item(vim_mode_indicator, window, cx);
462 status_bar.add_right_item(cursor_position, window, cx);
463 status_bar.add_right_item(image_info, window, cx);
464 });
465
466 let handle = cx.entity().downgrade();
467 window.on_window_should_close(cx, move |window, cx| {
468 handle
469 .update(cx, |workspace, cx| {
470 // We'll handle closing asynchronously
471 workspace.close_window(&CloseWindow, window, cx);
472 false
473 })
474 .unwrap_or(true)
475 });
476
477 initialize_panels(prompt_builder.clone(), window, cx);
478 register_actions(app_state.clone(), workspace, window, cx);
479
480 workspace.focus_handle(cx).focus(window, cx);
481 })
482 .detach();
483}
484
485#[cfg(target_os = "windows")]
486fn unstable_version_notification(cx: &mut App) {
487 if !matches!(
488 ReleaseChannel::try_global(cx),
489 Some(ReleaseChannel::Nightly)
490 ) {
491 return;
492 }
493 let db_key = "zed_windows_nightly_notif_shown_at".to_owned();
494 let time = chrono::Utc::now();
495 if let Some(last_shown) = db::kvp::KEY_VALUE_STORE
496 .read_kvp(&db_key)
497 .log_err()
498 .flatten()
499 .and_then(|timestamp| chrono::DateTime::parse_from_rfc3339(×tamp).ok())
500 {
501 if time.fixed_offset() - last_shown < chrono::Duration::days(7) {
502 return;
503 }
504 }
505 cx.spawn(async move |_| {
506 db::kvp::KEY_VALUE_STORE
507 .write_kvp(db_key, time.to_rfc3339())
508 .await
509 })
510 .detach_and_log_err(cx);
511 struct WindowsNightly;
512 show_app_notification(NotificationId::unique::<WindowsNightly>(), cx, |cx| {
513 cx.new(|cx| {
514 MessageNotification::new("You're using an unstable version of Zed (Nightly)", cx)
515 .primary_message("Download Stable")
516 .primary_icon_color(Color::Accent)
517 .primary_icon(IconName::Download)
518 .primary_on_click(|window, cx| {
519 window.dispatch_action(
520 zed_actions::OpenBrowser {
521 url: "https://zed.dev/download".to_string(),
522 }
523 .boxed_clone(),
524 cx,
525 );
526 cx.emit(DismissEvent);
527 })
528 })
529 });
530}
531
532#[cfg(any(target_os = "linux", target_os = "freebsd"))]
533fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
534 if let Err(e) = fs::fs_watcher::global(|_| {}) {
535 let message = format!(
536 db::indoc! {r#"
537 inotify_init returned {}
538
539 This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux
540 "#},
541 e
542 );
543 let prompt = window.prompt(
544 PromptLevel::Critical,
545 "Could not start inotify",
546 Some(&message),
547 &["Troubleshoot and Quit"],
548 cx,
549 );
550 cx.spawn(async move |_, cx| {
551 if prompt.await == Ok(0) {
552 cx.update(|cx| {
553 cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify");
554 cx.quit();
555 })
556 .ok();
557 }
558 })
559 .detach()
560 }
561}
562
563#[cfg(target_os = "windows")]
564fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
565 if let Err(e) = fs::fs_watcher::global(|_| {}) {
566 let message = format!(
567 db::indoc! {r#"
568 ReadDirectoryChangesW initialization failed: {}
569
570 This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows
571 "#},
572 e
573 );
574 let prompt = window.prompt(
575 PromptLevel::Critical,
576 "Could not start ReadDirectoryChangesW",
577 Some(&message),
578 &["Troubleshoot and Quit"],
579 cx,
580 );
581 cx.spawn(async move |_, cx| {
582 if prompt.await == Ok(0) {
583 cx.update(|cx| {
584 cx.open_url("https://zed.dev/docs/windows");
585 cx.quit()
586 })
587 .ok();
588 }
589 })
590 .detach()
591 }
592}
593
594fn show_software_emulation_warning_if_needed(
595 specs: gpui::GpuSpecs,
596 window: &mut Window,
597 cx: &mut Context<Workspace>,
598) {
599 if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() {
600 let (graphics_api, docs_url, open_url) = if cfg!(target_os = "windows") {
601 (
602 "DirectX",
603 "https://zed.dev/docs/windows",
604 "https://zed.dev/docs/windows",
605 )
606 } else {
607 (
608 "Vulkan",
609 "https://zed.dev/docs/linux",
610 "https://zed.dev/docs/linux#zed-fails-to-open-windows",
611 )
612 };
613 let message = format!(
614 db::indoc! {r#"
615 Zed uses {} for rendering and requires a compatible GPU.
616
617 Currently you are using a software emulated GPU ({}) which
618 will result in awful performance.
619
620 For troubleshooting see: {}
621 Set ZED_ALLOW_EMULATED_GPU=1 env var to permanently override.
622 "#},
623 graphics_api, specs.device_name, docs_url
624 );
625 let prompt = window.prompt(
626 PromptLevel::Critical,
627 "Unsupported GPU",
628 Some(&message),
629 &["Skip", "Troubleshoot and Quit"],
630 cx,
631 );
632 cx.spawn(async move |_, cx| {
633 if prompt.await == Ok(1) {
634 cx.update(|cx| {
635 cx.open_url(open_url);
636 cx.quit();
637 })
638 .ok();
639 }
640 })
641 .detach()
642 }
643}
644
645fn initialize_panels(
646 prompt_builder: Arc<PromptBuilder>,
647 window: &mut Window,
648 cx: &mut Context<Workspace>,
649) {
650 cx.spawn_in(window, async move |workspace_handle, cx| {
651 let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
652 let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
653 let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
654 let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone());
655 let channels_panel =
656 collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
657 let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
658 workspace_handle.clone(),
659 cx.clone(),
660 );
661 let debug_panel = DebugPanel::load(workspace_handle.clone(), cx);
662
663 async fn add_panel_when_ready(
664 panel_task: impl Future<Output = anyhow::Result<Entity<impl workspace::Panel>>> + 'static,
665 workspace_handle: WeakEntity<Workspace>,
666 mut cx: gpui::AsyncWindowContext,
667 ) {
668 if let Some(panel) = panel_task.await.context("failed to load panel").log_err()
669 {
670 workspace_handle
671 .update_in(&mut cx, |workspace, window, cx| {
672 workspace.add_panel(panel, window, cx);
673 })
674 .log_err();
675 }
676 }
677
678 futures::join!(
679 add_panel_when_ready(project_panel, workspace_handle.clone(), cx.clone()),
680 add_panel_when_ready(outline_panel, workspace_handle.clone(), cx.clone()),
681 add_panel_when_ready(terminal_panel, workspace_handle.clone(), cx.clone()),
682 add_panel_when_ready(git_panel, workspace_handle.clone(), cx.clone()),
683 add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()),
684 add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()),
685 add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()),
686 initialize_agent_panel(workspace_handle.clone(), prompt_builder, cx.clone()).map(|r| r.log_err()),
687 initialize_agents_panel(workspace_handle, cx.clone()).map(|r| r.log_err())
688 );
689
690 anyhow::Ok(())
691 })
692 .detach();
693}
694
695fn setup_or_teardown_ai_panel<P: Panel>(
696 workspace: &mut Workspace,
697 window: &mut Window,
698 cx: &mut Context<Workspace>,
699 load_panel: impl FnOnce(
700 WeakEntity<Workspace>,
701 AsyncWindowContext,
702 ) -> Task<anyhow::Result<Entity<P>>>
703 + 'static,
704) -> Task<anyhow::Result<()>> {
705 let disable_ai = SettingsStore::global(cx)
706 .get::<DisableAiSettings>(None)
707 .disable_ai
708 || cfg!(test);
709 let existing_panel = workspace.panel::<P>(cx);
710 match (disable_ai, existing_panel) {
711 (false, None) => cx.spawn_in(window, async move |workspace, cx| {
712 let panel = load_panel(workspace.clone(), cx.clone()).await?;
713 workspace.update_in(cx, |workspace, window, cx| {
714 let disable_ai = SettingsStore::global(cx)
715 .get::<DisableAiSettings>(None)
716 .disable_ai;
717 let have_panel = workspace.panel::<P>(cx).is_some();
718 if !disable_ai && !have_panel {
719 workspace.add_panel(panel, window, cx);
720 }
721 })
722 }),
723 (true, Some(existing_panel)) => {
724 workspace.remove_panel::<P>(&existing_panel, window, cx);
725 Task::ready(Ok(()))
726 }
727 _ => Task::ready(Ok(())),
728 }
729}
730
731async fn initialize_agent_panel(
732 workspace_handle: WeakEntity<Workspace>,
733 prompt_builder: Arc<PromptBuilder>,
734 mut cx: AsyncWindowContext,
735) -> anyhow::Result<()> {
736 workspace_handle
737 .update_in(&mut cx, |workspace, window, cx| {
738 let prompt_builder = prompt_builder.clone();
739 setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
740 agent_ui::AgentPanel::load(workspace, prompt_builder, cx)
741 })
742 })?
743 .await?;
744
745 workspace_handle.update_in(&mut cx, |workspace, window, cx| {
746 let prompt_builder = prompt_builder.clone();
747 cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
748 let prompt_builder = prompt_builder.clone();
749 setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
750 agent_ui::AgentPanel::load(workspace, prompt_builder, cx)
751 })
752 .detach_and_log_err(cx);
753 })
754 .detach();
755
756 // Register the actions that are shared between `assistant` and `assistant2`.
757 //
758 // We need to do this here instead of within the individual `init`
759 // functions so that we only register the actions once.
760 //
761 // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`.
762 if !cfg!(test) {
763 <dyn AgentPanelDelegate>::set_global(
764 Arc::new(agent_ui::ConcreteAssistantPanelDelegate),
765 cx,
766 );
767
768 workspace
769 .register_action(agent_ui::AgentPanel::toggle_focus)
770 .register_action(agent_ui::InlineAssistant::inline_assist);
771 }
772 })?;
773
774 anyhow::Ok(())
775}
776
777async fn initialize_agents_panel(
778 workspace_handle: WeakEntity<Workspace>,
779 mut cx: AsyncWindowContext,
780) -> anyhow::Result<()> {
781 workspace_handle
782 .update_in(&mut cx, |workspace, window, cx| {
783 setup_or_teardown_ai_panel(workspace, window, cx, |workspace, cx| {
784 AgentsPanel::load(workspace, cx)
785 })
786 })?
787 .await?;
788
789 workspace_handle.update_in(&mut cx, |_workspace, window, cx| {
790 cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
791 setup_or_teardown_ai_panel(workspace, window, cx, |workspace, cx| {
792 AgentsPanel::load(workspace, cx)
793 })
794 .detach_and_log_err(cx);
795 })
796 .detach();
797 })?;
798
799 anyhow::Ok(())
800}
801
802fn register_actions(
803 app_state: Arc<AppState>,
804 workspace: &mut Workspace,
805 _: &mut Window,
806 cx: &mut Context<Workspace>,
807) {
808 workspace
809 .register_action(|_, _: &OpenDocs, _, cx| cx.open_url(DOCS_URL))
810 .register_action(|_, _: &Minimize, window, _| {
811 window.minimize_window();
812 })
813 .register_action(|_, _: &Zoom, window, _| {
814 window.zoom_window();
815 })
816 .register_action(|_, _: &ToggleFullScreen, window, _| {
817 window.toggle_fullscreen();
818 })
819 .register_action(|_, action: &OpenZedUrl, _, cx| {
820 OpenListener::global(cx).open(RawOpenRequest {
821 urls: vec![action.url.clone()],
822 ..Default::default()
823 })
824 })
825 .register_action(|workspace, action: &OpenBrowser, _window, cx| {
826 // Parse and validate the URL to ensure it's properly formatted
827 match url::Url::parse(&action.url) {
828 Ok(parsed_url) => {
829 // Use the parsed URL's string representation which is properly escaped
830 cx.open_url(parsed_url.as_str());
831 }
832 Err(e) => {
833 workspace.show_error(
834 &anyhow::anyhow!(
835 "Opening this URL in a browser failed because the URL is invalid: {}\n\nError was: {e}",
836 action.url
837 ),
838 cx,
839 );
840 }
841 }
842 })
843 .register_action(|workspace, _: &workspace::Open, window, cx| {
844 telemetry::event!("Project Opened");
845 let paths = workspace.prompt_for_open_path(
846 PathPromptOptions {
847 files: true,
848 directories: true,
849 multiple: true,
850 prompt: None,
851 },
852 DirectoryLister::Local(
853 workspace.project().clone(),
854 workspace.app_state().fs.clone(),
855 ),
856 window,
857 cx,
858 );
859
860 cx.spawn_in(window, async move |this, cx| {
861 let Some(paths) = paths.await.log_err().flatten() else {
862 return;
863 };
864
865 if let Some(task) = this
866 .update_in(cx, |this, window, cx| {
867 this.open_workspace_for_paths(false, paths, window, cx)
868 })
869 .log_err()
870 {
871 task.await.log_err();
872 }
873 })
874 .detach()
875 })
876 .register_action(|workspace, action: &zed_actions::OpenRemote, window, cx| {
877 if !action.from_existing_connection {
878 cx.propagate();
879 return;
880 }
881 // You need existing remote connection to open it this way
882 if workspace.project().read(cx).is_local() {
883 return;
884 }
885 telemetry::event!("Project Opened");
886 let paths = workspace.prompt_for_open_path(
887 PathPromptOptions {
888 files: true,
889 directories: true,
890 multiple: true,
891 prompt: None,
892 },
893 DirectoryLister::Project(workspace.project().clone()),
894 window,
895 cx,
896 );
897 cx.spawn_in(window, async move |this, cx| {
898 let Some(paths) = paths.await.log_err().flatten() else {
899 return;
900 };
901 if let Some(task) = this
902 .update_in(cx, |this, window, cx| {
903 open_new_ssh_project_from_project(this, paths, window, cx)
904 })
905 .log_err()
906 {
907 task.await.log_err();
908 }
909 })
910 .detach()
911 })
912 .register_action({
913 let fs = app_state.fs.clone();
914 move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| {
915 if action.persist {
916 update_settings_file(fs.clone(), cx, move |settings, cx| {
917 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0);
918 let _ = settings
919 .theme
920 .ui_font_size
921 .insert(theme::clamp_font_size(ui_font_size).into());
922 });
923 } else {
924 theme::adjust_ui_font_size(cx, |size| size + px(1.0));
925 }
926 }
927 })
928 .register_action({
929 let fs = app_state.fs.clone();
930 move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| {
931 if action.persist {
932 update_settings_file(fs.clone(), cx, move |settings, cx| {
933 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0);
934 let _ = settings
935 .theme
936 .ui_font_size
937 .insert(theme::clamp_font_size(ui_font_size).into());
938 });
939 } else {
940 theme::adjust_ui_font_size(cx, |size| size - px(1.0));
941 }
942 }
943 })
944 .register_action({
945 let fs = app_state.fs.clone();
946 move |_, action: &zed_actions::ResetUiFontSize, _window, cx| {
947 if action.persist {
948 update_settings_file(fs.clone(), cx, move |settings, _| {
949 settings.theme.ui_font_size = None;
950 });
951 } else {
952 theme::reset_ui_font_size(cx);
953 }
954 }
955 })
956 .register_action({
957 let fs = app_state.fs.clone();
958 move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| {
959 if action.persist {
960 update_settings_file(fs.clone(), cx, move |settings, cx| {
961 let buffer_font_size =
962 ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0);
963 let _ = settings
964 .theme
965 .buffer_font_size
966 .insert(theme::clamp_font_size(buffer_font_size).into());
967 });
968 } else {
969 theme::adjust_buffer_font_size(cx, |size| size + px(1.0));
970 }
971 }
972 })
973 .register_action({
974 let fs = app_state.fs.clone();
975 move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| {
976 if action.persist {
977 update_settings_file(fs.clone(), cx, move |settings, cx| {
978 let buffer_font_size =
979 ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0);
980 let _ = settings
981 .theme
982 .buffer_font_size
983 .insert(theme::clamp_font_size(buffer_font_size).into());
984 });
985 } else {
986 theme::adjust_buffer_font_size(cx, |size| size - px(1.0));
987 }
988 }
989 })
990 .register_action({
991 let fs = app_state.fs.clone();
992 move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| {
993 if action.persist {
994 update_settings_file(fs.clone(), cx, move |settings, _| {
995 settings.theme.buffer_font_size = None;
996 });
997 } else {
998 theme::reset_buffer_font_size(cx);
999 }
1000 }
1001 })
1002 .register_action({
1003 let fs = app_state.fs.clone();
1004 move |_, action: &zed_actions::ResetAllZoom, _window, cx| {
1005 if action.persist {
1006 update_settings_file(fs.clone(), cx, move |settings, _| {
1007 settings.theme.ui_font_size = None;
1008 settings.theme.buffer_font_size = None;
1009 settings.theme.agent_ui_font_size = None;
1010 settings.theme.agent_buffer_font_size = None;
1011 });
1012 } else {
1013 theme::reset_ui_font_size(cx);
1014 theme::reset_buffer_font_size(cx);
1015 theme::reset_agent_ui_font_size(cx);
1016 theme::reset_agent_buffer_font_size(cx);
1017 }
1018 }
1019 })
1020 .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| {
1021 cx.spawn_in(window, async move |workspace, cx| {
1022 install_cli::register_zed_scheme(cx).await?;
1023 workspace.update_in(cx, |workspace, _, cx| {
1024 struct RegisterZedScheme;
1025
1026 workspace.show_toast(
1027 Toast::new(
1028 NotificationId::unique::<RegisterZedScheme>(),
1029 format!(
1030 "zed:// links will now open in {}.",
1031 ReleaseChannel::global(cx).display_name()
1032 ),
1033 ),
1034 cx,
1035 )
1036 })?;
1037 Ok(())
1038 })
1039 .detach_and_prompt_err(
1040 "Error registering zed:// scheme",
1041 window,
1042 cx,
1043 |_, _, _| None,
1044 );
1045 })
1046 .register_action(open_project_settings_file)
1047 .register_action(open_project_tasks_file)
1048 .register_action(open_project_debug_tasks_file)
1049 .register_action(
1050 |workspace: &mut Workspace,
1051 _: &zed_actions::project_panel::ToggleFocus,
1052 window: &mut Window,
1053 cx: &mut Context<Workspace>| {
1054 workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
1055 },
1056 )
1057 .register_action(
1058 |workspace: &mut Workspace,
1059 _: &outline_panel::ToggleFocus,
1060 window: &mut Window,
1061 cx: &mut Context<Workspace>| {
1062 workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
1063 },
1064 )
1065 .register_action(
1066 |workspace: &mut Workspace,
1067 _: &collab_ui::collab_panel::ToggleFocus,
1068 window: &mut Window,
1069 cx: &mut Context<Workspace>| {
1070 workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(window, cx);
1071 },
1072 )
1073 .register_action(
1074 |workspace: &mut Workspace,
1075 _: &collab_ui::notification_panel::ToggleFocus,
1076 window: &mut Window,
1077 cx: &mut Context<Workspace>| {
1078 workspace.toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(
1079 window, cx,
1080 );
1081 },
1082 )
1083 .register_action(
1084 |workspace: &mut Workspace,
1085 _: &terminal_panel::ToggleFocus,
1086 window: &mut Window,
1087 cx: &mut Context<Workspace>| {
1088 workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
1089 },
1090 )
1091 .register_action(
1092 |workspace: &mut Workspace,
1093 _: &zed_actions::agent::ToggleAgentPane,
1094 window: &mut Window,
1095 cx: &mut Context<Workspace>| {
1096 if let Some(panel) = workspace.panel::<AgentsPanel>(cx) {
1097 let position = panel.read(cx).position(window, cx);
1098 let slot = utility_slot_for_dock_position(position);
1099 workspace.toggle_utility_pane(slot, window, cx);
1100 }
1101 },
1102 )
1103 .register_action({
1104 let app_state = Arc::downgrade(&app_state);
1105 move |_, _: &NewWindow, _, cx| {
1106 if let Some(app_state) = app_state.upgrade() {
1107 open_new(
1108 Default::default(),
1109 app_state,
1110 cx,
1111 |workspace, window, cx| {
1112 cx.activate(true);
1113 // Create buffer synchronously to avoid flicker
1114 let project = workspace.project().clone();
1115 let buffer = project.update(cx, |project, cx| {
1116 project.create_local_buffer("", None, true, cx)
1117 });
1118 let editor = cx.new(|cx| {
1119 Editor::for_buffer(buffer, Some(project), window, cx)
1120 });
1121 workspace.add_item_to_active_pane(
1122 Box::new(editor),
1123 None,
1124 true,
1125 window,
1126 cx,
1127 );
1128 },
1129 )
1130 .detach();
1131 }
1132 }
1133 })
1134 .register_action({
1135 let app_state = Arc::downgrade(&app_state);
1136 move |_, _: &NewFile, _, cx| {
1137 if let Some(app_state) = app_state.upgrade() {
1138 open_new(
1139 Default::default(),
1140 app_state,
1141 cx,
1142 |workspace, window, cx| {
1143 Editor::new_file(workspace, &Default::default(), window, cx)
1144 },
1145 )
1146 .detach();
1147 }
1148 }
1149 })
1150 .register_action(|workspace, _: &CaptureRecentAudio, window, cx| {
1151 capture_recent_audio(workspace, window, cx);
1152 });
1153
1154 #[cfg(not(target_os = "windows"))]
1155 workspace.register_action(install_cli);
1156
1157 if workspace.project().read(cx).is_via_remote_server() {
1158 workspace.register_action({
1159 move |workspace, _: &OpenServerSettings, window, cx| {
1160 let open_server_settings = workspace
1161 .project()
1162 .update(cx, |project, cx| project.open_server_settings(cx));
1163
1164 cx.spawn_in(window, async move |workspace, cx| {
1165 let buffer = open_server_settings.await?;
1166
1167 workspace
1168 .update_in(cx, |workspace, window, cx| {
1169 workspace.open_path(
1170 buffer
1171 .read(cx)
1172 .project_path(cx)
1173 .expect("Settings file must have a location"),
1174 None,
1175 true,
1176 window,
1177 cx,
1178 )
1179 })?
1180 .await?;
1181
1182 anyhow::Ok(())
1183 })
1184 .detach_and_log_err(cx);
1185 }
1186 });
1187 }
1188}
1189
1190fn initialize_pane(
1191 workspace: &Workspace,
1192 pane: &Entity<Pane>,
1193 window: &mut Window,
1194 cx: &mut Context<Workspace>,
1195) {
1196 pane.update(cx, |pane, cx| {
1197 pane.toolbar().update(cx, |toolbar, cx| {
1198 let multibuffer_hint = cx.new(|_| MultibufferHint::new());
1199 toolbar.add_item(multibuffer_hint, window, cx);
1200 let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1201 toolbar.add_item(breadcrumbs, window, cx);
1202 let buffer_search_bar = cx.new(|cx| {
1203 search::BufferSearchBar::new(
1204 Some(workspace.project().read(cx).languages().clone()),
1205 window,
1206 cx,
1207 )
1208 });
1209 toolbar.add_item(buffer_search_bar.clone(), window, cx);
1210 let quick_action_bar =
1211 cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
1212 toolbar.add_item(quick_action_bar, window, cx);
1213 let diagnostic_editor_controls = cx.new(|_| diagnostics::ToolbarControls::new());
1214 toolbar.add_item(diagnostic_editor_controls, window, cx);
1215 let project_search_bar = cx.new(|_| ProjectSearchBar::new());
1216 toolbar.add_item(project_search_bar, window, cx);
1217 let lsp_log_item = cx.new(|_| LspLogToolbarItemView::new());
1218 toolbar.add_item(lsp_log_item, window, cx);
1219 let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new());
1220 toolbar.add_item(dap_log_item, window, cx);
1221 let acp_tools_item = cx.new(|_| acp_tools::AcpToolsToolbarItemView::new());
1222 toolbar.add_item(acp_tools_item, window, cx);
1223 let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new());
1224 toolbar.add_item(syntax_tree_item, window, cx);
1225 let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx));
1226 toolbar.add_item(migration_banner, window, cx);
1227 let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
1228 toolbar.add_item(project_diff_toolbar, window, cx);
1229 let commit_view_toolbar = cx.new(|_| CommitViewToolbar::new());
1230 toolbar.add_item(commit_view_toolbar, window, cx);
1231 let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
1232 toolbar.add_item(agent_diff_toolbar, window, cx);
1233 let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx));
1234 toolbar.add_item(basedpyright_banner, window, cx);
1235 })
1236 });
1237}
1238
1239fn about(_: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1240 use std::fmt::Write;
1241 let release_channel = ReleaseChannel::global(cx).display_name();
1242 let full_version = AppVersion::global(cx);
1243 let version = env!("CARGO_PKG_VERSION");
1244 let debug = if cfg!(debug_assertions) {
1245 "(debug)"
1246 } else {
1247 ""
1248 };
1249 let message = format!("{release_channel} {version} {debug}");
1250
1251 let mut detail = AppCommitSha::try_global(cx)
1252 .map(|sha| sha.full())
1253 .unwrap_or_default();
1254 if !detail.is_empty() {
1255 detail.push('\n');
1256 }
1257 _ = write!(&mut detail, "\n{full_version}");
1258
1259 let detail = Some(detail);
1260
1261 let prompt = window.prompt(
1262 PromptLevel::Info,
1263 &message,
1264 detail.as_deref(),
1265 &["Copy", "OK"],
1266 cx,
1267 );
1268 cx.spawn(async move |_, cx| {
1269 if let Ok(0) = prompt.await {
1270 let content = format!("{}\n{}", message, detail.as_deref().unwrap_or(""));
1271 cx.update(|cx| {
1272 cx.write_to_clipboard(gpui::ClipboardItem::new_string(content));
1273 })
1274 .ok();
1275 }
1276 })
1277 .detach();
1278}
1279
1280#[cfg(not(target_os = "windows"))]
1281fn install_cli(
1282 _: &mut Workspace,
1283 _: &install_cli::InstallCliBinary,
1284 window: &mut Window,
1285 cx: &mut Context<Workspace>,
1286) {
1287 install_cli::install_cli_binary(window, cx)
1288}
1289
1290static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);
1291fn quit(_: &Quit, cx: &mut App) {
1292 if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) {
1293 return;
1294 }
1295
1296 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
1297 cx.spawn(async move |cx| {
1298 let mut workspace_windows = cx.update(|cx| {
1299 cx.windows()
1300 .into_iter()
1301 .filter_map(|window| window.downcast::<Workspace>())
1302 .collect::<Vec<_>>()
1303 })?;
1304
1305 // If multiple windows have unsaved changes, and need a save prompt,
1306 // prompt in the active window before switching to a different window.
1307 cx.update(|cx| {
1308 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
1309 })
1310 .log_err();
1311
1312 if should_confirm && let Some(workspace) = workspace_windows.first() {
1313 let answer = workspace
1314 .update(cx, |_, window, cx| {
1315 window.prompt(
1316 PromptLevel::Info,
1317 "Are you sure you want to quit?",
1318 None,
1319 &["Quit", "Cancel"],
1320 cx,
1321 )
1322 })
1323 .log_err();
1324
1325 if let Some(answer) = answer {
1326 WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release);
1327 let answer = answer.await.ok();
1328 WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release);
1329 if answer != Some(0) {
1330 return Ok(());
1331 }
1332 }
1333 }
1334
1335 // If the user cancels any save prompt, then keep the app open.
1336 for window in workspace_windows {
1337 if let Some(should_close) = window
1338 .update(cx, |workspace, window, cx| {
1339 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
1340 })
1341 .log_err()
1342 && !should_close.await?
1343 {
1344 return Ok(());
1345 }
1346 }
1347 cx.update(|cx| cx.quit())?;
1348 anyhow::Ok(())
1349 })
1350 .detach_and_log_err(cx);
1351}
1352
1353fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1354 const MAX_LINES: usize = 1000;
1355 workspace
1356 .with_local_workspace(window, cx, move |workspace, window, cx| {
1357 let app_state = workspace.app_state();
1358 let languages = app_state.languages.clone();
1359 let fs = app_state.fs.clone();
1360 cx.spawn_in(window, async move |workspace, cx| {
1361 let (old_log, new_log, log_language) = futures::join!(
1362 fs.load(paths::old_log_file()),
1363 fs.load(paths::log_file()),
1364 languages.language_for_name("log")
1365 );
1366 let log = match (old_log, new_log) {
1367 (Err(_), Err(_)) => None,
1368 (old_log, new_log) => {
1369 let mut lines = VecDeque::with_capacity(MAX_LINES);
1370 for line in old_log
1371 .iter()
1372 .flat_map(|log| log.lines())
1373 .chain(new_log.iter().flat_map(|log| log.lines()))
1374 {
1375 if lines.len() == MAX_LINES {
1376 lines.pop_front();
1377 }
1378 lines.push_back(line);
1379 }
1380 Some(
1381 lines
1382 .into_iter()
1383 .flat_map(|line| [line, "\n"])
1384 .collect::<String>(),
1385 )
1386 }
1387 };
1388 let log_language = log_language.ok();
1389
1390 workspace
1391 .update_in(cx, |workspace, window, cx| {
1392 let Some(log) = log else {
1393 struct OpenLogError;
1394
1395 workspace.show_notification(
1396 NotificationId::unique::<OpenLogError>(),
1397 cx,
1398 |cx| {
1399 cx.new(|cx| {
1400 MessageNotification::new(
1401 format!(
1402 "Unable to access/open log file at path {:?}",
1403 paths::log_file().as_path()
1404 ),
1405 cx,
1406 )
1407 })
1408 },
1409 );
1410 return;
1411 };
1412 let project = workspace.project().clone();
1413 let buffer = project.update(cx, |project, cx| {
1414 project.create_local_buffer(&log, log_language, false, cx)
1415 });
1416
1417 let buffer = cx
1418 .new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into()));
1419 let editor = cx.new(|cx| {
1420 let mut editor =
1421 Editor::for_multibuffer(buffer, Some(project), window, cx);
1422 editor.set_read_only(true);
1423 editor.set_breadcrumb_header(format!(
1424 "Last {} lines in {}",
1425 MAX_LINES,
1426 paths::log_file().display()
1427 ));
1428 editor
1429 });
1430
1431 editor.update(cx, |editor, cx| {
1432 let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
1433 editor.change_selections(Default::default(), window, cx, |s| {
1434 s.select_ranges(Some(
1435 last_multi_buffer_offset..last_multi_buffer_offset,
1436 ));
1437 })
1438 });
1439
1440 workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
1441 })
1442 .log_err();
1443 })
1444 .detach();
1445 })
1446 .detach();
1447}
1448
1449fn notify_settings_errors(result: settings::SettingsParseResult, is_user: bool, cx: &mut App) {
1450 if let settings::ParseStatus::Failed { error: err } = &result.parse_status {
1451 let settings_type = if is_user { "user" } else { "global" };
1452 log::error!("Failed to load {} settings: {err}", settings_type);
1453 }
1454
1455 let error = match result.parse_status {
1456 settings::ParseStatus::Failed { error } => Some(anyhow::format_err!(error)),
1457 settings::ParseStatus::Success => None,
1458 };
1459 let id = NotificationId::Named(format!("failed-to-parse-settings-{is_user}").into());
1460
1461 let showed_parse_error = match error {
1462 Some(error) => {
1463 if let Some(InvalidSettingsError::LocalSettings { .. }) =
1464 error.downcast_ref::<InvalidSettingsError>()
1465 {
1466 false
1467 // Local settings errors are displayed by the projects
1468 } else {
1469 show_app_notification(id, cx, move |cx| {
1470 cx.new(|cx| {
1471 MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
1472 .primary_message("Open Settings File")
1473 .primary_icon(IconName::Settings)
1474 .primary_on_click(|window, cx| {
1475 window.dispatch_action(
1476 zed_actions::OpenSettingsFile.boxed_clone(),
1477 cx,
1478 );
1479 cx.emit(DismissEvent);
1480 })
1481 })
1482 });
1483 true
1484 }
1485 }
1486 None => {
1487 dismiss_app_notification(&id, cx);
1488 false
1489 }
1490 };
1491 let id = NotificationId::Named(format!("failed-to-migrate-settings-{is_user}").into());
1492
1493 match result.migration_status {
1494 settings::MigrationStatus::Succeeded | settings::MigrationStatus::NotNeeded => {
1495 dismiss_app_notification(&id, cx);
1496 }
1497 settings::MigrationStatus::Failed { error: err } => {
1498 if !showed_parse_error {
1499 show_app_notification(id, cx, move |cx| {
1500 cx.new(|cx| {
1501 MessageNotification::new(
1502 format!(
1503 "Failed to migrate settings\n\
1504 {err}"
1505 ),
1506 cx,
1507 )
1508 .primary_message("Open Settings File")
1509 .primary_icon(IconName::Settings)
1510 .primary_on_click(|window, cx| {
1511 window.dispatch_action(zed_actions::OpenSettingsFile.boxed_clone(), cx);
1512 cx.emit(DismissEvent);
1513 })
1514 })
1515 });
1516 }
1517 }
1518 };
1519}
1520
1521pub fn handle_settings_file_changes(
1522 mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
1523 mut global_settings_file_rx: mpsc::UnboundedReceiver<String>,
1524 cx: &mut App,
1525) {
1526 MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);
1527
1528 // Initial load of both settings files
1529 let global_content = cx
1530 .background_executor()
1531 .block(global_settings_file_rx.next())
1532 .unwrap();
1533 let user_content = cx
1534 .background_executor()
1535 .block(user_settings_file_rx.next())
1536 .unwrap();
1537
1538 SettingsStore::update_global(cx, |store, cx| {
1539 notify_settings_errors(store.set_user_settings(&user_content, cx), true, cx);
1540 notify_settings_errors(store.set_global_settings(&global_content, cx), false, cx);
1541 });
1542
1543 // Watch for changes in both files
1544 cx.spawn(async move |cx| {
1545 let mut settings_streams = futures::stream::select(
1546 global_settings_file_rx.map(Either::Left),
1547 user_settings_file_rx.map(Either::Right),
1548 );
1549
1550 while let Some(content) = settings_streams.next().await {
1551 let (content, is_user) = match content {
1552 Either::Left(content) => (content, false),
1553 Either::Right(content) => (content, true),
1554 };
1555
1556 let result = cx.update_global(|store: &mut SettingsStore, cx| {
1557 let result = if is_user {
1558 store.set_user_settings(&content, cx)
1559 } else {
1560 store.set_global_settings(&content, cx)
1561 };
1562 let migrating_in_memory =
1563 matches!(&result.migration_status, MigrationStatus::Succeeded);
1564 notify_settings_errors(result, is_user, cx);
1565 if let Some(notifier) = MigrationNotification::try_global(cx) {
1566 notifier.update(cx, |_, cx| {
1567 cx.emit(MigrationEvent::ContentChanged {
1568 migration_type: MigrationType::Settings,
1569 migrating_in_memory,
1570 });
1571 });
1572 }
1573 cx.refresh_windows();
1574 });
1575
1576 if result.is_err() {
1577 break; // App dropped
1578 }
1579 }
1580 })
1581 .detach();
1582}
1583
1584pub fn handle_keymap_file_changes(
1585 mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
1586 cx: &mut App,
1587) {
1588 let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
1589 let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded();
1590 let mut old_base_keymap = *BaseKeymap::get_global(cx);
1591 let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
1592 let mut old_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
1593
1594 cx.observe_global::<SettingsStore>(move |cx| {
1595 let new_base_keymap = *BaseKeymap::get_global(cx);
1596 let new_vim_enabled = VimModeSetting::get_global(cx).0;
1597 let new_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
1598
1599 if new_base_keymap != old_base_keymap
1600 || new_vim_enabled != old_vim_enabled
1601 || new_helix_enabled != old_helix_enabled
1602 {
1603 old_base_keymap = new_base_keymap;
1604 old_vim_enabled = new_vim_enabled;
1605 old_helix_enabled = new_helix_enabled;
1606
1607 base_keymap_tx.unbounded_send(()).unwrap();
1608 }
1609 })
1610 .detach();
1611
1612 #[cfg(target_os = "windows")]
1613 {
1614 let mut current_layout_id = cx.keyboard_layout().id().to_string();
1615 cx.on_keyboard_layout_change(move |cx| {
1616 let next_layout_id = cx.keyboard_layout().id();
1617 if next_layout_id != current_layout_id {
1618 current_layout_id = next_layout_id.to_string();
1619 keyboard_layout_tx.unbounded_send(()).ok();
1620 }
1621 })
1622 .detach();
1623 }
1624
1625 #[cfg(not(target_os = "windows"))]
1626 {
1627 let mut current_mapping = cx.keyboard_mapper().get_key_equivalents().cloned();
1628 cx.on_keyboard_layout_change(move |cx| {
1629 let next_mapping = cx.keyboard_mapper().get_key_equivalents();
1630 if current_mapping.as_ref() != next_mapping {
1631 current_mapping = next_mapping.cloned();
1632 keyboard_layout_tx.unbounded_send(()).ok();
1633 }
1634 })
1635 .detach();
1636 }
1637
1638 load_default_keymap(cx);
1639
1640 struct KeymapParseErrorNotification;
1641 let notification_id = NotificationId::unique::<KeymapParseErrorNotification>();
1642
1643 cx.spawn(async move |cx| {
1644 let mut user_keymap_content = String::new();
1645 let mut migrating_in_memory = false;
1646 loop {
1647 select_biased! {
1648 _ = base_keymap_rx.next() => {},
1649 _ = keyboard_layout_rx.next() => {},
1650 content = user_keymap_file_rx.next() => {
1651 if let Some(content) = content {
1652 if let Ok(Some(migrated_content)) = migrate_keymap(&content) {
1653 user_keymap_content = migrated_content;
1654 migrating_in_memory = true;
1655 } else {
1656 user_keymap_content = content;
1657 migrating_in_memory = false;
1658 }
1659 }
1660 }
1661 };
1662 cx.update(|cx| {
1663 if let Some(notifier) = MigrationNotification::try_global(cx) {
1664 notifier.update(cx, |_, cx| {
1665 cx.emit(MigrationEvent::ContentChanged {
1666 migration_type: MigrationType::Keymap,
1667 migrating_in_memory,
1668 });
1669 });
1670 }
1671 let load_result = KeymapFile::load(&user_keymap_content, cx);
1672 match load_result {
1673 KeymapFileLoadResult::Success { key_bindings } => {
1674 reload_keymaps(cx, key_bindings);
1675 dismiss_app_notification(¬ification_id.clone(), cx);
1676 }
1677 KeymapFileLoadResult::SomeFailedToLoad {
1678 key_bindings,
1679 error_message,
1680 } => {
1681 if !key_bindings.is_empty() {
1682 reload_keymaps(cx, key_bindings);
1683 }
1684 show_keymap_file_load_error(notification_id.clone(), error_message, cx);
1685 }
1686 KeymapFileLoadResult::JsonParseFailure { error } => {
1687 show_keymap_file_json_error(notification_id.clone(), &error, cx)
1688 }
1689 }
1690 })
1691 .ok();
1692 }
1693 })
1694 .detach();
1695}
1696
1697fn show_keymap_file_json_error(
1698 notification_id: NotificationId,
1699 error: &anyhow::Error,
1700 cx: &mut App,
1701) {
1702 let message: SharedString =
1703 format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
1704 show_app_notification(notification_id, cx, move |cx| {
1705 cx.new(|cx| {
1706 MessageNotification::new(message.clone(), cx)
1707 .primary_message("Open Keymap File")
1708 .primary_icon(IconName::Settings)
1709 .primary_on_click(|window, cx| {
1710 window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
1711 cx.emit(DismissEvent);
1712 })
1713 })
1714 });
1715}
1716
1717fn show_keymap_file_load_error(
1718 notification_id: NotificationId,
1719 error_message: MarkdownString,
1720 cx: &mut App,
1721) {
1722 show_markdown_app_notification(
1723 notification_id,
1724 error_message,
1725 "Open Keymap File".into(),
1726 |window, cx| {
1727 window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
1728 cx.emit(DismissEvent);
1729 },
1730 cx,
1731 )
1732}
1733
1734fn show_markdown_app_notification<F>(
1735 notification_id: NotificationId,
1736 message: MarkdownString,
1737 primary_button_message: SharedString,
1738 primary_button_on_click: F,
1739 cx: &mut App,
1740) where
1741 F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
1742{
1743 let parsed_markdown = cx.background_spawn(async move {
1744 let file_location_directory = None;
1745 let language_registry = None;
1746 markdown_preview::markdown_parser::parse_markdown(
1747 &message.0,
1748 file_location_directory,
1749 language_registry,
1750 )
1751 .await
1752 });
1753
1754 cx.spawn(async move |cx| {
1755 let parsed_markdown = Arc::new(parsed_markdown.await);
1756 let primary_button_message = primary_button_message.clone();
1757 let primary_button_on_click = Arc::new(primary_button_on_click);
1758 cx.update(|cx| {
1759 show_app_notification(notification_id, cx, move |cx| {
1760 let workspace_handle = cx.entity().downgrade();
1761 let parsed_markdown = parsed_markdown.clone();
1762 let primary_button_message = primary_button_message.clone();
1763 let primary_button_on_click = primary_button_on_click.clone();
1764 cx.new(move |cx| {
1765 MessageNotification::new_from_builder(cx, move |window, cx| {
1766 image_cache(retain_all("notification-cache"))
1767 .child(div().text_ui(cx).child(
1768 markdown_preview::markdown_renderer::render_parsed_markdown(
1769 &parsed_markdown.clone(),
1770 Some(workspace_handle.clone()),
1771 window,
1772 cx,
1773 ),
1774 ))
1775 .into_any()
1776 })
1777 .primary_message(primary_button_message)
1778 .primary_icon(IconName::Settings)
1779 .primary_on_click_arc(primary_button_on_click)
1780 })
1781 })
1782 })
1783 .ok();
1784 })
1785 .detach();
1786}
1787
1788fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec<KeyBinding>) {
1789 cx.clear_key_bindings();
1790 load_default_keymap(cx);
1791
1792 for key_binding in &mut user_key_bindings {
1793 key_binding.set_meta(KeybindSource::User.meta());
1794 }
1795 cx.bind_keys(user_key_bindings);
1796
1797 let menus = app_menus(cx);
1798 cx.set_menus(menus);
1799 // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
1800 #[cfg(not(target_os = "windows"))]
1801 cx.set_dock_menu(vec![gpui::MenuItem::action(
1802 "New Window",
1803 workspace::NewWindow,
1804 )]);
1805 // todo: nicer api here?
1806 keymap_editor::KeymapEventChannel::trigger_keymap_changed(cx);
1807}
1808
1809pub fn load_default_keymap(cx: &mut App) {
1810 let base_keymap = *BaseKeymap::get_global(cx);
1811 if base_keymap == BaseKeymap::None {
1812 return;
1813 }
1814
1815 cx.bind_keys(
1816 KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, Some(KeybindSource::Default), cx).unwrap(),
1817 );
1818
1819 if let Some(asset_path) = base_keymap.asset_path() {
1820 cx.bind_keys(KeymapFile::load_asset(asset_path, Some(KeybindSource::Base), cx).unwrap());
1821 }
1822
1823 if VimModeSetting::get_global(cx).0 || vim_mode_setting::HelixModeSetting::get_global(cx).0 {
1824 cx.bind_keys(
1825 KeymapFile::load_asset(VIM_KEYMAP_PATH, Some(KeybindSource::Vim), cx).unwrap(),
1826 );
1827 }
1828}
1829
1830pub fn open_new_ssh_project_from_project(
1831 workspace: &mut Workspace,
1832 paths: Vec<PathBuf>,
1833 window: &mut Window,
1834 cx: &mut Context<Workspace>,
1835) -> Task<anyhow::Result<()>> {
1836 let app_state = workspace.app_state().clone();
1837 let Some(ssh_client) = workspace.project().read(cx).remote_client() else {
1838 return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
1839 };
1840 let connection_options = ssh_client.read(cx).connection_options();
1841 cx.spawn_in(window, async move |_, cx| {
1842 open_remote_project(
1843 connection_options,
1844 paths,
1845 app_state,
1846 workspace::OpenOptions {
1847 open_new_workspace: Some(true),
1848 ..Default::default()
1849 },
1850 cx,
1851 )
1852 .await
1853 })
1854}
1855
1856fn open_project_settings_file(
1857 workspace: &mut Workspace,
1858 _: &OpenProjectSettingsFile,
1859 window: &mut Window,
1860 cx: &mut Context<Workspace>,
1861) {
1862 open_local_file(
1863 workspace,
1864 local_settings_file_relative_path(),
1865 initial_project_settings_content(),
1866 window,
1867 cx,
1868 )
1869}
1870
1871fn open_project_tasks_file(
1872 workspace: &mut Workspace,
1873 _: &OpenProjectTasks,
1874 window: &mut Window,
1875 cx: &mut Context<Workspace>,
1876) {
1877 open_local_file(
1878 workspace,
1879 local_tasks_file_relative_path(),
1880 initial_tasks_content(),
1881 window,
1882 cx,
1883 )
1884}
1885
1886fn open_project_debug_tasks_file(
1887 workspace: &mut Workspace,
1888 _: &zed_actions::OpenProjectDebugTasks,
1889 window: &mut Window,
1890 cx: &mut Context<Workspace>,
1891) {
1892 open_local_file(
1893 workspace,
1894 local_debug_file_relative_path(),
1895 initial_local_debug_tasks_content(),
1896 window,
1897 cx,
1898 )
1899}
1900
1901fn open_local_file(
1902 workspace: &mut Workspace,
1903 settings_relative_path: &'static RelPath,
1904 initial_contents: Cow<'static, str>,
1905 window: &mut Window,
1906 cx: &mut Context<Workspace>,
1907) {
1908 let project = workspace.project().clone();
1909 let worktree = project
1910 .read(cx)
1911 .visible_worktrees(cx)
1912 .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
1913 if let Some(worktree) = worktree {
1914 let tree_id = worktree.read(cx).id();
1915 cx.spawn_in(window, async move |workspace, cx| {
1916 // Check if the file actually exists on disk (even if it's excluded from worktree)
1917 let file_exists = {
1918 let full_path = worktree.read_with(cx, |tree, _| {
1919 tree.abs_path().join(settings_relative_path.as_std_path())
1920 })?;
1921
1922 let fs = project.read_with(cx, |project, _| project.fs().clone())?;
1923
1924 fs.metadata(&full_path)
1925 .await
1926 .ok()
1927 .flatten()
1928 .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo)
1929 };
1930
1931 if !file_exists {
1932 if let Some(dir_path) = settings_relative_path.parent()
1933 && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())?
1934 {
1935 project
1936 .update(cx, |project, cx| {
1937 project.create_entry((tree_id, dir_path), true, cx)
1938 })?
1939 .await
1940 .context("worktree was removed")?;
1941 }
1942
1943 if worktree.read_with(cx, |tree, _| {
1944 tree.entry_for_path(settings_relative_path).is_none()
1945 })? {
1946 project
1947 .update(cx, |project, cx| {
1948 project.create_entry((tree_id, settings_relative_path), false, cx)
1949 })?
1950 .await
1951 .context("worktree was removed")?;
1952 }
1953 }
1954
1955 let editor = workspace
1956 .update_in(cx, |workspace, window, cx| {
1957 workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
1958 })?
1959 .await?
1960 .downcast::<Editor>()
1961 .context("unexpected item type: expected editor item")?;
1962
1963 editor
1964 .downgrade()
1965 .update(cx, |editor, cx| {
1966 if let Some(buffer) = editor.buffer().read(cx).as_singleton()
1967 && buffer.read(cx).is_empty()
1968 {
1969 buffer.update(cx, |buffer, cx| {
1970 buffer.edit([(0..0, initial_contents)], None, cx)
1971 });
1972 }
1973 })
1974 .ok();
1975
1976 anyhow::Ok(())
1977 })
1978 .detach();
1979 } else {
1980 struct NoOpenFolders;
1981
1982 workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
1983 cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
1984 })
1985 }
1986}
1987
1988fn open_telemetry_log_file(
1989 workspace: &mut Workspace,
1990 window: &mut Window,
1991 cx: &mut Context<Workspace>,
1992) {
1993 const HEADER: &str = concat!(
1994 "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
1995 "// Telemetry can be disabled via the `settings.json` file.\n",
1996 "// Here is the data that has been reported for the current session:\n",
1997 );
1998 workspace
1999 .with_local_workspace(window, cx, move |workspace, window, cx| {
2000 let app_state = workspace.app_state().clone();
2001 cx.spawn_in(window, async move |workspace, cx| {
2002 async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
2003 let path = client::telemetry::Telemetry::log_file_path();
2004 app_state.fs.load(&path).await.log_err()
2005 }
2006
2007 let log = fetch_log_string(&app_state)
2008 .await
2009 .unwrap_or_else(|| "// No data has been collected yet".to_string());
2010
2011 const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
2012 let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
2013 if let Some(newline_offset) = log[start_offset..].find('\n') {
2014 start_offset += newline_offset + 1;
2015 }
2016 let log_suffix = &log[start_offset..];
2017 let content = format!("{}\n{}", HEADER, log_suffix);
2018 let json = app_state
2019 .languages
2020 .language_for_name("JSON")
2021 .await
2022 .log_err();
2023
2024 workspace
2025 .update_in(cx, |workspace, window, cx| {
2026 let project = workspace.project().clone();
2027 let buffer = project.update(cx, |project, cx| {
2028 project.create_local_buffer(&content, json, false, cx)
2029 });
2030 let buffer = cx.new(|cx| {
2031 MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
2032 });
2033 workspace.add_item_to_active_pane(
2034 Box::new(cx.new(|cx| {
2035 let mut editor =
2036 Editor::for_multibuffer(buffer, Some(project), window, cx);
2037 editor.set_read_only(true);
2038 editor.set_breadcrumb_header("Telemetry Log".into());
2039 editor
2040 })),
2041 None,
2042 true,
2043 window,
2044 cx,
2045 );
2046 })
2047 .log_err()?;
2048
2049 Some(())
2050 })
2051 .detach();
2052 })
2053 .detach();
2054}
2055
2056fn open_bundled_file(
2057 workspace: &Workspace,
2058 text: Cow<'static, str>,
2059 title: &'static str,
2060 language: &'static str,
2061 window: &mut Window,
2062 cx: &mut Context<Workspace>,
2063) {
2064 let language = workspace.app_state().languages.language_for_name(language);
2065 cx.spawn_in(window, async move |workspace, cx| {
2066 let language = language.await.log_err();
2067 workspace
2068 .update_in(cx, |workspace, window, cx| {
2069 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
2070 let project = workspace.project();
2071 let buffer = project.update(cx, move |project, cx| {
2072 let buffer =
2073 project.create_local_buffer(text.as_ref(), language, false, cx);
2074 buffer.update(cx, |buffer, cx| {
2075 buffer.set_capability(Capability::ReadOnly, cx);
2076 });
2077 buffer
2078 });
2079 let buffer =
2080 cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
2081 workspace.add_item_to_active_pane(
2082 Box::new(cx.new(|cx| {
2083 let mut editor =
2084 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2085 editor.set_read_only(true);
2086 editor.set_should_serialize(false, cx);
2087 editor.set_breadcrumb_header(title.into());
2088 editor
2089 })),
2090 None,
2091 true,
2092 window,
2093 cx,
2094 );
2095 })
2096 })?
2097 .await
2098 })
2099 .detach_and_log_err(cx);
2100}
2101
2102fn open_settings_file(
2103 abs_path: &'static Path,
2104 default_content: impl FnOnce() -> Rope + Send + 'static,
2105 window: &mut Window,
2106 cx: &mut Context<Workspace>,
2107) {
2108 cx.spawn_in(window, async move |workspace, cx| {
2109 let (worktree_creation_task, settings_open_task) = workspace
2110 .update_in(cx, |workspace, window, cx| {
2111 workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
2112 let worktree_creation_task = workspace.project().update(cx, |project, cx| {
2113 // Set up a dedicated worktree for settings, since
2114 // otherwise we're dropping and re-starting LSP servers
2115 // for each file inside on every settings file
2116 // close/open
2117
2118 // TODO: Do note that all other external files (e.g.
2119 // drag and drop from OS) still have their worktrees
2120 // released on file close, causing LSP servers'
2121 // restarts.
2122 project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
2123 });
2124 let settings_open_task =
2125 create_and_open_local_file(abs_path, window, cx, default_content);
2126 (worktree_creation_task, settings_open_task)
2127 })
2128 })?
2129 .await?;
2130 let _ = worktree_creation_task.await?;
2131 let _ = settings_open_task.await?;
2132 anyhow::Ok(())
2133 })
2134 .detach_and_log_err(cx);
2135}
2136
2137fn capture_recent_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
2138 struct CaptureRecentAudioNotification {
2139 focus_handle: gpui::FocusHandle,
2140 save_result: Option<Result<(PathBuf, Duration), anyhow::Error>>,
2141 _save_task: Task<anyhow::Result<()>>,
2142 }
2143
2144 impl gpui::EventEmitter<DismissEvent> for CaptureRecentAudioNotification {}
2145 impl gpui::EventEmitter<SuppressEvent> for CaptureRecentAudioNotification {}
2146 impl gpui::Focusable for CaptureRecentAudioNotification {
2147 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
2148 self.focus_handle.clone()
2149 }
2150 }
2151 impl workspace::notifications::Notification for CaptureRecentAudioNotification {}
2152
2153 impl Render for CaptureRecentAudioNotification {
2154 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2155 let message = match &self.save_result {
2156 None => format!(
2157 "Saving up to {} seconds of recent audio",
2158 REPLAY_DURATION.as_secs(),
2159 ),
2160 Some(Ok((path, duration))) => format!(
2161 "Saved {} seconds of all audio to {}",
2162 duration.as_secs(),
2163 path.display(),
2164 ),
2165 Some(Err(e)) => format!("Error saving audio replays: {e:?}"),
2166 };
2167
2168 NotificationFrame::new()
2169 .with_title(Some("Saved Audio"))
2170 .show_suppress_button(false)
2171 .on_close(cx.listener(|_, _, _, cx| {
2172 cx.emit(DismissEvent);
2173 }))
2174 .with_content(message)
2175 }
2176 }
2177
2178 impl CaptureRecentAudioNotification {
2179 fn new(cx: &mut Context<Self>) -> Self {
2180 if AudioSettings::get_global(cx).rodio_audio {
2181 let executor = cx.background_executor().clone();
2182 let save_task = cx.default_global::<audio::Audio>().save_replays(executor);
2183 let _save_task = cx.spawn(async move |this, cx| {
2184 let res = save_task.await;
2185 this.update(cx, |this, cx| {
2186 this.save_result = Some(res);
2187 cx.notify();
2188 })
2189 });
2190
2191 Self {
2192 focus_handle: cx.focus_handle(),
2193 _save_task,
2194 save_result: None,
2195 }
2196 } else {
2197 Self {
2198 focus_handle: cx.focus_handle(),
2199 _save_task: Task::ready(Ok(())),
2200 save_result: Some(Err(anyhow::anyhow!(
2201 "Capturing recent audio is only supported on the experimental rodio audio pipeline"
2202 ))),
2203 }
2204 }
2205 }
2206 }
2207
2208 workspace.show_notification(
2209 NotificationId::unique::<CaptureRecentAudioNotification>(),
2210 cx,
2211 |cx| cx.new(CaptureRecentAudioNotification::new),
2212 );
2213}
2214
2215/// Eagerly loads the active theme and icon theme based on the selections in the
2216/// theme settings.
2217///
2218/// This fast path exists to load these themes as soon as possible so the user
2219/// doesn't see the default themes while waiting on extensions to load.
2220pub(crate) fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &mut App) {
2221 let extension_store = ExtensionStore::global(cx);
2222 let theme_registry = ThemeRegistry::global(cx);
2223 let theme_settings = ThemeSettings::get_global(cx);
2224 let appearance = SystemAppearance::global(cx).0;
2225
2226 enum LoadTarget {
2227 Theme(PathBuf),
2228 IconTheme((PathBuf, PathBuf)),
2229 }
2230
2231 let theme_name = theme_settings.theme.name(appearance);
2232 let icon_theme_name = theme_settings.icon_theme.name(appearance);
2233 let themes_to_load = [
2234 theme_registry
2235 .get(&theme_name.0)
2236 .is_err()
2237 .then(|| {
2238 extension_store
2239 .read(cx)
2240 .path_to_extension_theme(&theme_name.0)
2241 })
2242 .flatten()
2243 .map(LoadTarget::Theme),
2244 theme_registry
2245 .get_icon_theme(&icon_theme_name.0)
2246 .is_err()
2247 .then(|| {
2248 extension_store
2249 .read(cx)
2250 .path_to_extension_icon_theme(&icon_theme_name.0)
2251 })
2252 .flatten()
2253 .map(LoadTarget::IconTheme),
2254 ];
2255
2256 enum ReloadTarget {
2257 Theme,
2258 IconTheme,
2259 }
2260
2261 let executor = cx.background_executor();
2262 let reload_tasks = parking_lot::Mutex::new(Vec::with_capacity(themes_to_load.len()));
2263
2264 let mut themes_to_load = themes_to_load.into_iter().flatten().peekable();
2265
2266 if themes_to_load.peek().is_none() {
2267 return;
2268 }
2269
2270 executor.block(executor.scoped(|scope| {
2271 for load_target in themes_to_load {
2272 let theme_registry = &theme_registry;
2273 let reload_tasks = &reload_tasks;
2274 let fs = fs.clone();
2275
2276 scope.spawn(async {
2277 match load_target {
2278 LoadTarget::Theme(theme_path) => {
2279 if theme_registry
2280 .load_user_theme(&theme_path, fs)
2281 .await
2282 .log_err()
2283 .is_some()
2284 {
2285 reload_tasks.lock().push(ReloadTarget::Theme);
2286 }
2287 }
2288 LoadTarget::IconTheme((icon_theme_path, icons_root_path)) => {
2289 if theme_registry
2290 .load_icon_theme(&icon_theme_path, &icons_root_path, fs)
2291 .await
2292 .log_err()
2293 .is_some()
2294 {
2295 reload_tasks.lock().push(ReloadTarget::IconTheme);
2296 }
2297 }
2298 }
2299 });
2300 }
2301 }));
2302
2303 for reload_target in reload_tasks.into_inner() {
2304 match reload_target {
2305 ReloadTarget::Theme => GlobalTheme::reload_theme(cx),
2306 ReloadTarget::IconTheme => GlobalTheme::reload_icon_theme(cx),
2307 };
2308 }
2309}
2310
2311#[cfg(test)]
2312mod tests {
2313 use super::*;
2314 use assets::Assets;
2315 use collections::HashSet;
2316 use editor::{
2317 DisplayPoint, Editor, MultiBufferOffset, SelectionEffects, display_map::DisplayRow,
2318 };
2319 use gpui::{
2320 Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, TestAppContext, UpdateGlobal,
2321 VisualTestContext, WindowHandle, actions,
2322 };
2323 use language::LanguageRegistry;
2324 use languages::{markdown_lang, rust_lang};
2325 use pretty_assertions::{assert_eq, assert_ne};
2326 use project::{Project, ProjectPath};
2327 use semver::Version;
2328 use serde_json::json;
2329 use settings::{SaturatingBool, SettingsStore, watch_config_file};
2330 use std::{
2331 path::{Path, PathBuf},
2332 time::Duration,
2333 };
2334 use theme::ThemeRegistry;
2335 use util::{
2336 path,
2337 rel_path::{RelPath, rel_path},
2338 };
2339 use workspace::{
2340 NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
2341 WorkspaceHandle,
2342 item::SaveOptions,
2343 item::{Item, ItemHandle},
2344 open_new, open_paths, pane,
2345 };
2346
2347 #[gpui::test]
2348 async fn test_open_non_existing_file(cx: &mut TestAppContext) {
2349 let app_state = init_test(cx);
2350 app_state
2351 .fs
2352 .as_fake()
2353 .insert_tree(
2354 path!("/root"),
2355 json!({
2356 "a": {
2357 },
2358 }),
2359 )
2360 .await;
2361
2362 cx.update(|cx| {
2363 open_paths(
2364 &[PathBuf::from(path!("/root/a/new"))],
2365 app_state.clone(),
2366 workspace::OpenOptions::default(),
2367 cx,
2368 )
2369 })
2370 .await
2371 .unwrap();
2372 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2373
2374 let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
2375 workspace
2376 .update(cx, |workspace, _, cx| {
2377 assert!(workspace.active_item_as::<Editor>(cx).is_some())
2378 })
2379 .unwrap();
2380 }
2381
2382 #[gpui::test]
2383 async fn test_open_paths_action(cx: &mut TestAppContext) {
2384 let app_state = init_test(cx);
2385 app_state
2386 .fs
2387 .as_fake()
2388 .insert_tree(
2389 "/root",
2390 json!({
2391 "a": {
2392 "aa": null,
2393 "ab": null,
2394 },
2395 "b": {
2396 "ba": null,
2397 "bb": null,
2398 },
2399 "c": {
2400 "ca": null,
2401 "cb": null,
2402 },
2403 "d": {
2404 "da": null,
2405 "db": null,
2406 },
2407 "e": {
2408 "ea": null,
2409 "eb": null,
2410 }
2411 }),
2412 )
2413 .await;
2414
2415 cx.update(|cx| {
2416 open_paths(
2417 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
2418 app_state.clone(),
2419 workspace::OpenOptions::default(),
2420 cx,
2421 )
2422 })
2423 .await
2424 .unwrap();
2425 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2426
2427 cx.update(|cx| {
2428 open_paths(
2429 &[PathBuf::from("/root/a")],
2430 app_state.clone(),
2431 workspace::OpenOptions::default(),
2432 cx,
2433 )
2434 })
2435 .await
2436 .unwrap();
2437 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2438 let workspace_1 = cx
2439 .read(|cx| cx.windows()[0].downcast::<Workspace>())
2440 .unwrap();
2441 cx.run_until_parked();
2442 workspace_1
2443 .update(cx, |workspace, window, cx| {
2444 assert_eq!(workspace.worktrees(cx).count(), 2);
2445 assert!(workspace.left_dock().read(cx).is_open());
2446 assert!(
2447 workspace
2448 .active_pane()
2449 .read(cx)
2450 .focus_handle(cx)
2451 .is_focused(window)
2452 );
2453 })
2454 .unwrap();
2455
2456 cx.update(|cx| {
2457 open_paths(
2458 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
2459 app_state.clone(),
2460 workspace::OpenOptions::default(),
2461 cx,
2462 )
2463 })
2464 .await
2465 .unwrap();
2466 assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2467
2468 // Replace existing windows
2469 let window = cx
2470 .update(|cx| cx.windows()[0].downcast::<Workspace>())
2471 .unwrap();
2472 cx.update(|cx| {
2473 open_paths(
2474 &[PathBuf::from("/root/e")],
2475 app_state,
2476 workspace::OpenOptions {
2477 replace_window: Some(window),
2478 ..Default::default()
2479 },
2480 cx,
2481 )
2482 })
2483 .await
2484 .unwrap();
2485 cx.background_executor.run_until_parked();
2486 assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2487 let workspace_1 = cx
2488 .update(|cx| cx.windows()[0].downcast::<Workspace>())
2489 .unwrap();
2490 workspace_1
2491 .update(cx, |workspace, window, cx| {
2492 assert_eq!(
2493 workspace
2494 .worktrees(cx)
2495 .map(|w| w.read(cx).abs_path())
2496 .collect::<Vec<_>>(),
2497 &[Path::new("/root/e").into()]
2498 );
2499 assert!(workspace.left_dock().read(cx).is_open());
2500 assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
2501 })
2502 .unwrap();
2503 }
2504
2505 #[gpui::test]
2506 async fn test_open_add_new(cx: &mut TestAppContext) {
2507 let app_state = init_test(cx);
2508 app_state
2509 .fs
2510 .as_fake()
2511 .insert_tree(
2512 path!("/root"),
2513 json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
2514 )
2515 .await;
2516
2517 cx.update(|cx| {
2518 open_paths(
2519 &[PathBuf::from(path!("/root/dir"))],
2520 app_state.clone(),
2521 workspace::OpenOptions::default(),
2522 cx,
2523 )
2524 })
2525 .await
2526 .unwrap();
2527 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2528
2529 cx.update(|cx| {
2530 open_paths(
2531 &[PathBuf::from(path!("/root/a"))],
2532 app_state.clone(),
2533 workspace::OpenOptions {
2534 open_new_workspace: Some(false),
2535 ..Default::default()
2536 },
2537 cx,
2538 )
2539 })
2540 .await
2541 .unwrap();
2542 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2543
2544 cx.update(|cx| {
2545 open_paths(
2546 &[PathBuf::from(path!("/root/dir/c"))],
2547 app_state.clone(),
2548 workspace::OpenOptions {
2549 open_new_workspace: Some(true),
2550 ..Default::default()
2551 },
2552 cx,
2553 )
2554 })
2555 .await
2556 .unwrap();
2557 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2558 }
2559
2560 #[gpui::test]
2561 async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
2562 let app_state = init_test(cx);
2563 app_state
2564 .fs
2565 .as_fake()
2566 .insert_tree(
2567 path!("/root"),
2568 json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
2569 )
2570 .await;
2571
2572 cx.update(|cx| {
2573 open_paths(
2574 &[PathBuf::from(path!("/root/dir1/a"))],
2575 app_state.clone(),
2576 workspace::OpenOptions::default(),
2577 cx,
2578 )
2579 })
2580 .await
2581 .unwrap();
2582 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2583 let window1 = cx.update(|cx| cx.active_window().unwrap());
2584
2585 cx.update(|cx| {
2586 open_paths(
2587 &[PathBuf::from(path!("/root/dir2/c"))],
2588 app_state.clone(),
2589 workspace::OpenOptions::default(),
2590 cx,
2591 )
2592 })
2593 .await
2594 .unwrap();
2595 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2596
2597 cx.update(|cx| {
2598 open_paths(
2599 &[PathBuf::from(path!("/root/dir2"))],
2600 app_state.clone(),
2601 workspace::OpenOptions::default(),
2602 cx,
2603 )
2604 })
2605 .await
2606 .unwrap();
2607 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2608 let window2 = cx.update(|cx| cx.active_window().unwrap());
2609 assert!(window1 != window2);
2610 cx.update_window(window1, |_, window, _| window.activate_window())
2611 .unwrap();
2612
2613 cx.update(|cx| {
2614 open_paths(
2615 &[PathBuf::from(path!("/root/dir2/c"))],
2616 app_state.clone(),
2617 workspace::OpenOptions::default(),
2618 cx,
2619 )
2620 })
2621 .await
2622 .unwrap();
2623 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2624 // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
2625 assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
2626 }
2627
2628 #[gpui::test]
2629 async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
2630 let executor = cx.executor();
2631 let app_state = init_test(cx);
2632
2633 cx.update(|cx| {
2634 SettingsStore::update_global(cx, |store, cx| {
2635 store.update_user_settings(cx, |settings| {
2636 settings
2637 .session
2638 .get_or_insert_default()
2639 .restore_unsaved_buffers = Some(false)
2640 });
2641 });
2642 });
2643
2644 app_state
2645 .fs
2646 .as_fake()
2647 .insert_tree(path!("/root"), json!({"a": "hey"}))
2648 .await;
2649
2650 cx.update(|cx| {
2651 open_paths(
2652 &[PathBuf::from(path!("/root/a"))],
2653 app_state.clone(),
2654 workspace::OpenOptions::default(),
2655 cx,
2656 )
2657 })
2658 .await
2659 .unwrap();
2660 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2661
2662 // When opening the workspace, the window is not in a edited state.
2663 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2664
2665 let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2666 cx.update(|cx| window.read(cx).unwrap().is_edited())
2667 };
2668 let pane = window
2669 .read_with(cx, |workspace, _| workspace.active_pane().clone())
2670 .unwrap();
2671 let editor = window
2672 .read_with(cx, |workspace, cx| {
2673 workspace
2674 .active_item(cx)
2675 .unwrap()
2676 .downcast::<Editor>()
2677 .unwrap()
2678 })
2679 .unwrap();
2680
2681 assert!(!window_is_edited(window, cx));
2682
2683 // Editing a buffer marks the window as edited.
2684 window
2685 .update(cx, |_, window, cx| {
2686 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2687 })
2688 .unwrap();
2689
2690 assert!(window_is_edited(window, cx));
2691
2692 // Undoing the edit restores the window's edited state.
2693 window
2694 .update(cx, |_, window, cx| {
2695 editor.update(cx, |editor, cx| {
2696 editor.undo(&Default::default(), window, cx)
2697 });
2698 })
2699 .unwrap();
2700 assert!(!window_is_edited(window, cx));
2701
2702 // Redoing the edit marks the window as edited again.
2703 window
2704 .update(cx, |_, window, cx| {
2705 editor.update(cx, |editor, cx| {
2706 editor.redo(&Default::default(), window, cx)
2707 });
2708 })
2709 .unwrap();
2710 assert!(window_is_edited(window, cx));
2711 let weak = editor.downgrade();
2712
2713 // Closing the item restores the window's edited state.
2714 let close = window
2715 .update(cx, |_, window, cx| {
2716 pane.update(cx, |pane, cx| {
2717 drop(editor);
2718 pane.close_active_item(&Default::default(), window, cx)
2719 })
2720 })
2721 .unwrap();
2722 executor.run_until_parked();
2723
2724 cx.simulate_prompt_answer("Don't Save");
2725 close.await.unwrap();
2726
2727 // Advance the clock to ensure that the item has been serialized and dropped from the queue
2728 cx.executor().advance_clock(Duration::from_secs(1));
2729
2730 weak.assert_released();
2731 assert!(!window_is_edited(window, cx));
2732 // Opening the buffer again doesn't impact the window's edited state.
2733 cx.update(|cx| {
2734 open_paths(
2735 &[PathBuf::from(path!("/root/a"))],
2736 app_state,
2737 workspace::OpenOptions::default(),
2738 cx,
2739 )
2740 })
2741 .await
2742 .unwrap();
2743 executor.run_until_parked();
2744
2745 window
2746 .update(cx, |workspace, _, cx| {
2747 let editor = workspace
2748 .active_item(cx)
2749 .unwrap()
2750 .downcast::<Editor>()
2751 .unwrap();
2752
2753 editor.update(cx, |editor, cx| {
2754 assert_eq!(editor.text(cx), "hey");
2755 });
2756 })
2757 .unwrap();
2758
2759 let editor = window
2760 .read_with(cx, |workspace, cx| {
2761 workspace
2762 .active_item(cx)
2763 .unwrap()
2764 .downcast::<Editor>()
2765 .unwrap()
2766 })
2767 .unwrap();
2768 assert!(!window_is_edited(window, cx));
2769
2770 // Editing the buffer marks the window as edited.
2771 window
2772 .update(cx, |_, window, cx| {
2773 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2774 })
2775 .unwrap();
2776 executor.run_until_parked();
2777 assert!(window_is_edited(window, cx));
2778
2779 // Ensure closing the window via the mouse gets preempted due to the
2780 // buffer having unsaved changes.
2781 assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2782 executor.run_until_parked();
2783 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2784
2785 // The window is successfully closed after the user dismisses the prompt.
2786 cx.simulate_prompt_answer("Don't Save");
2787 executor.run_until_parked();
2788 assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2789 }
2790
2791 #[gpui::test]
2792 async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
2793 let app_state = init_test(cx);
2794 app_state
2795 .fs
2796 .as_fake()
2797 .insert_tree(path!("/root"), json!({"a": "hey"}))
2798 .await;
2799
2800 cx.update(|cx| {
2801 open_paths(
2802 &[PathBuf::from(path!("/root/a"))],
2803 app_state.clone(),
2804 workspace::OpenOptions::default(),
2805 cx,
2806 )
2807 })
2808 .await
2809 .unwrap();
2810
2811 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2812
2813 // When opening the workspace, the window is not in a edited state.
2814 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2815
2816 let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2817 cx.update(|cx| window.read(cx).unwrap().is_edited())
2818 };
2819
2820 let editor = window
2821 .read_with(cx, |workspace, cx| {
2822 workspace
2823 .active_item(cx)
2824 .unwrap()
2825 .downcast::<Editor>()
2826 .unwrap()
2827 })
2828 .unwrap();
2829
2830 assert!(!window_is_edited(window, cx));
2831
2832 // Editing a buffer marks the window as edited.
2833 window
2834 .update(cx, |_, window, cx| {
2835 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2836 })
2837 .unwrap();
2838
2839 assert!(window_is_edited(window, cx));
2840 cx.run_until_parked();
2841
2842 // Advance the clock to make sure the workspace is serialized
2843 cx.executor().advance_clock(Duration::from_secs(1));
2844
2845 // When closing the window, no prompt shows up and the window is closed.
2846 // buffer having unsaved changes.
2847 assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2848 cx.run_until_parked();
2849 assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2850
2851 // When we now reopen the window, the edited state and the edited buffer are back
2852 cx.update(|cx| {
2853 open_paths(
2854 &[PathBuf::from(path!("/root/a"))],
2855 app_state.clone(),
2856 workspace::OpenOptions::default(),
2857 cx,
2858 )
2859 })
2860 .await
2861 .unwrap();
2862
2863 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2864 assert!(cx.update(|cx| cx.active_window().is_some()));
2865
2866 cx.run_until_parked();
2867
2868 // When opening the workspace, the window is not in a edited state.
2869 let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
2870 assert!(window_is_edited(window, cx));
2871
2872 window
2873 .update(cx, |workspace, _, cx| {
2874 let editor = workspace
2875 .active_item(cx)
2876 .unwrap()
2877 .downcast::<editor::Editor>()
2878 .unwrap();
2879 editor.update(cx, |editor, cx| {
2880 assert_eq!(editor.text(cx), "EDIThey");
2881 assert!(editor.is_dirty(cx));
2882 });
2883
2884 editor
2885 })
2886 .unwrap();
2887 }
2888
2889 #[gpui::test]
2890 async fn test_new_empty_workspace(cx: &mut TestAppContext) {
2891 let app_state = init_test(cx);
2892 cx.update(|cx| {
2893 open_new(
2894 Default::default(),
2895 app_state.clone(),
2896 cx,
2897 |workspace, window, cx| {
2898 Editor::new_file(workspace, &Default::default(), window, cx)
2899 },
2900 )
2901 })
2902 .await
2903 .unwrap();
2904 cx.run_until_parked();
2905
2906 let workspace = cx
2907 .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
2908 .unwrap();
2909
2910 let editor = workspace
2911 .update(cx, |workspace, _, cx| {
2912 let editor = workspace
2913 .active_item(cx)
2914 .unwrap()
2915 .downcast::<editor::Editor>()
2916 .unwrap();
2917 editor.update(cx, |editor, cx| {
2918 assert!(editor.text(cx).is_empty());
2919 assert!(!editor.is_dirty(cx));
2920 });
2921
2922 editor
2923 })
2924 .unwrap();
2925
2926 let save_task = workspace
2927 .update(cx, |workspace, window, cx| {
2928 workspace.save_active_item(SaveIntent::Save, window, cx)
2929 })
2930 .unwrap();
2931 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2932 cx.background_executor.run_until_parked();
2933 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
2934 save_task.await.unwrap();
2935 workspace
2936 .update(cx, |_, _, cx| {
2937 editor.update(cx, |editor, cx| {
2938 assert!(!editor.is_dirty(cx));
2939 assert_eq!(editor.title(cx), "the-new-name");
2940 });
2941 })
2942 .unwrap();
2943 }
2944
2945 #[gpui::test]
2946 async fn test_open_entry(cx: &mut TestAppContext) {
2947 let app_state = init_test(cx);
2948 app_state
2949 .fs
2950 .as_fake()
2951 .insert_tree(
2952 path!("/root"),
2953 json!({
2954 "a": {
2955 "file1": "contents 1",
2956 "file2": "contents 2",
2957 "file3": "contents 3",
2958 },
2959 }),
2960 )
2961 .await;
2962
2963 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2964 project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
2965 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2966 let workspace = window.root(cx).unwrap();
2967
2968 let entries = cx.read(|cx| workspace.file_project_paths(cx));
2969 let file1 = entries[0].clone();
2970 let file2 = entries[1].clone();
2971 let file3 = entries[2].clone();
2972
2973 // Open the first entry
2974 let entry_1 = window
2975 .update(cx, |w, window, cx| {
2976 w.open_path(file1.clone(), None, true, window, cx)
2977 })
2978 .unwrap()
2979 .await
2980 .unwrap();
2981 cx.read(|cx| {
2982 let pane = workspace.read(cx).active_pane().read(cx);
2983 assert_eq!(
2984 pane.active_item().unwrap().project_path(cx),
2985 Some(file1.clone())
2986 );
2987 assert_eq!(pane.items_len(), 1);
2988 });
2989
2990 // Open the second entry
2991 window
2992 .update(cx, |w, window, cx| {
2993 w.open_path(file2.clone(), None, true, window, cx)
2994 })
2995 .unwrap()
2996 .await
2997 .unwrap();
2998 cx.read(|cx| {
2999 let pane = workspace.read(cx).active_pane().read(cx);
3000 assert_eq!(
3001 pane.active_item().unwrap().project_path(cx),
3002 Some(file2.clone())
3003 );
3004 assert_eq!(pane.items_len(), 2);
3005 });
3006
3007 // Open the first entry again. The existing pane item is activated.
3008 let entry_1b = window
3009 .update(cx, |w, window, cx| {
3010 w.open_path(file1.clone(), None, true, window, cx)
3011 })
3012 .unwrap()
3013 .await
3014 .unwrap();
3015 assert_eq!(entry_1.item_id(), entry_1b.item_id());
3016
3017 cx.read(|cx| {
3018 let pane = workspace.read(cx).active_pane().read(cx);
3019 assert_eq!(
3020 pane.active_item().unwrap().project_path(cx),
3021 Some(file1.clone())
3022 );
3023 assert_eq!(pane.items_len(), 2);
3024 });
3025
3026 // Split the pane with the first entry, then open the second entry again.
3027 window
3028 .update(cx, |w, window, cx| {
3029 w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx)
3030 })
3031 .unwrap()
3032 .await
3033 .unwrap();
3034 window
3035 .update(cx, |w, window, cx| {
3036 w.open_path(file2.clone(), None, true, window, cx)
3037 })
3038 .unwrap()
3039 .await
3040 .unwrap();
3041
3042 window
3043 .read_with(cx, |w, cx| {
3044 assert_eq!(
3045 w.active_pane()
3046 .read(cx)
3047 .active_item()
3048 .unwrap()
3049 .project_path(cx),
3050 Some(file2.clone())
3051 );
3052 })
3053 .unwrap();
3054
3055 // Open the third entry twice concurrently. Only one pane item is added.
3056 let (t1, t2) = window
3057 .update(cx, |w, window, cx| {
3058 (
3059 w.open_path(file3.clone(), None, true, window, cx),
3060 w.open_path(file3.clone(), None, true, window, cx),
3061 )
3062 })
3063 .unwrap();
3064 t1.await.unwrap();
3065 t2.await.unwrap();
3066 cx.read(|cx| {
3067 let pane = workspace.read(cx).active_pane().read(cx);
3068 assert_eq!(
3069 pane.active_item().unwrap().project_path(cx),
3070 Some(file3.clone())
3071 );
3072 let pane_entries = pane
3073 .items()
3074 .map(|i| i.project_path(cx).unwrap())
3075 .collect::<Vec<_>>();
3076 assert_eq!(pane_entries, &[file1, file2, file3]);
3077 });
3078 }
3079
3080 #[gpui::test]
3081 async fn test_open_paths(cx: &mut TestAppContext) {
3082 let app_state = init_test(cx);
3083
3084 app_state
3085 .fs
3086 .as_fake()
3087 .insert_tree(
3088 path!("/"),
3089 json!({
3090 "dir1": {
3091 "a.txt": ""
3092 },
3093 "dir2": {
3094 "b.txt": ""
3095 },
3096 "dir3": {
3097 "c.txt": ""
3098 },
3099 "d.txt": ""
3100 }),
3101 )
3102 .await;
3103
3104 cx.update(|cx| {
3105 open_paths(
3106 &[PathBuf::from(path!("/dir1/"))],
3107 app_state,
3108 workspace::OpenOptions::default(),
3109 cx,
3110 )
3111 })
3112 .await
3113 .unwrap();
3114 cx.run_until_parked();
3115 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3116 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
3117 let workspace = window.root(cx).unwrap();
3118
3119 #[track_caller]
3120 fn assert_project_panel_selection(
3121 workspace: &Workspace,
3122 expected_worktree_path: &Path,
3123 expected_entry_path: &RelPath,
3124 cx: &App,
3125 ) {
3126 let project_panel = [
3127 workspace.left_dock().read(cx).panel::<ProjectPanel>(),
3128 workspace.right_dock().read(cx).panel::<ProjectPanel>(),
3129 workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
3130 ]
3131 .into_iter()
3132 .find_map(std::convert::identity)
3133 .expect("found no project panels")
3134 .read(cx);
3135 let (selected_worktree, selected_entry) = project_panel
3136 .selected_entry(cx)
3137 .expect("project panel should have a selected entry");
3138 assert_eq!(
3139 selected_worktree.abs_path().as_ref(),
3140 expected_worktree_path,
3141 "Unexpected project panel selected worktree path"
3142 );
3143 assert_eq!(
3144 selected_entry.path.as_ref(),
3145 expected_entry_path,
3146 "Unexpected project panel selected entry path"
3147 );
3148 }
3149
3150 // Open a file within an existing worktree.
3151 window
3152 .update(cx, |workspace, window, cx| {
3153 workspace.open_paths(
3154 vec![path!("/dir1/a.txt").into()],
3155 OpenOptions {
3156 visible: Some(OpenVisible::All),
3157 ..Default::default()
3158 },
3159 None,
3160 window,
3161 cx,
3162 )
3163 })
3164 .unwrap()
3165 .await;
3166 cx.run_until_parked();
3167 cx.read(|cx| {
3168 let workspace = workspace.read(cx);
3169 assert_project_panel_selection(
3170 workspace,
3171 Path::new(path!("/dir1")),
3172 rel_path("a.txt"),
3173 cx,
3174 );
3175 assert_eq!(
3176 workspace
3177 .active_pane()
3178 .read(cx)
3179 .active_item()
3180 .unwrap()
3181 .act_as::<Editor>(cx)
3182 .unwrap()
3183 .read(cx)
3184 .title(cx),
3185 "a.txt"
3186 );
3187 });
3188
3189 // Open a file outside of any existing worktree.
3190 window
3191 .update(cx, |workspace, window, cx| {
3192 workspace.open_paths(
3193 vec![path!("/dir2/b.txt").into()],
3194 OpenOptions {
3195 visible: Some(OpenVisible::All),
3196 ..Default::default()
3197 },
3198 None,
3199 window,
3200 cx,
3201 )
3202 })
3203 .unwrap()
3204 .await;
3205 cx.run_until_parked();
3206 cx.read(|cx| {
3207 let workspace = workspace.read(cx);
3208 assert_project_panel_selection(
3209 workspace,
3210 Path::new(path!("/dir2/b.txt")),
3211 rel_path(""),
3212 cx,
3213 );
3214 let worktree_roots = workspace
3215 .worktrees(cx)
3216 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3217 .collect::<HashSet<_>>();
3218 assert_eq!(
3219 worktree_roots,
3220 vec![path!("/dir1"), path!("/dir2/b.txt")]
3221 .into_iter()
3222 .map(Path::new)
3223 .collect(),
3224 );
3225 assert_eq!(
3226 workspace
3227 .active_pane()
3228 .read(cx)
3229 .active_item()
3230 .unwrap()
3231 .act_as::<Editor>(cx)
3232 .unwrap()
3233 .read(cx)
3234 .title(cx),
3235 "b.txt"
3236 );
3237 });
3238
3239 // Ensure opening a directory and one of its children only adds one worktree.
3240 window
3241 .update(cx, |workspace, window, cx| {
3242 workspace.open_paths(
3243 vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
3244 OpenOptions {
3245 visible: Some(OpenVisible::All),
3246 ..Default::default()
3247 },
3248 None,
3249 window,
3250 cx,
3251 )
3252 })
3253 .unwrap()
3254 .await;
3255 cx.run_until_parked();
3256 cx.read(|cx| {
3257 let workspace = workspace.read(cx);
3258 assert_project_panel_selection(
3259 workspace,
3260 Path::new(path!("/dir3")),
3261 rel_path("c.txt"),
3262 cx,
3263 );
3264 let worktree_roots = workspace
3265 .worktrees(cx)
3266 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3267 .collect::<HashSet<_>>();
3268 assert_eq!(
3269 worktree_roots,
3270 vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
3271 .into_iter()
3272 .map(Path::new)
3273 .collect(),
3274 );
3275 assert_eq!(
3276 workspace
3277 .active_pane()
3278 .read(cx)
3279 .active_item()
3280 .unwrap()
3281 .act_as::<Editor>(cx)
3282 .unwrap()
3283 .read(cx)
3284 .title(cx),
3285 "c.txt"
3286 );
3287 });
3288
3289 // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
3290 window
3291 .update(cx, |workspace, window, cx| {
3292 workspace.open_paths(
3293 vec![path!("/d.txt").into()],
3294 OpenOptions {
3295 visible: Some(OpenVisible::None),
3296 ..Default::default()
3297 },
3298 None,
3299 window,
3300 cx,
3301 )
3302 })
3303 .unwrap()
3304 .await;
3305 cx.run_until_parked();
3306 cx.read(|cx| {
3307 let workspace = workspace.read(cx);
3308 assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx);
3309 let worktree_roots = workspace
3310 .worktrees(cx)
3311 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3312 .collect::<HashSet<_>>();
3313 assert_eq!(
3314 worktree_roots,
3315 vec![
3316 path!("/dir1"),
3317 path!("/dir2/b.txt"),
3318 path!("/dir3"),
3319 path!("/d.txt")
3320 ]
3321 .into_iter()
3322 .map(Path::new)
3323 .collect(),
3324 );
3325
3326 let visible_worktree_roots = workspace
3327 .visible_worktrees(cx)
3328 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3329 .collect::<HashSet<_>>();
3330 assert_eq!(
3331 visible_worktree_roots,
3332 vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
3333 .into_iter()
3334 .map(Path::new)
3335 .collect(),
3336 );
3337
3338 assert_eq!(
3339 workspace
3340 .active_pane()
3341 .read(cx)
3342 .active_item()
3343 .unwrap()
3344 .act_as::<Editor>(cx)
3345 .unwrap()
3346 .read(cx)
3347 .title(cx),
3348 "d.txt"
3349 );
3350 });
3351 }
3352
3353 #[gpui::test]
3354 async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
3355 let app_state = init_test(cx);
3356 cx.update(|cx| {
3357 cx.update_global::<SettingsStore, _>(|store, cx| {
3358 store.update_user_settings(cx, |project_settings| {
3359 project_settings.project.worktree.file_scan_exclusions =
3360 Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3361 });
3362 });
3363 });
3364 app_state
3365 .fs
3366 .as_fake()
3367 .insert_tree(
3368 path!("/root"),
3369 json!({
3370 ".gitignore": "ignored_dir\n",
3371 ".git": {
3372 "HEAD": "ref: refs/heads/main",
3373 },
3374 "regular_dir": {
3375 "file": "regular file contents",
3376 },
3377 "ignored_dir": {
3378 "ignored_subdir": {
3379 "file": "ignored subfile contents",
3380 },
3381 "file": "ignored file contents",
3382 },
3383 "excluded_dir": {
3384 "file": "excluded file contents",
3385 "ignored_subdir": {
3386 "file": "ignored subfile contents",
3387 },
3388 },
3389 }),
3390 )
3391 .await;
3392
3393 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3394 project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
3395 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3396 let workspace = window.root(cx).unwrap();
3397
3398 let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
3399 let paths_to_open = [
3400 PathBuf::from(path!("/root/excluded_dir/file")),
3401 PathBuf::from(path!("/root/.git/HEAD")),
3402 PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
3403 ];
3404 let (opened_workspace, new_items) = cx
3405 .update(|cx| {
3406 workspace::open_paths(
3407 &paths_to_open,
3408 app_state,
3409 workspace::OpenOptions::default(),
3410 cx,
3411 )
3412 })
3413 .await
3414 .unwrap();
3415
3416 assert_eq!(
3417 opened_workspace.root(cx).unwrap().entity_id(),
3418 workspace.entity_id(),
3419 "Excluded files in subfolders of a workspace root should be opened in the workspace"
3420 );
3421 let mut opened_paths = cx.read(|cx| {
3422 assert_eq!(
3423 new_items.len(),
3424 paths_to_open.len(),
3425 "Expect to get the same number of opened items as submitted paths to open"
3426 );
3427 new_items
3428 .iter()
3429 .zip(paths_to_open.iter())
3430 .map(|(i, path)| {
3431 match i {
3432 Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)),
3433 Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
3434 None => None,
3435 }
3436 .flatten()
3437 })
3438 .collect::<Vec<_>>()
3439 });
3440 opened_paths.sort();
3441 assert_eq!(
3442 opened_paths,
3443 vec![
3444 None,
3445 Some(rel_path(".git/HEAD").into()),
3446 Some(rel_path("excluded_dir/file").into()),
3447 ],
3448 "Excluded files should get opened, excluded dir should not get opened"
3449 );
3450
3451 let entries = cx.read(|cx| workspace.file_project_paths(cx));
3452 assert_eq!(
3453 initial_entries, entries,
3454 "Workspace entries should not change after opening excluded files and directories paths"
3455 );
3456
3457 cx.read(|cx| {
3458 let pane = workspace.read(cx).active_pane().read(cx);
3459 let mut opened_buffer_paths = pane
3460 .items()
3461 .map(|i| {
3462 i.project_path(cx)
3463 .expect("all excluded files that got open should have a path")
3464 .path
3465 })
3466 .collect::<Vec<_>>();
3467 opened_buffer_paths.sort();
3468 assert_eq!(
3469 opened_buffer_paths,
3470 vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()],
3471 "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
3472 );
3473 });
3474 }
3475
3476 #[gpui::test]
3477 async fn test_save_conflicting_item(cx: &mut TestAppContext) {
3478 let app_state = init_test(cx);
3479 app_state
3480 .fs
3481 .as_fake()
3482 .insert_tree(path!("/root"), json!({ "a.txt": "" }))
3483 .await;
3484
3485 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3486 project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
3487 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3488 let workspace = window.root(cx).unwrap();
3489
3490 // Open a file within an existing worktree.
3491 window
3492 .update(cx, |workspace, window, cx| {
3493 workspace.open_paths(
3494 vec![PathBuf::from(path!("/root/a.txt"))],
3495 OpenOptions {
3496 visible: Some(OpenVisible::All),
3497 ..Default::default()
3498 },
3499 None,
3500 window,
3501 cx,
3502 )
3503 })
3504 .unwrap()
3505 .await;
3506 let editor = cx.read(|cx| {
3507 let pane = workspace.read(cx).active_pane().read(cx);
3508 let item = pane.active_item().unwrap();
3509 item.downcast::<Editor>().unwrap()
3510 });
3511
3512 window
3513 .update(cx, |_, window, cx| {
3514 editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
3515 })
3516 .unwrap();
3517
3518 app_state
3519 .fs
3520 .as_fake()
3521 .insert_file(path!("/root/a.txt"), b"changed".to_vec())
3522 .await;
3523
3524 cx.run_until_parked();
3525 cx.read(|cx| assert!(editor.is_dirty(cx)));
3526 cx.read(|cx| assert!(editor.has_conflict(cx)));
3527
3528 let save_task = window
3529 .update(cx, |workspace, window, cx| {
3530 workspace.save_active_item(SaveIntent::Save, window, cx)
3531 })
3532 .unwrap();
3533 cx.background_executor.run_until_parked();
3534 cx.simulate_prompt_answer("Overwrite");
3535 save_task.await.unwrap();
3536 window
3537 .update(cx, |_, _, cx| {
3538 editor.update(cx, |editor, cx| {
3539 assert!(!editor.is_dirty(cx));
3540 assert!(!editor.has_conflict(cx));
3541 });
3542 })
3543 .unwrap();
3544 }
3545
3546 #[gpui::test]
3547 async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
3548 let app_state = init_test(cx);
3549 app_state
3550 .fs
3551 .create_dir(Path::new(path!("/root")))
3552 .await
3553 .unwrap();
3554
3555 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3556 project.update(cx, |project, _| {
3557 project.languages().add(markdown_lang());
3558 project.languages().add(rust_lang());
3559 });
3560 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3561 let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
3562
3563 // Create a new untitled buffer
3564 cx.dispatch_action(window.into(), NewFile);
3565 let editor = window
3566 .read_with(cx, |workspace, cx| {
3567 workspace
3568 .active_item(cx)
3569 .unwrap()
3570 .downcast::<Editor>()
3571 .unwrap()
3572 })
3573 .unwrap();
3574
3575 window
3576 .update(cx, |_, window, cx| {
3577 editor.update(cx, |editor, cx| {
3578 assert!(!editor.is_dirty(cx));
3579 assert_eq!(editor.title(cx), "untitled");
3580 assert!(Arc::ptr_eq(
3581 &editor
3582 .buffer()
3583 .read(cx)
3584 .language_at(MultiBufferOffset(0), cx)
3585 .unwrap(),
3586 &languages::PLAIN_TEXT
3587 ));
3588 editor.handle_input("hi", window, cx);
3589 assert!(editor.is_dirty(cx));
3590 });
3591 })
3592 .unwrap();
3593
3594 // Save the buffer. This prompts for a filename.
3595 let save_task = window
3596 .update(cx, |workspace, window, cx| {
3597 workspace.save_active_item(SaveIntent::Save, window, cx)
3598 })
3599 .unwrap();
3600 cx.background_executor.run_until_parked();
3601 cx.simulate_new_path_selection(|parent_dir| {
3602 assert_eq!(parent_dir, Path::new(path!("/root")));
3603 Some(parent_dir.join("the-new-name.rs"))
3604 });
3605 cx.read(|cx| {
3606 assert!(editor.is_dirty(cx));
3607 assert_eq!(editor.read(cx).title(cx), "hi");
3608 });
3609
3610 // When the save completes, the buffer's title is updated and the language is assigned based
3611 // on the path.
3612 save_task.await.unwrap();
3613 window
3614 .update(cx, |_, _, cx| {
3615 editor.update(cx, |editor, cx| {
3616 assert!(!editor.is_dirty(cx));
3617 assert_eq!(editor.title(cx), "the-new-name.rs");
3618 assert_eq!(
3619 editor
3620 .buffer()
3621 .read(cx)
3622 .language_at(MultiBufferOffset(0), cx)
3623 .unwrap()
3624 .name(),
3625 "Rust".into()
3626 );
3627 });
3628 })
3629 .unwrap();
3630
3631 // Edit the file and save it again. This time, there is no filename prompt.
3632 window
3633 .update(cx, |_, window, cx| {
3634 editor.update(cx, |editor, cx| {
3635 editor.handle_input(" there", window, cx);
3636 assert!(editor.is_dirty(cx));
3637 });
3638 })
3639 .unwrap();
3640
3641 let save_task = window
3642 .update(cx, |workspace, window, cx| {
3643 workspace.save_active_item(SaveIntent::Save, window, cx)
3644 })
3645 .unwrap();
3646 save_task.await.unwrap();
3647
3648 assert!(!cx.did_prompt_for_new_path());
3649 window
3650 .update(cx, |_, _, cx| {
3651 editor.update(cx, |editor, cx| {
3652 assert!(!editor.is_dirty(cx));
3653 assert_eq!(editor.title(cx), "the-new-name.rs")
3654 });
3655 })
3656 .unwrap();
3657
3658 // Open the same newly-created file in another pane item. The new editor should reuse
3659 // the same buffer.
3660 cx.dispatch_action(window.into(), NewFile);
3661 window
3662 .update(cx, |workspace, window, cx| {
3663 workspace.split_and_clone(
3664 workspace.active_pane().clone(),
3665 SplitDirection::Right,
3666 window,
3667 cx,
3668 )
3669 })
3670 .unwrap()
3671 .await
3672 .unwrap();
3673 window
3674 .update(cx, |workspace, window, cx| {
3675 workspace.open_path(
3676 (worktree.read(cx).id(), rel_path("the-new-name.rs")),
3677 None,
3678 true,
3679 window,
3680 cx,
3681 )
3682 })
3683 .unwrap()
3684 .await
3685 .unwrap();
3686 let editor2 = window
3687 .update(cx, |workspace, _, cx| {
3688 workspace
3689 .active_item(cx)
3690 .unwrap()
3691 .downcast::<Editor>()
3692 .unwrap()
3693 })
3694 .unwrap();
3695 cx.read(|cx| {
3696 assert_eq!(
3697 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
3698 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
3699 );
3700 })
3701 }
3702
3703 #[gpui::test]
3704 async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
3705 let app_state = init_test(cx);
3706 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3707
3708 let project = Project::test(app_state.fs.clone(), [], cx).await;
3709 project.update(cx, |project, _| {
3710 project.languages().add(language::rust_lang());
3711 project.languages().add(language::markdown_lang());
3712 });
3713 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3714
3715 // Create a new untitled buffer
3716 cx.dispatch_action(window.into(), NewFile);
3717 let editor = window
3718 .read_with(cx, |workspace, cx| {
3719 workspace
3720 .active_item(cx)
3721 .unwrap()
3722 .downcast::<Editor>()
3723 .unwrap()
3724 })
3725 .unwrap();
3726 window
3727 .update(cx, |_, window, cx| {
3728 editor.update(cx, |editor, cx| {
3729 assert!(Arc::ptr_eq(
3730 &editor
3731 .buffer()
3732 .read(cx)
3733 .language_at(MultiBufferOffset(0), cx)
3734 .unwrap(),
3735 &languages::PLAIN_TEXT
3736 ));
3737 editor.handle_input("hi", window, cx);
3738 assert!(editor.is_dirty(cx));
3739 });
3740 })
3741 .unwrap();
3742
3743 // Save the buffer. This prompts for a filename.
3744 let save_task = window
3745 .update(cx, |workspace, window, cx| {
3746 workspace.save_active_item(SaveIntent::Save, window, cx)
3747 })
3748 .unwrap();
3749 cx.background_executor.run_until_parked();
3750 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
3751 save_task.await.unwrap();
3752 // The buffer is not dirty anymore and the language is assigned based on the path.
3753 window
3754 .update(cx, |_, _, cx| {
3755 editor.update(cx, |editor, cx| {
3756 assert!(!editor.is_dirty(cx));
3757 assert_eq!(
3758 editor
3759 .buffer()
3760 .read(cx)
3761 .language_at(MultiBufferOffset(0), cx)
3762 .unwrap()
3763 .name(),
3764 "Rust".into()
3765 )
3766 });
3767 })
3768 .unwrap();
3769 }
3770
3771 #[gpui::test]
3772 async fn test_pane_actions(cx: &mut TestAppContext) {
3773 let app_state = init_test(cx);
3774 app_state
3775 .fs
3776 .as_fake()
3777 .insert_tree(
3778 path!("/root"),
3779 json!({
3780 "a": {
3781 "file1": "contents 1",
3782 "file2": "contents 2",
3783 "file3": "contents 3",
3784 },
3785 }),
3786 )
3787 .await;
3788
3789 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3790 project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
3791 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3792 let workspace = window.root(cx).unwrap();
3793
3794 let entries = cx.read(|cx| workspace.file_project_paths(cx));
3795 let file1 = entries[0].clone();
3796
3797 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
3798
3799 window
3800 .update(cx, |w, window, cx| {
3801 w.open_path(file1.clone(), None, true, window, cx)
3802 })
3803 .unwrap()
3804 .await
3805 .unwrap();
3806
3807 let (editor_1, buffer) = window
3808 .update(cx, |_, window, cx| {
3809 pane_1.update(cx, |pane_1, cx| {
3810 let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
3811 assert_eq!(editor.project_path(cx), Some(file1.clone()));
3812 let buffer = editor.update(cx, |editor, cx| {
3813 editor.insert("dirt", window, cx);
3814 editor.buffer().downgrade()
3815 });
3816 (editor.downgrade(), buffer)
3817 })
3818 })
3819 .unwrap();
3820
3821 cx.dispatch_action(window.into(), pane::SplitRight);
3822 let editor_2 = cx.update(|cx| {
3823 let pane_2 = workspace.read(cx).active_pane().clone();
3824 assert_ne!(pane_1, pane_2);
3825
3826 let pane2_item = pane_2.read(cx).active_item().unwrap();
3827 assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
3828
3829 pane2_item.downcast::<Editor>().unwrap().downgrade()
3830 });
3831 cx.dispatch_action(
3832 window.into(),
3833 workspace::CloseActiveItem {
3834 save_intent: None,
3835 close_pinned: false,
3836 },
3837 );
3838
3839 cx.background_executor.run_until_parked();
3840 window
3841 .read_with(cx, |workspace, _| {
3842 assert_eq!(workspace.panes().len(), 1);
3843 assert_eq!(workspace.active_pane(), &pane_1);
3844 })
3845 .unwrap();
3846
3847 cx.dispatch_action(
3848 window.into(),
3849 workspace::CloseActiveItem {
3850 save_intent: None,
3851 close_pinned: false,
3852 },
3853 );
3854 cx.background_executor.run_until_parked();
3855 cx.simulate_prompt_answer("Don't Save");
3856 cx.background_executor.run_until_parked();
3857
3858 window
3859 .update(cx, |workspace, _, cx| {
3860 assert_eq!(workspace.panes().len(), 1);
3861 assert!(workspace.active_item(cx).is_none());
3862 })
3863 .unwrap();
3864
3865 cx.background_executor
3866 .advance_clock(SERIALIZATION_THROTTLE_TIME);
3867 cx.update(|_| {});
3868 editor_1.assert_released();
3869 editor_2.assert_released();
3870 buffer.assert_released();
3871 }
3872
3873 #[gpui::test]
3874 async fn test_navigation(cx: &mut TestAppContext) {
3875 let app_state = init_test(cx);
3876 app_state
3877 .fs
3878 .as_fake()
3879 .insert_tree(
3880 path!("/root"),
3881 json!({
3882 "a": {
3883 "file1": "contents 1\n".repeat(20),
3884 "file2": "contents 2\n".repeat(20),
3885 "file3": "contents 3\n".repeat(20),
3886 },
3887 }),
3888 )
3889 .await;
3890
3891 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3892 project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
3893 let workspace =
3894 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3895 let pane = workspace
3896 .read_with(cx, |workspace, _| workspace.active_pane().clone())
3897 .unwrap();
3898
3899 let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3900 let file1 = entries[0].clone();
3901 let file2 = entries[1].clone();
3902 let file3 = entries[2].clone();
3903
3904 let editor1 = workspace
3905 .update(cx, |w, window, cx| {
3906 w.open_path(file1.clone(), None, true, window, cx)
3907 })
3908 .unwrap()
3909 .await
3910 .unwrap()
3911 .downcast::<Editor>()
3912 .unwrap();
3913 workspace
3914 .update(cx, |_, window, cx| {
3915 editor1.update(cx, |editor, cx| {
3916 editor.change_selections(Default::default(), window, cx, |s| {
3917 s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
3918 ..DisplayPoint::new(DisplayRow(10), 0)])
3919 });
3920 });
3921 })
3922 .unwrap();
3923
3924 let editor2 = workspace
3925 .update(cx, |w, window, cx| {
3926 w.open_path(file2.clone(), None, true, window, cx)
3927 })
3928 .unwrap()
3929 .await
3930 .unwrap()
3931 .downcast::<Editor>()
3932 .unwrap();
3933 let editor3 = workspace
3934 .update(cx, |w, window, cx| {
3935 w.open_path(file3.clone(), None, true, window, cx)
3936 })
3937 .unwrap()
3938 .await
3939 .unwrap()
3940 .downcast::<Editor>()
3941 .unwrap();
3942
3943 workspace
3944 .update(cx, |_, window, cx| {
3945 editor3.update(cx, |editor, cx| {
3946 editor.change_selections(Default::default(), window, cx, |s| {
3947 s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
3948 ..DisplayPoint::new(DisplayRow(12), 0)])
3949 });
3950 editor.newline(&Default::default(), window, cx);
3951 editor.newline(&Default::default(), window, cx);
3952 editor.move_down(&Default::default(), window, cx);
3953 editor.move_down(&Default::default(), window, cx);
3954 editor.save(
3955 SaveOptions {
3956 format: true,
3957 autosave: false,
3958 },
3959 project.clone(),
3960 window,
3961 cx,
3962 )
3963 })
3964 })
3965 .unwrap()
3966 .await
3967 .unwrap();
3968 workspace
3969 .update(cx, |_, window, cx| {
3970 editor3.update(cx, |editor, cx| {
3971 editor.set_scroll_position(point(0., 12.5), window, cx)
3972 });
3973 })
3974 .unwrap();
3975 assert_eq!(
3976 active_location(&workspace, cx),
3977 (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3978 );
3979
3980 workspace
3981 .update(cx, |w, window, cx| {
3982 w.go_back(w.active_pane().downgrade(), window, cx)
3983 })
3984 .unwrap()
3985 .await
3986 .unwrap();
3987 assert_eq!(
3988 active_location(&workspace, cx),
3989 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3990 );
3991
3992 workspace
3993 .update(cx, |w, window, cx| {
3994 w.go_back(w.active_pane().downgrade(), window, cx)
3995 })
3996 .unwrap()
3997 .await
3998 .unwrap();
3999 assert_eq!(
4000 active_location(&workspace, cx),
4001 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4002 );
4003
4004 workspace
4005 .update(cx, |w, window, cx| {
4006 w.go_back(w.active_pane().downgrade(), window, cx)
4007 })
4008 .unwrap()
4009 .await
4010 .unwrap();
4011 assert_eq!(
4012 active_location(&workspace, cx),
4013 (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
4014 );
4015
4016 workspace
4017 .update(cx, |w, window, cx| {
4018 w.go_back(w.active_pane().downgrade(), window, cx)
4019 })
4020 .unwrap()
4021 .await
4022 .unwrap();
4023 assert_eq!(
4024 active_location(&workspace, cx),
4025 (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4026 );
4027
4028 // Go back one more time and ensure we don't navigate past the first item in the history.
4029 workspace
4030 .update(cx, |w, window, cx| {
4031 w.go_back(w.active_pane().downgrade(), window, cx)
4032 })
4033 .unwrap()
4034 .await
4035 .unwrap();
4036 assert_eq!(
4037 active_location(&workspace, cx),
4038 (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4039 );
4040
4041 workspace
4042 .update(cx, |w, window, cx| {
4043 w.go_forward(w.active_pane().downgrade(), window, cx)
4044 })
4045 .unwrap()
4046 .await
4047 .unwrap();
4048 assert_eq!(
4049 active_location(&workspace, cx),
4050 (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
4051 );
4052
4053 workspace
4054 .update(cx, |w, window, cx| {
4055 w.go_forward(w.active_pane().downgrade(), window, cx)
4056 })
4057 .unwrap()
4058 .await
4059 .unwrap();
4060 assert_eq!(
4061 active_location(&workspace, cx),
4062 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4063 );
4064
4065 // Go forward to an item that has been closed, ensuring it gets re-opened at the same
4066 // location.
4067 workspace
4068 .update(cx, |_, window, cx| {
4069 pane.update(cx, |pane, cx| {
4070 let editor3_id = editor3.entity_id();
4071 drop(editor3);
4072 pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
4073 })
4074 })
4075 .unwrap()
4076 .await
4077 .unwrap();
4078 workspace
4079 .update(cx, |w, window, cx| {
4080 w.go_forward(w.active_pane().downgrade(), window, cx)
4081 })
4082 .unwrap()
4083 .await
4084 .unwrap();
4085 assert_eq!(
4086 active_location(&workspace, cx),
4087 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4088 );
4089
4090 workspace
4091 .update(cx, |w, window, cx| {
4092 w.go_forward(w.active_pane().downgrade(), window, cx)
4093 })
4094 .unwrap()
4095 .await
4096 .unwrap();
4097 assert_eq!(
4098 active_location(&workspace, cx),
4099 (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
4100 );
4101
4102 workspace
4103 .update(cx, |w, window, cx| {
4104 w.go_back(w.active_pane().downgrade(), window, cx)
4105 })
4106 .unwrap()
4107 .await
4108 .unwrap();
4109 assert_eq!(
4110 active_location(&workspace, cx),
4111 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4112 );
4113
4114 // Go back to an item that has been closed and removed from disk
4115 workspace
4116 .update(cx, |_, window, cx| {
4117 pane.update(cx, |pane, cx| {
4118 let editor2_id = editor2.entity_id();
4119 drop(editor2);
4120 pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
4121 })
4122 })
4123 .unwrap()
4124 .await
4125 .unwrap();
4126 app_state
4127 .fs
4128 .remove_file(Path::new(path!("/root/a/file2")), Default::default())
4129 .await
4130 .unwrap();
4131 cx.background_executor.run_until_parked();
4132
4133 workspace
4134 .update(cx, |w, window, cx| {
4135 w.go_back(w.active_pane().downgrade(), window, cx)
4136 })
4137 .unwrap()
4138 .await
4139 .unwrap();
4140 assert_eq!(
4141 active_location(&workspace, cx),
4142 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4143 );
4144 workspace
4145 .update(cx, |w, window, cx| {
4146 w.go_forward(w.active_pane().downgrade(), window, cx)
4147 })
4148 .unwrap()
4149 .await
4150 .unwrap();
4151 assert_eq!(
4152 active_location(&workspace, cx),
4153 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4154 );
4155
4156 // Modify file to collapse multiple nav history entries into the same location.
4157 // Ensure we don't visit the same location twice when navigating.
4158 workspace
4159 .update(cx, |_, window, cx| {
4160 editor1.update(cx, |editor, cx| {
4161 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4162 s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
4163 ..DisplayPoint::new(DisplayRow(15), 0)])
4164 })
4165 });
4166 })
4167 .unwrap();
4168 for _ in 0..5 {
4169 workspace
4170 .update(cx, |_, window, cx| {
4171 editor1.update(cx, |editor, cx| {
4172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4173 s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
4174 ..DisplayPoint::new(DisplayRow(3), 0)])
4175 });
4176 });
4177 })
4178 .unwrap();
4179
4180 workspace
4181 .update(cx, |_, window, cx| {
4182 editor1.update(cx, |editor, cx| {
4183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4184 s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
4185 ..DisplayPoint::new(DisplayRow(13), 0)])
4186 })
4187 });
4188 })
4189 .unwrap();
4190 }
4191 workspace
4192 .update(cx, |_, window, cx| {
4193 editor1.update(cx, |editor, cx| {
4194 editor.transact(window, cx, |editor, window, cx| {
4195 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4196 s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
4197 ..DisplayPoint::new(DisplayRow(14), 0)])
4198 });
4199 editor.insert("", window, cx);
4200 })
4201 });
4202 })
4203 .unwrap();
4204
4205 workspace
4206 .update(cx, |_, window, cx| {
4207 editor1.update(cx, |editor, cx| {
4208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4209 s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
4210 ..DisplayPoint::new(DisplayRow(1), 0)])
4211 })
4212 });
4213 })
4214 .unwrap();
4215 workspace
4216 .update(cx, |w, window, cx| {
4217 w.go_back(w.active_pane().downgrade(), window, cx)
4218 })
4219 .unwrap()
4220 .await
4221 .unwrap();
4222 assert_eq!(
4223 active_location(&workspace, cx),
4224 (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
4225 );
4226 workspace
4227 .update(cx, |w, window, cx| {
4228 w.go_back(w.active_pane().downgrade(), window, cx)
4229 })
4230 .unwrap()
4231 .await
4232 .unwrap();
4233 assert_eq!(
4234 active_location(&workspace, cx),
4235 (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
4236 );
4237
4238 fn active_location(
4239 workspace: &WindowHandle<Workspace>,
4240 cx: &mut TestAppContext,
4241 ) -> (ProjectPath, DisplayPoint, f64) {
4242 workspace
4243 .update(cx, |workspace, _, cx| {
4244 let item = workspace.active_item(cx).unwrap();
4245 let editor = item.downcast::<Editor>().unwrap();
4246 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
4247 (
4248 editor
4249 .selections
4250 .display_ranges(&editor.display_snapshot(cx)),
4251 editor.scroll_position(cx),
4252 )
4253 });
4254 (
4255 item.project_path(cx).unwrap(),
4256 selections[0].start,
4257 scroll_position.y,
4258 )
4259 })
4260 .unwrap()
4261 }
4262 }
4263
4264 #[gpui::test]
4265 async fn test_reopening_closed_items(cx: &mut TestAppContext) {
4266 let app_state = init_test(cx);
4267 app_state
4268 .fs
4269 .as_fake()
4270 .insert_tree(
4271 path!("/root"),
4272 json!({
4273 "a": {
4274 "file1": "",
4275 "file2": "",
4276 "file3": "",
4277 "file4": "",
4278 },
4279 }),
4280 )
4281 .await;
4282
4283 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4284 project.update(cx, |project, _cx| project.languages().add(markdown_lang()));
4285 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
4286 let pane = workspace
4287 .read_with(cx, |workspace, _| workspace.active_pane().clone())
4288 .unwrap();
4289
4290 let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
4291 let file1 = entries[0].clone();
4292 let file2 = entries[1].clone();
4293 let file3 = entries[2].clone();
4294 let file4 = entries[3].clone();
4295
4296 let file1_item_id = workspace
4297 .update(cx, |w, window, cx| {
4298 w.open_path(file1.clone(), None, true, window, cx)
4299 })
4300 .unwrap()
4301 .await
4302 .unwrap()
4303 .item_id();
4304 let file2_item_id = workspace
4305 .update(cx, |w, window, cx| {
4306 w.open_path(file2.clone(), None, true, window, cx)
4307 })
4308 .unwrap()
4309 .await
4310 .unwrap()
4311 .item_id();
4312 let file3_item_id = workspace
4313 .update(cx, |w, window, cx| {
4314 w.open_path(file3.clone(), None, true, window, cx)
4315 })
4316 .unwrap()
4317 .await
4318 .unwrap()
4319 .item_id();
4320 let file4_item_id = workspace
4321 .update(cx, |w, window, cx| {
4322 w.open_path(file4.clone(), None, true, window, cx)
4323 })
4324 .unwrap()
4325 .await
4326 .unwrap()
4327 .item_id();
4328 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4329
4330 // Close all the pane items in some arbitrary order.
4331 workspace
4332 .update(cx, |_, window, cx| {
4333 pane.update(cx, |pane, cx| {
4334 pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
4335 })
4336 })
4337 .unwrap()
4338 .await
4339 .unwrap();
4340 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4341
4342 workspace
4343 .update(cx, |_, window, cx| {
4344 pane.update(cx, |pane, cx| {
4345 pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx)
4346 })
4347 })
4348 .unwrap()
4349 .await
4350 .unwrap();
4351 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4352
4353 workspace
4354 .update(cx, |_, window, cx| {
4355 pane.update(cx, |pane, cx| {
4356 pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
4357 })
4358 })
4359 .unwrap()
4360 .await
4361 .unwrap();
4362 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4363 workspace
4364 .update(cx, |_, window, cx| {
4365 pane.update(cx, |pane, cx| {
4366 pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
4367 })
4368 })
4369 .unwrap()
4370 .await
4371 .unwrap();
4372
4373 assert_eq!(active_path(&workspace, cx), None);
4374
4375 // Reopen all the closed items, ensuring they are reopened in the same order
4376 // in which they were closed.
4377 workspace
4378 .update(cx, Workspace::reopen_closed_item)
4379 .unwrap()
4380 .await
4381 .unwrap();
4382 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4383
4384 workspace
4385 .update(cx, Workspace::reopen_closed_item)
4386 .unwrap()
4387 .await
4388 .unwrap();
4389 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4390
4391 workspace
4392 .update(cx, Workspace::reopen_closed_item)
4393 .unwrap()
4394 .await
4395 .unwrap();
4396 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4397
4398 workspace
4399 .update(cx, Workspace::reopen_closed_item)
4400 .unwrap()
4401 .await
4402 .unwrap();
4403 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4404
4405 // Reopening past the last closed item is a no-op.
4406 workspace
4407 .update(cx, Workspace::reopen_closed_item)
4408 .unwrap()
4409 .await
4410 .unwrap();
4411 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4412
4413 // Reopening closed items doesn't interfere with navigation history.
4414 workspace
4415 .update(cx, |workspace, window, cx| {
4416 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4417 })
4418 .unwrap()
4419 .await
4420 .unwrap();
4421 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4422
4423 workspace
4424 .update(cx, |workspace, window, cx| {
4425 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4426 })
4427 .unwrap()
4428 .await
4429 .unwrap();
4430 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4431
4432 workspace
4433 .update(cx, |workspace, window, cx| {
4434 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4435 })
4436 .unwrap()
4437 .await
4438 .unwrap();
4439 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4440
4441 workspace
4442 .update(cx, |workspace, window, cx| {
4443 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4444 })
4445 .unwrap()
4446 .await
4447 .unwrap();
4448 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4449
4450 workspace
4451 .update(cx, |workspace, window, cx| {
4452 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4453 })
4454 .unwrap()
4455 .await
4456 .unwrap();
4457 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4458
4459 workspace
4460 .update(cx, |workspace, window, cx| {
4461 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4462 })
4463 .unwrap()
4464 .await
4465 .unwrap();
4466 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4467
4468 workspace
4469 .update(cx, |workspace, window, cx| {
4470 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4471 })
4472 .unwrap()
4473 .await
4474 .unwrap();
4475 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4476
4477 workspace
4478 .update(cx, |workspace, window, cx| {
4479 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4480 })
4481 .unwrap()
4482 .await
4483 .unwrap();
4484 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4485
4486 fn active_path(
4487 workspace: &WindowHandle<Workspace>,
4488 cx: &TestAppContext,
4489 ) -> Option<ProjectPath> {
4490 workspace
4491 .read_with(cx, |workspace, cx| {
4492 let item = workspace.active_item(cx)?;
4493 item.project_path(cx)
4494 })
4495 .unwrap()
4496 }
4497 }
4498
4499 fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
4500 cx.update(|cx| {
4501 let app_state = AppState::test(cx);
4502
4503 theme::init(theme::LoadThemes::JustBase, cx);
4504 client::init(&app_state.client, cx);
4505 workspace::init(app_state.clone(), cx);
4506 onboarding::init(cx);
4507 app_state
4508 })
4509 }
4510
4511 actions!(test_only, [ActionA, ActionB]);
4512
4513 #[gpui::test]
4514 async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
4515 let executor = cx.executor();
4516 let app_state = init_keymap_test(cx);
4517 let project = Project::test(app_state.fs.clone(), [], cx).await;
4518 let workspace =
4519 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4520
4521 // From the Atom keymap
4522 use workspace::ActivatePreviousPane;
4523 // From the JetBrains keymap
4524 use workspace::ActivatePreviousItem;
4525
4526 app_state
4527 .fs
4528 .save(
4529 "/settings.json".as_ref(),
4530 &r#"{"base_keymap": "Atom"}"#.into(),
4531 Default::default(),
4532 )
4533 .await
4534 .unwrap();
4535
4536 app_state
4537 .fs
4538 .save(
4539 "/keymap.json".as_ref(),
4540 &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4541 Default::default(),
4542 )
4543 .await
4544 .unwrap();
4545 executor.run_until_parked();
4546 cx.update(|cx| {
4547 let settings_rx = watch_config_file(
4548 &executor,
4549 app_state.fs.clone(),
4550 PathBuf::from("/settings.json"),
4551 );
4552 let keymap_rx = watch_config_file(
4553 &executor,
4554 app_state.fs.clone(),
4555 PathBuf::from("/keymap.json"),
4556 );
4557 let global_settings_rx = watch_config_file(
4558 &executor,
4559 app_state.fs.clone(),
4560 PathBuf::from("/global_settings.json"),
4561 );
4562 handle_settings_file_changes(settings_rx, global_settings_rx, cx);
4563 handle_keymap_file_changes(keymap_rx, cx);
4564 });
4565 workspace
4566 .update(cx, |workspace, _, cx| {
4567 workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4568 workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4569 workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
4570 workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
4571 cx.notify();
4572 })
4573 .unwrap();
4574 executor.run_until_parked();
4575 // Test loading the keymap base at all
4576 assert_key_bindings_for(
4577 workspace.into(),
4578 cx,
4579 vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4580 line!(),
4581 );
4582
4583 // Test modifying the users keymap, while retaining the base keymap
4584 app_state
4585 .fs
4586 .save(
4587 "/keymap.json".as_ref(),
4588 &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(),
4589 Default::default(),
4590 )
4591 .await
4592 .unwrap();
4593
4594 executor.run_until_parked();
4595
4596 assert_key_bindings_for(
4597 workspace.into(),
4598 cx,
4599 vec![("backspace", &ActionB), ("k", &ActivatePreviousPane)],
4600 line!(),
4601 );
4602
4603 // Test modifying the base, while retaining the users keymap
4604 app_state
4605 .fs
4606 .save(
4607 "/settings.json".as_ref(),
4608 &r#"{"base_keymap": "JetBrains"}"#.into(),
4609 Default::default(),
4610 )
4611 .await
4612 .unwrap();
4613
4614 executor.run_until_parked();
4615
4616 assert_key_bindings_for(
4617 workspace.into(),
4618 cx,
4619 vec![("backspace", &ActionB), ("{", &ActivatePreviousItem)],
4620 line!(),
4621 );
4622 }
4623
4624 #[gpui::test]
4625 async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
4626 let executor = cx.executor();
4627 let app_state = init_keymap_test(cx);
4628 let project = Project::test(app_state.fs.clone(), [], cx).await;
4629 let workspace =
4630 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4631
4632 // From the Atom keymap
4633 use workspace::ActivatePreviousPane;
4634 // From the JetBrains keymap
4635 use diagnostics::Deploy;
4636
4637 workspace
4638 .update(cx, |workspace, _, _| {
4639 workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4640 workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4641 workspace.register_action(|_, _: &Deploy, _window, _cx| {});
4642 })
4643 .unwrap();
4644 app_state
4645 .fs
4646 .save(
4647 "/settings.json".as_ref(),
4648 &r#"{"base_keymap": "Atom"}"#.into(),
4649 Default::default(),
4650 )
4651 .await
4652 .unwrap();
4653 app_state
4654 .fs
4655 .save(
4656 "/keymap.json".as_ref(),
4657 &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4658 Default::default(),
4659 )
4660 .await
4661 .unwrap();
4662
4663 cx.update(|cx| {
4664 let settings_rx = watch_config_file(
4665 &executor,
4666 app_state.fs.clone(),
4667 PathBuf::from("/settings.json"),
4668 );
4669 let keymap_rx = watch_config_file(
4670 &executor,
4671 app_state.fs.clone(),
4672 PathBuf::from("/keymap.json"),
4673 );
4674
4675 let global_settings_rx = watch_config_file(
4676 &executor,
4677 app_state.fs.clone(),
4678 PathBuf::from("/global_settings.json"),
4679 );
4680 handle_settings_file_changes(settings_rx, global_settings_rx, cx);
4681 handle_keymap_file_changes(keymap_rx, cx);
4682 });
4683
4684 cx.background_executor.run_until_parked();
4685
4686 cx.background_executor.run_until_parked();
4687 // Test loading the keymap base at all
4688 assert_key_bindings_for(
4689 workspace.into(),
4690 cx,
4691 vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4692 line!(),
4693 );
4694
4695 // Test disabling the key binding for the base keymap
4696 app_state
4697 .fs
4698 .save(
4699 "/keymap.json".as_ref(),
4700 &r#"[{"bindings": {"backspace": null}}]"#.into(),
4701 Default::default(),
4702 )
4703 .await
4704 .unwrap();
4705
4706 cx.background_executor.run_until_parked();
4707
4708 assert_key_bindings_for(
4709 workspace.into(),
4710 cx,
4711 vec![("k", &ActivatePreviousPane)],
4712 line!(),
4713 );
4714
4715 // Test modifying the base, while retaining the users keymap
4716 app_state
4717 .fs
4718 .save(
4719 "/settings.json".as_ref(),
4720 &r#"{"base_keymap": "JetBrains"}"#.into(),
4721 Default::default(),
4722 )
4723 .await
4724 .unwrap();
4725
4726 cx.background_executor.run_until_parked();
4727
4728 assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!());
4729 }
4730
4731 #[gpui::test]
4732 async fn test_generate_keymap_json_schema_for_registered_actions(
4733 cx: &mut gpui::TestAppContext,
4734 ) {
4735 init_keymap_test(cx);
4736 cx.update(|cx| {
4737 // Make sure it doesn't panic.
4738 KeymapFile::generate_json_schema_for_registered_actions(cx);
4739 });
4740 }
4741
4742 /// Checks that action namespaces are the expected set. The purpose of this is to prevent typos
4743 /// and let you know when introducing a new namespace.
4744 #[gpui::test]
4745 async fn test_action_namespaces(cx: &mut gpui::TestAppContext) {
4746 use itertools::Itertools;
4747
4748 init_keymap_test(cx);
4749 cx.update(|cx| {
4750 let all_actions = cx.all_action_names();
4751
4752 let mut actions_without_namespace = Vec::new();
4753 let all_namespaces = all_actions
4754 .iter()
4755 .filter_map(|action_name| {
4756 let namespace = action_name
4757 .split("::")
4758 .collect::<Vec<_>>()
4759 .into_iter()
4760 .rev()
4761 .skip(1)
4762 .rev()
4763 .join("::");
4764 if namespace.is_empty() {
4765 actions_without_namespace.push(*action_name);
4766 }
4767 if &namespace == "test_only" || &namespace == "stories" {
4768 None
4769 } else {
4770 Some(namespace)
4771 }
4772 })
4773 .sorted()
4774 .dedup()
4775 .collect::<Vec<_>>();
4776 assert_eq!(actions_without_namespace, Vec::<&str>::new());
4777
4778 let expected_namespaces = vec![
4779 "action",
4780 "activity_indicator",
4781 "agent",
4782 "agents",
4783 "app_menu",
4784 "assistant",
4785 "assistant2",
4786 "auto_update",
4787 "branch_picker",
4788 "bedrock",
4789 "branches",
4790 "buffer_search",
4791 "channel_modal",
4792 "cli",
4793 "client",
4794 "collab",
4795 "collab_panel",
4796 "command_palette",
4797 "console",
4798 "context_server",
4799 "copilot",
4800 "debug_panel",
4801 "debugger",
4802 "dev",
4803 "diagnostics",
4804 "edit_prediction",
4805 "editor",
4806 "feedback",
4807 "file_finder",
4808 "git",
4809 "git_onboarding",
4810 "git_panel",
4811 "go_to_line",
4812 "icon_theme_selector",
4813 "inline_assistant",
4814 "journal",
4815 "keymap_editor",
4816 "keystroke_input",
4817 "language_selector",
4818 "welcome",
4819 "line_ending_selector",
4820 "lsp_tool",
4821 "markdown",
4822 "menu",
4823 "notebook",
4824 "notification_panel",
4825 "onboarding",
4826 "outline",
4827 "outline_panel",
4828 "pane",
4829 "panel",
4830 "picker",
4831 "project_panel",
4832 "project_search",
4833 "project_symbols",
4834 "projects",
4835 "repl",
4836 "rules_library",
4837 "search",
4838 "settings_editor",
4839 "settings_profile_selector",
4840 "snippets",
4841 "stash_picker",
4842 "supermaven",
4843 "svg",
4844 "syntax_tree_view",
4845 "tab_switcher",
4846 "task",
4847 "terminal",
4848 "terminal_panel",
4849 "theme_selector",
4850 "toast",
4851 "toolchain",
4852 "variable_list",
4853 "vim",
4854 "window",
4855 "workspace",
4856 "zed",
4857 "zed_actions",
4858 "zed_predict_onboarding",
4859 "zeta",
4860 ];
4861 assert_eq!(
4862 all_namespaces,
4863 expected_namespaces
4864 .into_iter()
4865 .map(|namespace| namespace.to_string())
4866 .sorted()
4867 .collect::<Vec<_>>()
4868 );
4869 });
4870 }
4871
4872 #[gpui::test]
4873 fn test_bundled_settings_and_themes(cx: &mut App) {
4874 cx.text_system()
4875 .add_fonts(vec![
4876 Assets
4877 .load("fonts/lilex/Lilex-Regular.ttf")
4878 .unwrap()
4879 .unwrap(),
4880 Assets
4881 .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf")
4882 .unwrap()
4883 .unwrap(),
4884 ])
4885 .unwrap();
4886 let themes = ThemeRegistry::default();
4887 settings::init(cx);
4888 theme::init(theme::LoadThemes::JustBase, cx);
4889
4890 let mut has_default_theme = false;
4891 for theme_name in themes.list().into_iter().map(|meta| meta.name) {
4892 let theme = themes.get(&theme_name).unwrap();
4893 assert_eq!(theme.name, theme_name);
4894 if theme.name.as_ref() == "One Dark" {
4895 has_default_theme = true;
4896 }
4897 }
4898 assert!(has_default_theme);
4899 }
4900
4901 #[gpui::test]
4902 async fn test_bundled_files_editor(cx: &mut TestAppContext) {
4903 let app_state = init_test(cx);
4904 cx.update(init);
4905
4906 let project = Project::test(app_state.fs.clone(), [], cx).await;
4907 let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
4908
4909 cx.update(|cx| {
4910 cx.dispatch_action(&OpenDefaultSettings);
4911 });
4912 cx.run_until_parked();
4913
4914 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
4915
4916 let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
4917 let active_editor = workspace
4918 .update(cx, |workspace, _, cx| {
4919 workspace.active_item_as::<Editor>(cx)
4920 })
4921 .unwrap();
4922 assert!(
4923 active_editor.is_some(),
4924 "Settings action should have opened an editor with the default file contents"
4925 );
4926
4927 let active_editor = active_editor.unwrap();
4928 assert!(
4929 active_editor.read_with(cx, |editor, cx| editor.read_only(cx)),
4930 "Default settings should be readonly"
4931 );
4932 assert!(
4933 active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()),
4934 "The underlying buffer should also be readonly for the shipped default settings"
4935 );
4936 }
4937
4938 #[gpui::test]
4939 async fn test_bundled_languages(cx: &mut TestAppContext) {
4940 let fs = fs::FakeFs::new(cx.background_executor.clone());
4941 env_logger::builder().is_test(true).try_init().ok();
4942 let settings = cx.update(SettingsStore::test);
4943 cx.set_global(settings);
4944 let languages = LanguageRegistry::test(cx.executor());
4945 let languages = Arc::new(languages);
4946 let node_runtime = node_runtime::NodeRuntime::unavailable();
4947 cx.update(|cx| {
4948 languages::init(languages.clone(), fs, node_runtime, cx);
4949 });
4950 for name in languages.language_names() {
4951 languages
4952 .language_for_name(name.as_ref())
4953 .await
4954 .with_context(|| format!("language name {name}"))
4955 .unwrap();
4956 }
4957 cx.run_until_parked();
4958 }
4959
4960 pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
4961 init_test_with_state(cx, cx.update(AppState::test))
4962 }
4963
4964 fn init_test_with_state(
4965 cx: &mut TestAppContext,
4966 mut app_state: Arc<AppState>,
4967 ) -> Arc<AppState> {
4968 cx.update(move |cx| {
4969 env_logger::builder().is_test(true).try_init().ok();
4970
4971 let state = Arc::get_mut(&mut app_state).unwrap();
4972 state.build_window_options = build_window_options;
4973 app_state.languages.add(markdown_lang());
4974
4975 gpui_tokio::init(cx);
4976 theme::init(theme::LoadThemes::JustBase, cx);
4977 audio::init(cx);
4978 channel::init(&app_state.client, app_state.user_store.clone(), cx);
4979 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4980 notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4981 workspace::init(app_state.clone(), cx);
4982 release_channel::init(Version::new(0, 0, 0), cx);
4983 command_palette::init(cx);
4984 editor::init(cx);
4985 collab_ui::init(&app_state, cx);
4986 git_ui::init(cx);
4987 project_panel::init(cx);
4988 outline_panel::init(cx);
4989 terminal_view::init(cx);
4990 copilot::copilot_chat::init(
4991 app_state.fs.clone(),
4992 app_state.client.http_client(),
4993 copilot::copilot_chat::CopilotChatConfiguration::default(),
4994 cx,
4995 );
4996 image_viewer::init(cx);
4997 language_model::init(app_state.client.clone(), cx);
4998 language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
4999 web_search::init(cx);
5000 web_search_providers::init(app_state.client.clone(), cx);
5001 let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
5002 agent_ui::init(
5003 app_state.fs.clone(),
5004 app_state.client.clone(),
5005 prompt_builder.clone(),
5006 app_state.languages.clone(),
5007 false,
5008 cx,
5009 );
5010 agent_ui_v2::agents_panel::init(cx);
5011 repl::init(app_state.fs.clone(), cx);
5012 repl::notebook::init(cx);
5013 tasks_ui::init(cx);
5014 project::debugger::breakpoint_store::BreakpointStore::init(
5015 &app_state.client.clone().into(),
5016 );
5017 project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
5018 debugger_ui::init(cx);
5019 initialize_workspace(app_state.clone(), prompt_builder, cx);
5020 search::init(cx);
5021 app_state
5022 })
5023 }
5024
5025 #[track_caller]
5026 fn assert_key_bindings_for(
5027 window: AnyWindowHandle,
5028 cx: &TestAppContext,
5029 actions: Vec<(&'static str, &dyn Action)>,
5030 line: u32,
5031 ) {
5032 let available_actions = cx
5033 .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
5034 .unwrap();
5035 for (key, action) in actions {
5036 let bindings = cx
5037 .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
5038 .unwrap();
5039 // assert that...
5040 assert!(
5041 available_actions.iter().any(|bound_action| {
5042 // actions match...
5043 bound_action.partial_eq(action)
5044 }),
5045 "On {} Failed to find {}",
5046 line,
5047 action.name(),
5048 );
5049 assert!(
5050 // and key strokes contain the given key
5051 bindings
5052 .into_iter()
5053 .any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)),
5054 "On {} Failed to find {} with key binding {}",
5055 line,
5056 action.name(),
5057 key
5058 );
5059 }
5060 }
5061
5062 #[gpui::test]
5063 async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
5064 // Use the proper initialization for runtime state
5065 let app_state = init_keymap_test(cx);
5066
5067 eprintln!("Running test_opening_project_settings_when_excluded");
5068
5069 // 1. Set up a project with some project settings
5070 let settings_init =
5071 r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
5072 app_state
5073 .fs
5074 .as_fake()
5075 .insert_tree(
5076 Path::new("/root"),
5077 json!({
5078 ".zed": {
5079 "settings.json": settings_init
5080 }
5081 }),
5082 )
5083 .await;
5084
5085 eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");
5086
5087 // 2. Create a project with the file system and load it
5088 let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;
5089
5090 // Save original settings content for comparison
5091 let original_settings = app_state
5092 .fs
5093 .load(Path::new("/root/.zed/settings.json"))
5094 .await
5095 .unwrap();
5096
5097 let original_settings_str = original_settings.clone();
5098
5099 // Verify settings exist on disk and have expected content
5100 eprintln!("Original settings content: {}", original_settings_str);
5101 assert!(
5102 original_settings_str.contains("UNIQUEVALUE"),
5103 "Test setup failed - settings file doesn't contain our marker"
5104 );
5105
5106 // 3. Add .zed to file scan exclusions in user settings
5107 cx.update_global::<SettingsStore, _>(|store, cx| {
5108 store.update_user_settings(cx, |worktree_settings| {
5109 worktree_settings.project.worktree.file_scan_exclusions =
5110 Some(vec![".zed".to_string()]);
5111 });
5112 });
5113
5114 eprintln!("Added .zed to file_scan_exclusions in settings");
5115
5116 // 4. Run tasks to apply settings
5117 cx.background_executor.run_until_parked();
5118
5119 // 5. Critical: Verify .zed is actually excluded from worktree
5120 let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap());
5121
5122 let has_zed_entry =
5123 cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some());
5124
5125 eprintln!(
5126 "Is .zed directory visible in worktree after exclusion: {}",
5127 has_zed_entry
5128 );
5129
5130 // This assertion verifies the test is set up correctly to show the bug
5131 // If .zed is not excluded, the test will fail here
5132 assert!(
5133 !has_zed_entry,
5134 "Test precondition failed: .zed directory should be excluded but was found in worktree"
5135 );
5136
5137 // 6. Create workspace and trigger the actual function that causes the bug
5138 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5139 window
5140 .update(cx, |workspace, window, cx| {
5141 // Call the exact function that contains the bug
5142 eprintln!("About to call open_project_settings_file");
5143 open_project_settings_file(workspace, &OpenProjectSettingsFile, window, cx);
5144 })
5145 .unwrap();
5146
5147 // 7. Run background tasks until completion
5148 cx.background_executor.run_until_parked();
5149
5150 // 8. Verify file contents after calling function
5151 let new_content = app_state
5152 .fs
5153 .load(Path::new("/root/.zed/settings.json"))
5154 .await
5155 .unwrap();
5156
5157 let new_content_str = new_content;
5158 eprintln!("New settings content: {}", new_content_str);
5159
5160 // The bug causes the settings to be overwritten with empty settings
5161 // So if the unique value is no longer present, the bug has been reproduced
5162 let bug_exists = !new_content_str.contains("UNIQUEVALUE");
5163 eprintln!("Bug reproduced: {}", bug_exists);
5164
5165 // This assertion should fail if the bug exists - showing the bug is real
5166 assert!(
5167 new_content_str.contains("UNIQUEVALUE"),
5168 "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
5169 );
5170 }
5171
5172 #[gpui::test]
5173 async fn test_disable_ai_crash(cx: &mut gpui::TestAppContext) {
5174 let app_state = init_test(cx);
5175 cx.update(init);
5176 let project = Project::test(app_state.fs.clone(), [], cx).await;
5177 let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
5178
5179 cx.run_until_parked();
5180
5181 cx.update(|cx| {
5182 SettingsStore::update_global(cx, |settings_store, cx| {
5183 settings_store.update_user_settings(cx, |settings| {
5184 settings.disable_ai = Some(SaturatingBool(true));
5185 });
5186 });
5187 });
5188
5189 cx.run_until_parked();
5190
5191 // If this panics, the test has failed
5192 }
5193
5194 #[gpui::test]
5195 async fn test_prefer_focused_window(cx: &mut gpui::TestAppContext) {
5196 let app_state = init_test(cx);
5197 let paths = [PathBuf::from(path!("/dir/document.txt"))];
5198
5199 app_state
5200 .fs
5201 .as_fake()
5202 .insert_tree(
5203 path!("/dir"),
5204 json!({
5205 "document.txt": "Some of the documentation's content."
5206 }),
5207 )
5208 .await;
5209
5210 let project_a = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
5211 let window_a =
5212 cx.add_window(|window, cx| Workspace::test_new(project_a.clone(), window, cx));
5213
5214 let project_b = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
5215 let window_b =
5216 cx.add_window(|window, cx| Workspace::test_new(project_b.clone(), window, cx));
5217
5218 let project_c = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
5219 let window_c =
5220 cx.add_window(|window, cx| Workspace::test_new(project_c.clone(), window, cx));
5221
5222 for window in [window_a, window_b, window_c] {
5223 let _ = cx.update_window(*window, |_, window, _| {
5224 window.activate_window();
5225 });
5226
5227 cx.update(|cx| {
5228 let open_options = OpenOptions {
5229 prefer_focused_window: true,
5230 ..Default::default()
5231 };
5232
5233 workspace::open_paths(&paths, app_state.clone(), open_options, cx)
5234 })
5235 .await
5236 .unwrap();
5237
5238 cx.update_window(*window, |_, window, _| assert!(window.is_window_active()))
5239 .unwrap();
5240
5241 let _ = window.read_with(cx, |workspace, cx| {
5242 let pane = workspace.active_pane().read(cx);
5243 let project_path = pane.active_item().unwrap().project_path(cx).unwrap();
5244
5245 assert_eq!(
5246 project_path.path.as_ref().as_std_path().to_str().unwrap(),
5247 path!("document.txt")
5248 )
5249 });
5250 }
5251 }
5252}