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