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