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