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