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