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