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