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