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