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