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