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