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