1pub mod languages;
2pub mod menus;
3#[cfg(any(test, feature = "test-support"))]
4pub mod test;
5use anyhow::Context;
6use assets::Assets;
7use breadcrumbs::Breadcrumbs;
8pub use client;
9use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
10use collections::VecDeque;
11pub use editor;
12use editor::{Editor, MultiBuffer};
13
14use feedback::{
15 feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton,
16};
17use futures::StreamExt;
18use gpui::{
19 actions,
20 geometry::vector::vec2f,
21 impl_actions,
22 platform::{WindowBounds, WindowOptions},
23 AssetSource, Platform, PromptLevel, TitlebarOptions, ViewContext, WindowKind,
24};
25use language::Rope;
26pub use lsp;
27pub use project;
28use project_panel::ProjectPanel;
29use search::{BufferSearchBar, ProjectSearchBar};
30use serde::Deserialize;
31use serde_json::to_string_pretty;
32use settings::Settings;
33use std::{borrow::Cow, env, path::Path, str, sync::Arc};
34use terminal_view::terminal_button::{self, TerminalButton};
35use util::{channel::ReleaseChannel, paths, ResultExt};
36use uuid::Uuid;
37pub use workspace;
38use workspace::{sidebar::SidebarSide, AppState, Restart, Workspace};
39
40#[derive(Deserialize, Clone, PartialEq)]
41pub struct OpenBrowser {
42 url: Arc<str>,
43}
44
45impl_actions!(zed, [OpenBrowser]);
46
47actions!(
48 zed,
49 [
50 About,
51 Hide,
52 HideOthers,
53 ShowAll,
54 Minimize,
55 Zoom,
56 ToggleFullScreen,
57 Quit,
58 DebugElements,
59 OpenSettings,
60 OpenLog,
61 OpenLicenses,
62 OpenTelemetryLog,
63 OpenKeymap,
64 OpenDefaultSettings,
65 OpenDefaultKeymap,
66 IncreaseBufferFontSize,
67 DecreaseBufferFontSize,
68 ResetBufferFontSize,
69 ResetDatabase,
70 ]
71);
72
73const MIN_FONT_SIZE: f32 = 6.0;
74
75pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
76 terminal_button::init(cx);
77 cx.add_action(about);
78 cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| {
79 cx.platform().hide();
80 });
81 cx.add_global_action(|_: &HideOthers, cx: &mut gpui::MutableAppContext| {
82 cx.platform().hide_other_apps();
83 });
84 cx.add_global_action(|_: &ShowAll, cx: &mut gpui::MutableAppContext| {
85 cx.platform().unhide_other_apps();
86 });
87 cx.add_action(
88 |_: &mut Workspace, _: &Minimize, cx: &mut ViewContext<Workspace>| {
89 cx.minimize_window();
90 },
91 );
92 cx.add_action(
93 |_: &mut Workspace, _: &Zoom, cx: &mut ViewContext<Workspace>| {
94 cx.zoom_window();
95 },
96 );
97 cx.add_action(
98 |_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext<Workspace>| {
99 cx.toggle_full_screen();
100 },
101 );
102 cx.add_action(
103 |workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext<Workspace>| {
104 if let Some(item) = workspace
105 .titlebar_item()
106 .and_then(|item| item.downcast::<CollabTitlebarItem>())
107 {
108 cx.as_mut().defer(move |cx| {
109 item.update(cx, |item, cx| {
110 item.toggle_contacts_popover(&Default::default(), cx);
111 });
112 });
113 }
114 },
115 );
116 cx.add_global_action(quit);
117 cx.add_global_action(restart);
118 cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
119 cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
120 cx.update_global::<Settings, _, _>(|settings, cx| {
121 settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
122 if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
123 *terminal_font_size = (*terminal_font_size + 1.0).max(MIN_FONT_SIZE);
124 }
125 cx.refresh_windows();
126 });
127 });
128 cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
129 cx.update_global::<Settings, _, _>(|settings, cx| {
130 settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
131 if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
132 *terminal_font_size = (*terminal_font_size - 1.0).max(MIN_FONT_SIZE);
133 }
134 cx.refresh_windows();
135 });
136 });
137 cx.add_global_action(move |_: &ResetBufferFontSize, cx| {
138 cx.update_global::<Settings, _, _>(|settings, cx| {
139 settings.buffer_font_size = settings.default_buffer_font_size;
140 settings.terminal_overrides.font_size = settings.terminal_defaults.font_size;
141 cx.refresh_windows();
142 });
143 });
144 cx.add_global_action(move |_: &install_cli::Install, cx| {
145 cx.spawn(|cx| async move {
146 install_cli::install_cli(&cx)
147 .await
148 .context("error creating CLI symlink")
149 })
150 .detach_and_log_err(cx);
151 });
152 cx.add_action({
153 let app_state = app_state.clone();
154 move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
155 open_config_file(&paths::SETTINGS, app_state.clone(), cx, || {
156 str::from_utf8(
157 Assets
158 .load("settings/initial_user_settings.json")
159 .unwrap()
160 .as_ref(),
161 )
162 .unwrap()
163 .into()
164 });
165 }
166 });
167 cx.add_action({
168 let app_state = app_state.clone();
169 move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
170 open_log_file(workspace, app_state.clone(), cx);
171 }
172 });
173 cx.add_action({
174 let app_state = app_state.clone();
175 move |_: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
176 open_bundled_file(
177 app_state.clone(),
178 "licenses.md",
179 "Open Source License Attribution",
180 "Markdown",
181 cx,
182 );
183 }
184 });
185 cx.add_action({
186 let app_state = app_state.clone();
187 move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
188 open_telemetry_log_file(workspace, app_state.clone(), cx);
189 }
190 });
191 cx.add_action({
192 let app_state = app_state.clone();
193 move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
194 open_config_file(&paths::KEYMAP, app_state.clone(), cx, Default::default);
195 }
196 });
197 cx.add_action({
198 let app_state = app_state.clone();
199 move |_: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
200 open_bundled_file(
201 app_state.clone(),
202 "keymaps/default.json",
203 "Default Key Bindings",
204 "JSON",
205 cx,
206 );
207 }
208 });
209 cx.add_action({
210 let app_state = app_state.clone();
211 move |_: &mut Workspace, _: &OpenDefaultSettings, cx: &mut ViewContext<Workspace>| {
212 open_bundled_file(
213 app_state.clone(),
214 "settings/default.json",
215 "Default Settings",
216 "JSON",
217 cx,
218 );
219 }
220 });
221 cx.add_action({
222 let app_state = app_state.clone();
223 move |_: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
224 let app_state = app_state.clone();
225 let markdown = app_state.languages.language_for_name("JSON");
226 let content = to_string_pretty(&cx.debug_elements()).unwrap();
227 cx.spawn(|workspace, mut cx| async move {
228 let markdown = markdown.await.log_err();
229 workspace
230 .update(&mut cx, |workspace, cx| {
231 workspace.with_local_workspace(&app_state, cx, move |workspace, cx| {
232 let project = workspace.project().clone();
233
234 let buffer = project
235 .update(cx, |project, cx| {
236 project.create_buffer(&content, markdown, cx)
237 })
238 .expect("creating buffers on a local workspace always succeeds");
239 let buffer = cx.add_model(|cx| {
240 MultiBuffer::singleton(buffer, cx)
241 .with_title("Debug Elements".into())
242 });
243 workspace.add_item(
244 Box::new(cx.add_view(|cx| {
245 Editor::for_multibuffer(buffer, Some(project.clone()), cx)
246 })),
247 cx,
248 );
249 })
250 })
251 .await;
252 })
253 .detach();
254 }
255 });
256 cx.add_action(
257 |workspace: &mut Workspace,
258 _: &project_panel::ToggleFocus,
259 cx: &mut ViewContext<Workspace>| {
260 workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
261 },
262 );
263 activity_indicator::init(cx);
264 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
265 settings::KeymapFileContent::load_defaults(cx);
266}
267
268pub fn initialize_workspace(
269 workspace: &mut Workspace,
270 app_state: &Arc<AppState>,
271 cx: &mut ViewContext<Workspace>,
272) {
273 let workspace_handle = cx.handle();
274 cx.subscribe(&workspace_handle, {
275 move |_, _, event, cx| {
276 if let workspace::Event::PaneAdded(pane) = event {
277 pane.update(cx, |pane, cx| {
278 pane.toolbar().update(cx, |toolbar, cx| {
279 let breadcrumbs = cx.add_view(|_| Breadcrumbs::new());
280 toolbar.add_item(breadcrumbs, cx);
281 let buffer_search_bar = cx.add_view(BufferSearchBar::new);
282 toolbar.add_item(buffer_search_bar, cx);
283 let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
284 toolbar.add_item(project_search_bar, cx);
285 let submit_feedback_button = cx.add_view(|_| SubmitFeedbackButton::new());
286 toolbar.add_item(submit_feedback_button, cx);
287 let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
288 toolbar.add_item(feedback_info_text, cx);
289 })
290 });
291 }
292 }
293 })
294 .detach();
295
296 cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
297 cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
298
299 let collab_titlebar_item =
300 cx.add_view(|cx| CollabTitlebarItem::new(&workspace_handle, &app_state.user_store, cx));
301 workspace.set_titlebar_item(collab_titlebar_item, cx);
302
303 let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
304 workspace.left_sidebar().update(cx, |sidebar, cx| {
305 sidebar.add_item(
306 "icons/folder_tree_16.svg",
307 "Project Panel".to_string(),
308 project_panel,
309 cx,
310 )
311 });
312
313 let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
314 let diagnostic_summary =
315 cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
316 let activity_indicator =
317 activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
318 let active_buffer_language = cx.add_view(|_| language_selector::ActiveBufferLanguage::new());
319 let feedback_button =
320 cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new());
321 let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
322 workspace.status_bar().update(cx, |status_bar, cx| {
323 status_bar.add_left_item(diagnostic_summary, cx);
324 status_bar.add_left_item(activity_indicator, cx);
325 status_bar.add_right_item(toggle_terminal, cx);
326 status_bar.add_right_item(feedback_button, cx);
327 status_bar.add_right_item(active_buffer_language, cx);
328 status_bar.add_right_item(cursor_position, cx);
329 });
330
331 auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
332
333 let window_id = cx.window_id();
334 vim::observe_keystrokes(window_id, cx);
335
336 cx.on_window_should_close(|workspace, cx| {
337 if let Some(task) = workspace.close(&Default::default(), cx) {
338 task.detach_and_log_err(cx);
339 }
340 false
341 });
342}
343
344pub fn build_window_options(
345 bounds: Option<WindowBounds>,
346 display: Option<Uuid>,
347 platform: &dyn Platform,
348) -> WindowOptions<'static> {
349 let bounds = bounds.unwrap_or(WindowBounds::Maximized);
350 let screen = display.and_then(|display| platform.screen_by_id(display));
351
352 WindowOptions {
353 titlebar: Some(TitlebarOptions {
354 title: None,
355 appears_transparent: true,
356 traffic_light_position: Some(vec2f(8., 8.)),
357 }),
358 center: false,
359 focus: true,
360 kind: WindowKind::Normal,
361 is_movable: true,
362 bounds,
363 screen,
364 }
365}
366
367fn restart(_: &Restart, cx: &mut gpui::MutableAppContext) {
368 let mut workspaces = cx
369 .window_ids()
370 .filter_map(|window_id| cx.root_view::<Workspace>(window_id))
371 .collect::<Vec<_>>();
372
373 // If multiple windows have unsaved changes, and need a save prompt,
374 // prompt in the active window before switching to a different window.
375 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
376
377 let should_confirm = cx.global::<Settings>().confirm_quit;
378 cx.spawn(|mut cx| async move {
379 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
380 let answer = cx
381 .prompt(
382 workspace.window_id(),
383 PromptLevel::Info,
384 "Are you sure you want to restart?",
385 &["Restart", "Cancel"],
386 )
387 .next()
388 .await;
389 if answer != Some(0) {
390 return Ok(());
391 }
392 }
393
394 // If the user cancels any save prompt, then keep the app open.
395 for workspace in workspaces {
396 if !workspace
397 .update(&mut cx, |workspace, cx| {
398 workspace.prepare_to_close(true, cx)
399 })
400 .await?
401 {
402 return Ok(());
403 }
404 }
405 cx.platform().restart();
406 anyhow::Ok(())
407 })
408 .detach_and_log_err(cx);
409}
410
411fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
412 let mut workspaces = cx
413 .window_ids()
414 .filter_map(|window_id| cx.root_view::<Workspace>(window_id))
415 .collect::<Vec<_>>();
416
417 // If multiple windows have unsaved changes, and need a save prompt,
418 // prompt in the active window before switching to a different window.
419 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
420
421 let should_confirm = cx.global::<Settings>().confirm_quit;
422 cx.spawn(|mut cx| async move {
423 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
424 let answer = cx
425 .prompt(
426 workspace.window_id(),
427 PromptLevel::Info,
428 "Are you sure you want to quit?",
429 &["Quit", "Cancel"],
430 )
431 .next()
432 .await;
433 if answer != Some(0) {
434 return Ok(());
435 }
436 }
437
438 // If the user cancels any save prompt, then keep the app open.
439 for workspace in workspaces {
440 if !workspace
441 .update(&mut cx, |workspace, cx| {
442 workspace.prepare_to_close(true, cx)
443 })
444 .await?
445 {
446 return Ok(());
447 }
448 }
449 cx.platform().quit();
450 anyhow::Ok(())
451 })
452 .detach_and_log_err(cx);
453}
454
455fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
456 let app_name = cx.global::<ReleaseChannel>().display_name();
457 let version = env!("CARGO_PKG_VERSION");
458 cx.prompt(
459 gpui::PromptLevel::Info,
460 &format!("{app_name} {version}"),
461 &["OK"],
462 );
463}
464
465fn open_config_file(
466 path: &'static Path,
467 app_state: Arc<AppState>,
468 cx: &mut ViewContext<Workspace>,
469 default_content: impl 'static + Send + FnOnce() -> Rope,
470) {
471 cx.spawn(|workspace, mut cx| async move {
472 let fs = &app_state.fs;
473 if !fs.is_file(path).await {
474 fs.create_file(path, Default::default()).await?;
475 fs.save(path, &default_content(), Default::default())
476 .await?;
477 }
478
479 workspace
480 .update(&mut cx, |workspace, cx| {
481 workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
482 workspace.open_paths(vec![path.to_path_buf()], false, cx)
483 })
484 })
485 .await
486 .await;
487 Ok::<_, anyhow::Error>(())
488 })
489 .detach_and_log_err(cx)
490}
491
492fn open_log_file(
493 workspace: &mut Workspace,
494 app_state: Arc<AppState>,
495 cx: &mut ViewContext<Workspace>,
496) {
497 const MAX_LINES: usize = 1000;
498
499 workspace
500 .with_local_workspace(&app_state.clone(), cx, move |_, cx| {
501 cx.spawn_weak(|workspace, mut cx| async move {
502 let (old_log, new_log) = futures::join!(
503 app_state.fs.load(&paths::OLD_LOG),
504 app_state.fs.load(&paths::LOG)
505 );
506
507 if let Some(workspace) = workspace.upgrade(&cx) {
508 let mut lines = VecDeque::with_capacity(MAX_LINES);
509 for line in old_log
510 .iter()
511 .flat_map(|log| log.lines())
512 .chain(new_log.iter().flat_map(|log| log.lines()))
513 {
514 if lines.len() == MAX_LINES {
515 lines.pop_front();
516 }
517 lines.push_back(line);
518 }
519 let log = lines
520 .into_iter()
521 .flat_map(|line| [line, "\n"])
522 .collect::<String>();
523
524 workspace.update(&mut cx, |workspace, cx| {
525 let project = workspace.project().clone();
526 let buffer = project
527 .update(cx, |project, cx| project.create_buffer("", None, cx))
528 .expect("creating buffers on a local workspace always succeeds");
529 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
530
531 let buffer = cx.add_model(|cx| {
532 MultiBuffer::singleton(buffer, cx).with_title("Log".into())
533 });
534 workspace.add_item(
535 Box::new(
536 cx.add_view(|cx| {
537 Editor::for_multibuffer(buffer, Some(project), cx)
538 }),
539 ),
540 cx,
541 );
542 });
543 }
544 })
545 .detach();
546 })
547 .detach();
548}
549
550fn open_telemetry_log_file(
551 workspace: &mut Workspace,
552 app_state: Arc<AppState>,
553 cx: &mut ViewContext<Workspace>,
554) {
555 workspace.with_local_workspace(&app_state.clone(), cx, move |_, cx| {
556 cx.spawn_weak(|workspace, mut cx| async move {
557 let workspace = workspace.upgrade(&cx)?;
558
559 async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
560 let path = app_state.client.telemetry_log_file_path()?;
561 app_state.fs.load(&path).await.log_err()
562 }
563
564 let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
565
566 const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
567 let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
568 if let Some(newline_offset) = log[start_offset..].find('\n') {
569 start_offset += newline_offset + 1;
570 }
571 let log_suffix = &log[start_offset..];
572 let json = app_state.languages.language_for_name("JSON").await.log_err();
573
574 workspace.update(&mut cx, |workspace, cx| {
575 let project = workspace.project().clone();
576 let buffer = project
577 .update(cx, |project, cx| project.create_buffer("", None, cx))
578 .expect("creating buffers on a local workspace always succeeds");
579 buffer.update(cx, |buffer, cx| {
580 buffer.set_language(json, cx);
581 buffer.edit(
582 [(
583 0..0,
584 concat!(
585 "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
586 "// Telemetry can be disabled via the `settings.json` file.\n",
587 "// Here is the data that has been reported for the current session:\n",
588 "\n"
589 ),
590 )],
591 None,
592 cx,
593 );
594 buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
595 });
596
597 let buffer = cx.add_model(|cx| {
598 MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
599 });
600 workspace.add_item(
601 Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
602 cx,
603 );
604 });
605
606 Some(())
607 })
608 .detach();
609 }).detach();
610}
611
612fn open_bundled_file(
613 app_state: Arc<AppState>,
614 asset_path: &'static str,
615 title: &'static str,
616 language: &'static str,
617 cx: &mut ViewContext<Workspace>,
618) {
619 let language = app_state.languages.language_for_name(language);
620 cx.spawn(|workspace, mut cx| async move {
621 let language = language.await.log_err();
622 workspace
623 .update(&mut cx, |workspace, cx| {
624 workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
625 let project = workspace.project();
626 let buffer = project.update(cx, |project, cx| {
627 let text = Assets::get(asset_path)
628 .map(|f| f.data)
629 .unwrap_or_else(|| Cow::Borrowed(b"File not found"));
630 let text = str::from_utf8(text.as_ref()).unwrap();
631 project
632 .create_buffer(text, language, cx)
633 .expect("creating buffers on a local workspace always succeeds")
634 });
635 let buffer = cx.add_model(|cx| {
636 MultiBuffer::singleton(buffer, cx).with_title(title.into())
637 });
638 workspace.add_item(
639 Box::new(cx.add_view(|cx| {
640 Editor::for_multibuffer(buffer, Some(project.clone()), cx)
641 })),
642 cx,
643 );
644 })
645 })
646 .await;
647 })
648 .detach();
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use assets::Assets;
655 use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
656 use gpui::{
657 executor::Deterministic, AssetSource, MutableAppContext, Task, TestAppContext, ViewHandle,
658 };
659 use language::LanguageRegistry;
660 use project::{Project, ProjectPath};
661 use serde_json::json;
662 use std::{
663 collections::HashSet,
664 path::{Path, PathBuf},
665 };
666 use theme::ThemeRegistry;
667 use workspace::{
668 item::{Item, ItemHandle},
669 open_new, open_paths, pane, NewFile, Pane, SplitDirection, WorkspaceHandle,
670 };
671
672 #[gpui::test]
673 async fn test_open_paths_action(cx: &mut TestAppContext) {
674 let app_state = init(cx);
675 app_state
676 .fs
677 .as_fake()
678 .insert_tree(
679 "/root",
680 json!({
681 "a": {
682 "aa": null,
683 "ab": null,
684 },
685 "b": {
686 "ba": null,
687 "bb": null,
688 },
689 "c": {
690 "ca": null,
691 "cb": null,
692 },
693 "d": {
694 "da": null,
695 "db": null,
696 },
697 }),
698 )
699 .await;
700
701 cx.update(|cx| {
702 open_paths(
703 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
704 &app_state,
705 None,
706 cx,
707 )
708 })
709 .await;
710 assert_eq!(cx.window_ids().len(), 1);
711
712 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
713 .await;
714 assert_eq!(cx.window_ids().len(), 1);
715 let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
716 workspace_1.update(cx, |workspace, cx| {
717 assert_eq!(workspace.worktrees(cx).count(), 2);
718 assert!(workspace.left_sidebar().read(cx).is_open());
719 assert!(workspace.active_pane().is_focused(cx));
720 });
721
722 cx.update(|cx| {
723 open_paths(
724 &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
725 &app_state,
726 None,
727 cx,
728 )
729 })
730 .await;
731 assert_eq!(cx.window_ids().len(), 2);
732
733 // Replace existing windows
734 let window_id = cx.window_ids()[0];
735 cx.update(|cx| {
736 open_paths(
737 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
738 &app_state,
739 Some(window_id),
740 cx,
741 )
742 })
743 .await;
744 assert_eq!(cx.window_ids().len(), 2);
745 let workspace_1 = cx.root_view::<Workspace>(window_id).unwrap();
746 workspace_1.read_with(cx, |workspace, cx| {
747 assert_eq!(
748 workspace
749 .worktrees(cx)
750 .map(|w| w.read(cx).abs_path())
751 .collect::<Vec<_>>(),
752 &[Path::new("/root/c").into(), Path::new("/root/d").into()]
753 );
754 assert!(workspace.left_sidebar().read(cx).is_open());
755 assert!(workspace.active_pane().is_focused(cx));
756 });
757 }
758
759 #[gpui::test]
760 async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
761 let app_state = init(cx);
762 app_state
763 .fs
764 .as_fake()
765 .insert_tree("/root", json!({"a": "hey"}))
766 .await;
767
768 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
769 .await;
770 assert_eq!(cx.window_ids().len(), 1);
771
772 // When opening the workspace, the window is not in a edited state.
773 let workspace = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
774 let editor = workspace.read_with(cx, |workspace, cx| {
775 workspace
776 .active_item(cx)
777 .unwrap()
778 .downcast::<Editor>()
779 .unwrap()
780 });
781 assert!(!cx.is_window_edited(workspace.window_id()));
782
783 // Editing a buffer marks the window as edited.
784 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
785 assert!(cx.is_window_edited(workspace.window_id()));
786
787 // Undoing the edit restores the window's edited state.
788 editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
789 assert!(!cx.is_window_edited(workspace.window_id()));
790
791 // Redoing the edit marks the window as edited again.
792 editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
793 assert!(cx.is_window_edited(workspace.window_id()));
794
795 // Closing the item restores the window's edited state.
796 let close = workspace.update(cx, |workspace, cx| {
797 drop(editor);
798 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
799 });
800 executor.run_until_parked();
801 cx.simulate_prompt_answer(workspace.window_id(), 1);
802 close.await.unwrap();
803 assert!(!cx.is_window_edited(workspace.window_id()));
804
805 // Opening the buffer again doesn't impact the window's edited state.
806 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
807 .await;
808 let editor = workspace.read_with(cx, |workspace, cx| {
809 workspace
810 .active_item(cx)
811 .unwrap()
812 .downcast::<Editor>()
813 .unwrap()
814 });
815 assert!(!cx.is_window_edited(workspace.window_id()));
816
817 // Editing the buffer marks the window as edited.
818 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
819 assert!(cx.is_window_edited(workspace.window_id()));
820
821 // Ensure closing the window via the mouse gets preempted due to the
822 // buffer having unsaved changes.
823 assert!(!cx.simulate_window_close(workspace.window_id()));
824 executor.run_until_parked();
825 assert_eq!(cx.window_ids().len(), 1);
826
827 // The window is successfully closed after the user dismisses the prompt.
828 cx.simulate_prompt_answer(workspace.window_id(), 1);
829 executor.run_until_parked();
830 assert_eq!(cx.window_ids().len(), 0);
831 }
832
833 #[gpui::test]
834 async fn test_new_empty_workspace(cx: &mut TestAppContext) {
835 let app_state = init(cx);
836 cx.update(|cx| open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)))
837 .await;
838
839 let window_id = *cx.window_ids().first().unwrap();
840 let workspace = cx.root_view::<Workspace>(window_id).unwrap();
841 let editor = workspace.update(cx, |workspace, cx| {
842 workspace
843 .active_item(cx)
844 .unwrap()
845 .downcast::<editor::Editor>()
846 .unwrap()
847 });
848
849 editor.update(cx, |editor, cx| {
850 assert!(editor.text(cx).is_empty());
851 });
852
853 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
854 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
855 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
856 save_task.await.unwrap();
857 editor.read_with(cx, |editor, cx| {
858 assert!(!editor.is_dirty(cx));
859 assert_eq!(editor.title(cx), "the-new-name");
860 });
861 }
862
863 #[gpui::test]
864 async fn test_open_entry(cx: &mut TestAppContext) {
865 let app_state = init(cx);
866 app_state
867 .fs
868 .as_fake()
869 .insert_tree(
870 "/root",
871 json!({
872 "a": {
873 "file1": "contents 1",
874 "file2": "contents 2",
875 "file3": "contents 3",
876 },
877 }),
878 )
879 .await;
880
881 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
882 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
883
884 let entries = cx.read(|cx| workspace.file_project_paths(cx));
885 let file1 = entries[0].clone();
886 let file2 = entries[1].clone();
887 let file3 = entries[2].clone();
888
889 // Open the first entry
890 let entry_1 = workspace
891 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
892 .await
893 .unwrap();
894 cx.read(|cx| {
895 let pane = workspace.read(cx).active_pane().read(cx);
896 assert_eq!(
897 pane.active_item().unwrap().project_path(cx),
898 Some(file1.clone())
899 );
900 assert_eq!(pane.items_len(), 1);
901 });
902
903 // Open the second entry
904 workspace
905 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
906 .await
907 .unwrap();
908 cx.read(|cx| {
909 let pane = workspace.read(cx).active_pane().read(cx);
910 assert_eq!(
911 pane.active_item().unwrap().project_path(cx),
912 Some(file2.clone())
913 );
914 assert_eq!(pane.items_len(), 2);
915 });
916
917 // Open the first entry again. The existing pane item is activated.
918 let entry_1b = workspace
919 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
920 .await
921 .unwrap();
922 assert_eq!(entry_1.id(), entry_1b.id());
923
924 cx.read(|cx| {
925 let pane = workspace.read(cx).active_pane().read(cx);
926 assert_eq!(
927 pane.active_item().unwrap().project_path(cx),
928 Some(file1.clone())
929 );
930 assert_eq!(pane.items_len(), 2);
931 });
932
933 // Split the pane with the first entry, then open the second entry again.
934 workspace
935 .update(cx, |w, cx| {
936 w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
937 w.open_path(file2.clone(), None, true, cx)
938 })
939 .await
940 .unwrap();
941
942 workspace.read_with(cx, |w, cx| {
943 assert_eq!(
944 w.active_pane()
945 .read(cx)
946 .active_item()
947 .unwrap()
948 .project_path(cx.as_ref()),
949 Some(file2.clone())
950 );
951 });
952
953 // Open the third entry twice concurrently. Only one pane item is added.
954 let (t1, t2) = workspace.update(cx, |w, cx| {
955 (
956 w.open_path(file3.clone(), None, true, cx),
957 w.open_path(file3.clone(), None, true, cx),
958 )
959 });
960 t1.await.unwrap();
961 t2.await.unwrap();
962 cx.read(|cx| {
963 let pane = workspace.read(cx).active_pane().read(cx);
964 assert_eq!(
965 pane.active_item().unwrap().project_path(cx),
966 Some(file3.clone())
967 );
968 let pane_entries = pane
969 .items()
970 .map(|i| i.project_path(cx).unwrap())
971 .collect::<Vec<_>>();
972 assert_eq!(pane_entries, &[file1, file2, file3]);
973 });
974 }
975
976 #[gpui::test]
977 async fn test_open_paths(cx: &mut TestAppContext) {
978 let app_state = init(cx);
979
980 app_state
981 .fs
982 .as_fake()
983 .insert_tree(
984 "/",
985 json!({
986 "dir1": {
987 "a.txt": ""
988 },
989 "dir2": {
990 "b.txt": ""
991 },
992 "dir3": {
993 "c.txt": ""
994 },
995 "d.txt": ""
996 }),
997 )
998 .await;
999
1000 let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
1001 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1002
1003 // Open a file within an existing worktree.
1004 cx.update(|cx| {
1005 workspace.update(cx, |view, cx| {
1006 view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
1007 })
1008 })
1009 .await;
1010 cx.read(|cx| {
1011 assert_eq!(
1012 workspace
1013 .read(cx)
1014 .active_pane()
1015 .read(cx)
1016 .active_item()
1017 .unwrap()
1018 .to_any()
1019 .downcast::<Editor>()
1020 .unwrap()
1021 .read(cx)
1022 .title(cx),
1023 "a.txt"
1024 );
1025 });
1026
1027 // Open a file outside of any existing worktree.
1028 cx.update(|cx| {
1029 workspace.update(cx, |view, cx| {
1030 view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
1031 })
1032 })
1033 .await;
1034 cx.read(|cx| {
1035 let worktree_roots = workspace
1036 .read(cx)
1037 .worktrees(cx)
1038 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1039 .collect::<HashSet<_>>();
1040 assert_eq!(
1041 worktree_roots,
1042 vec!["/dir1", "/dir2/b.txt"]
1043 .into_iter()
1044 .map(Path::new)
1045 .collect(),
1046 );
1047 assert_eq!(
1048 workspace
1049 .read(cx)
1050 .active_pane()
1051 .read(cx)
1052 .active_item()
1053 .unwrap()
1054 .to_any()
1055 .downcast::<Editor>()
1056 .unwrap()
1057 .read(cx)
1058 .title(cx),
1059 "b.txt"
1060 );
1061 });
1062
1063 // Ensure opening a directory and one of its children only adds one worktree.
1064 cx.update(|cx| {
1065 workspace.update(cx, |view, cx| {
1066 view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
1067 })
1068 })
1069 .await;
1070 cx.read(|cx| {
1071 let worktree_roots = workspace
1072 .read(cx)
1073 .worktrees(cx)
1074 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1075 .collect::<HashSet<_>>();
1076 assert_eq!(
1077 worktree_roots,
1078 vec!["/dir1", "/dir2/b.txt", "/dir3"]
1079 .into_iter()
1080 .map(Path::new)
1081 .collect(),
1082 );
1083 assert_eq!(
1084 workspace
1085 .read(cx)
1086 .active_pane()
1087 .read(cx)
1088 .active_item()
1089 .unwrap()
1090 .to_any()
1091 .downcast::<Editor>()
1092 .unwrap()
1093 .read(cx)
1094 .title(cx),
1095 "c.txt"
1096 );
1097 });
1098
1099 // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
1100 cx.update(|cx| {
1101 workspace.update(cx, |view, cx| {
1102 view.open_paths(vec!["/d.txt".into()], false, cx)
1103 })
1104 })
1105 .await;
1106 cx.read(|cx| {
1107 let worktree_roots = workspace
1108 .read(cx)
1109 .worktrees(cx)
1110 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1111 .collect::<HashSet<_>>();
1112 assert_eq!(
1113 worktree_roots,
1114 vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
1115 .into_iter()
1116 .map(Path::new)
1117 .collect(),
1118 );
1119
1120 let visible_worktree_roots = workspace
1121 .read(cx)
1122 .visible_worktrees(cx)
1123 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1124 .collect::<HashSet<_>>();
1125 assert_eq!(
1126 visible_worktree_roots,
1127 vec!["/dir1", "/dir2/b.txt", "/dir3"]
1128 .into_iter()
1129 .map(Path::new)
1130 .collect(),
1131 );
1132
1133 assert_eq!(
1134 workspace
1135 .read(cx)
1136 .active_pane()
1137 .read(cx)
1138 .active_item()
1139 .unwrap()
1140 .to_any()
1141 .downcast::<Editor>()
1142 .unwrap()
1143 .read(cx)
1144 .title(cx),
1145 "d.txt"
1146 );
1147 });
1148 }
1149
1150 #[gpui::test]
1151 async fn test_save_conflicting_item(cx: &mut TestAppContext) {
1152 let app_state = init(cx);
1153 app_state
1154 .fs
1155 .as_fake()
1156 .insert_tree("/root", json!({ "a.txt": "" }))
1157 .await;
1158
1159 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1160 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1161
1162 // Open a file within an existing worktree.
1163 cx.update(|cx| {
1164 workspace.update(cx, |view, cx| {
1165 view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
1166 })
1167 })
1168 .await;
1169 let editor = cx.read(|cx| {
1170 let pane = workspace.read(cx).active_pane().read(cx);
1171 let item = pane.active_item().unwrap();
1172 item.downcast::<Editor>().unwrap()
1173 });
1174
1175 cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input("x", cx)));
1176 app_state
1177 .fs
1178 .as_fake()
1179 .insert_file("/root/a.txt", "changed".to_string())
1180 .await;
1181 editor
1182 .condition(cx, |editor, cx| editor.has_conflict(cx))
1183 .await;
1184 cx.read(|cx| assert!(editor.is_dirty(cx)));
1185
1186 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1187 cx.simulate_prompt_answer(window_id, 0);
1188 save_task.await.unwrap();
1189 editor.read_with(cx, |editor, cx| {
1190 assert!(!editor.is_dirty(cx));
1191 assert!(!editor.has_conflict(cx));
1192 });
1193 }
1194
1195 #[gpui::test]
1196 async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
1197 let app_state = init(cx);
1198 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1199
1200 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1201 project.update(cx, |project, _| project.languages().add(rust_lang()));
1202 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1203 let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
1204
1205 // Create a new untitled buffer
1206 cx.dispatch_action(window_id, NewFile);
1207 let editor = workspace.read_with(cx, |workspace, cx| {
1208 workspace
1209 .active_item(cx)
1210 .unwrap()
1211 .downcast::<Editor>()
1212 .unwrap()
1213 });
1214
1215 editor.update(cx, |editor, cx| {
1216 assert!(!editor.is_dirty(cx));
1217 assert_eq!(editor.title(cx), "untitled");
1218 assert!(Arc::ptr_eq(
1219 &editor.language_at(0, cx).unwrap(),
1220 &languages::PLAIN_TEXT
1221 ));
1222 editor.handle_input("hi", cx);
1223 assert!(editor.is_dirty(cx));
1224 });
1225
1226 // Save the buffer. This prompts for a filename.
1227 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1228 cx.simulate_new_path_selection(|parent_dir| {
1229 assert_eq!(parent_dir, Path::new("/root"));
1230 Some(parent_dir.join("the-new-name.rs"))
1231 });
1232 cx.read(|cx| {
1233 assert!(editor.is_dirty(cx));
1234 assert_eq!(editor.read(cx).title(cx), "untitled");
1235 });
1236
1237 // When the save completes, the buffer's title is updated and the language is assigned based
1238 // on the path.
1239 save_task.await.unwrap();
1240 editor.read_with(cx, |editor, cx| {
1241 assert!(!editor.is_dirty(cx));
1242 assert_eq!(editor.title(cx), "the-new-name.rs");
1243 assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
1244 });
1245
1246 // Edit the file and save it again. This time, there is no filename prompt.
1247 editor.update(cx, |editor, cx| {
1248 editor.handle_input(" there", cx);
1249 assert!(editor.is_dirty(cx.as_ref()));
1250 });
1251 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1252 save_task.await.unwrap();
1253 assert!(!cx.did_prompt_for_new_path());
1254 editor.read_with(cx, |editor, cx| {
1255 assert!(!editor.is_dirty(cx));
1256 assert_eq!(editor.title(cx), "the-new-name.rs")
1257 });
1258
1259 // Open the same newly-created file in another pane item. The new editor should reuse
1260 // the same buffer.
1261 cx.dispatch_action(window_id, NewFile);
1262 workspace
1263 .update(cx, |workspace, cx| {
1264 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
1265 workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
1266 })
1267 .await
1268 .unwrap();
1269 let editor2 = workspace.update(cx, |workspace, cx| {
1270 workspace
1271 .active_item(cx)
1272 .unwrap()
1273 .downcast::<Editor>()
1274 .unwrap()
1275 });
1276 cx.read(|cx| {
1277 assert_eq!(
1278 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
1279 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
1280 );
1281 })
1282 }
1283
1284 #[gpui::test]
1285 async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
1286 let app_state = init(cx);
1287 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1288
1289 let project = Project::test(app_state.fs.clone(), [], cx).await;
1290 project.update(cx, |project, _| project.languages().add(rust_lang()));
1291 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1292
1293 // Create a new untitled buffer
1294 cx.dispatch_action(window_id, NewFile);
1295 let editor = workspace.read_with(cx, |workspace, cx| {
1296 workspace
1297 .active_item(cx)
1298 .unwrap()
1299 .downcast::<Editor>()
1300 .unwrap()
1301 });
1302
1303 editor.update(cx, |editor, cx| {
1304 assert!(Arc::ptr_eq(
1305 &editor.language_at(0, cx).unwrap(),
1306 &languages::PLAIN_TEXT
1307 ));
1308 editor.handle_input("hi", cx);
1309 assert!(editor.is_dirty(cx.as_ref()));
1310 });
1311
1312 // Save the buffer. This prompts for a filename.
1313 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1314 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
1315 save_task.await.unwrap();
1316 // The buffer is not dirty anymore and the language is assigned based on the path.
1317 editor.read_with(cx, |editor, cx| {
1318 assert!(!editor.is_dirty(cx));
1319 assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
1320 });
1321 }
1322
1323 #[gpui::test]
1324 async fn test_pane_actions(cx: &mut TestAppContext) {
1325 init(cx);
1326
1327 let app_state = cx.update(AppState::test);
1328 app_state
1329 .fs
1330 .as_fake()
1331 .insert_tree(
1332 "/root",
1333 json!({
1334 "a": {
1335 "file1": "contents 1",
1336 "file2": "contents 2",
1337 "file3": "contents 3",
1338 },
1339 }),
1340 )
1341 .await;
1342
1343 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1344 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1345
1346 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1347 let file1 = entries[0].clone();
1348
1349 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1350
1351 workspace
1352 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1353 .await
1354 .unwrap();
1355
1356 let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
1357 let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
1358 assert_eq!(editor.project_path(cx), Some(file1.clone()));
1359 let buffer = editor.update(cx, |editor, cx| {
1360 editor.insert("dirt", cx);
1361 editor.buffer().downgrade()
1362 });
1363 (editor.downgrade(), buffer)
1364 });
1365
1366 cx.dispatch_action(window_id, pane::SplitRight);
1367 let editor_2 = cx.update(|cx| {
1368 let pane_2 = workspace.read(cx).active_pane().clone();
1369 assert_ne!(pane_1, pane_2);
1370
1371 let pane2_item = pane_2.read(cx).active_item().unwrap();
1372 assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
1373
1374 pane2_item.downcast::<Editor>().unwrap().downgrade()
1375 });
1376 cx.dispatch_action(window_id, workspace::CloseActiveItem);
1377
1378 cx.foreground().run_until_parked();
1379 workspace.read_with(cx, |workspace, _| {
1380 assert_eq!(workspace.panes().len(), 2); //Center pane + Dock pane
1381 assert_eq!(workspace.active_pane(), &pane_1);
1382 });
1383
1384 cx.dispatch_action(window_id, workspace::CloseActiveItem);
1385 cx.foreground().run_until_parked();
1386 cx.simulate_prompt_answer(window_id, 1);
1387 cx.foreground().run_until_parked();
1388
1389 workspace.read_with(cx, |workspace, cx| {
1390 assert_eq!(workspace.panes().len(), 2);
1391 assert!(workspace.active_item(cx).is_none());
1392 });
1393
1394 cx.assert_dropped(editor_1);
1395 cx.assert_dropped(editor_2);
1396 cx.assert_dropped(buffer);
1397 }
1398
1399 #[gpui::test]
1400 async fn test_navigation(cx: &mut TestAppContext) {
1401 let app_state = init(cx);
1402 app_state
1403 .fs
1404 .as_fake()
1405 .insert_tree(
1406 "/root",
1407 json!({
1408 "a": {
1409 "file1": "contents 1\n".repeat(20),
1410 "file2": "contents 2\n".repeat(20),
1411 "file3": "contents 3\n".repeat(20),
1412 },
1413 }),
1414 )
1415 .await;
1416
1417 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1418 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1419
1420 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1421 let file1 = entries[0].clone();
1422 let file2 = entries[1].clone();
1423 let file3 = entries[2].clone();
1424
1425 let editor1 = workspace
1426 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1427 .await
1428 .unwrap()
1429 .downcast::<Editor>()
1430 .unwrap();
1431 editor1.update(cx, |editor, cx| {
1432 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1433 s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
1434 });
1435 });
1436 let editor2 = workspace
1437 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1438 .await
1439 .unwrap()
1440 .downcast::<Editor>()
1441 .unwrap();
1442 let editor3 = workspace
1443 .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1444 .await
1445 .unwrap()
1446 .downcast::<Editor>()
1447 .unwrap();
1448
1449 editor3
1450 .update(cx, |editor, cx| {
1451 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1452 s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
1453 });
1454 editor.newline(&Default::default(), cx);
1455 editor.newline(&Default::default(), cx);
1456 editor.move_down(&Default::default(), cx);
1457 editor.move_down(&Default::default(), cx);
1458 editor.save(project.clone(), cx)
1459 })
1460 .await
1461 .unwrap();
1462 editor3.update(cx, |editor, cx| {
1463 editor.set_scroll_position(vec2f(0., 12.5), cx)
1464 });
1465 assert_eq!(
1466 active_location(&workspace, cx),
1467 (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1468 );
1469
1470 workspace
1471 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1472 .await;
1473 assert_eq!(
1474 active_location(&workspace, cx),
1475 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1476 );
1477
1478 workspace
1479 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1480 .await;
1481 assert_eq!(
1482 active_location(&workspace, cx),
1483 (file2.clone(), DisplayPoint::new(0, 0), 0.)
1484 );
1485
1486 workspace
1487 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1488 .await;
1489 assert_eq!(
1490 active_location(&workspace, cx),
1491 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1492 );
1493
1494 workspace
1495 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1496 .await;
1497 assert_eq!(
1498 active_location(&workspace, cx),
1499 (file1.clone(), DisplayPoint::new(0, 0), 0.)
1500 );
1501
1502 // Go back one more time and ensure we don't navigate past the first item in the history.
1503 workspace
1504 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1505 .await;
1506 assert_eq!(
1507 active_location(&workspace, cx),
1508 (file1.clone(), DisplayPoint::new(0, 0), 0.)
1509 );
1510
1511 workspace
1512 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1513 .await;
1514 assert_eq!(
1515 active_location(&workspace, cx),
1516 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1517 );
1518
1519 workspace
1520 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1521 .await;
1522 assert_eq!(
1523 active_location(&workspace, cx),
1524 (file2.clone(), DisplayPoint::new(0, 0), 0.)
1525 );
1526
1527 // Go forward to an item that has been closed, ensuring it gets re-opened at the same
1528 // location.
1529 workspace
1530 .update(cx, |workspace, cx| {
1531 let editor3_id = editor3.id();
1532 drop(editor3);
1533 Pane::close_item(workspace, workspace.active_pane().clone(), editor3_id, cx)
1534 })
1535 .await
1536 .unwrap();
1537 workspace
1538 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1539 .await;
1540 assert_eq!(
1541 active_location(&workspace, cx),
1542 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1543 );
1544
1545 workspace
1546 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1547 .await;
1548 assert_eq!(
1549 active_location(&workspace, cx),
1550 (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1551 );
1552
1553 workspace
1554 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1555 .await;
1556 assert_eq!(
1557 active_location(&workspace, cx),
1558 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1559 );
1560
1561 // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
1562 workspace
1563 .update(cx, |workspace, cx| {
1564 let editor2_id = editor2.id();
1565 drop(editor2);
1566 Pane::close_item(workspace, workspace.active_pane().clone(), editor2_id, cx)
1567 })
1568 .await
1569 .unwrap();
1570 app_state
1571 .fs
1572 .remove_file(Path::new("/root/a/file2"), Default::default())
1573 .await
1574 .unwrap();
1575 workspace
1576 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1577 .await;
1578 assert_eq!(
1579 active_location(&workspace, cx),
1580 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1581 );
1582 workspace
1583 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1584 .await;
1585 assert_eq!(
1586 active_location(&workspace, cx),
1587 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1588 );
1589
1590 // Modify file to collapse multiple nav history entries into the same location.
1591 // Ensure we don't visit the same location twice when navigating.
1592 editor1.update(cx, |editor, cx| {
1593 editor.change_selections(None, cx, |s| {
1594 s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
1595 })
1596 });
1597
1598 for _ in 0..5 {
1599 editor1.update(cx, |editor, cx| {
1600 editor.change_selections(None, cx, |s| {
1601 s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
1602 });
1603 });
1604 editor1.update(cx, |editor, cx| {
1605 editor.change_selections(None, cx, |s| {
1606 s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
1607 })
1608 });
1609 }
1610
1611 editor1.update(cx, |editor, cx| {
1612 editor.transact(cx, |editor, cx| {
1613 editor.change_selections(None, cx, |s| {
1614 s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
1615 });
1616 editor.insert("", cx);
1617 })
1618 });
1619
1620 editor1.update(cx, |editor, cx| {
1621 editor.change_selections(None, cx, |s| {
1622 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1623 })
1624 });
1625 workspace
1626 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1627 .await;
1628 assert_eq!(
1629 active_location(&workspace, cx),
1630 (file1.clone(), DisplayPoint::new(2, 0), 0.)
1631 );
1632 workspace
1633 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1634 .await;
1635 assert_eq!(
1636 active_location(&workspace, cx),
1637 (file1.clone(), DisplayPoint::new(3, 0), 0.)
1638 );
1639
1640 fn active_location(
1641 workspace: &ViewHandle<Workspace>,
1642 cx: &mut TestAppContext,
1643 ) -> (ProjectPath, DisplayPoint, f32) {
1644 workspace.update(cx, |workspace, cx| {
1645 let item = workspace.active_item(cx).unwrap();
1646 let editor = item.downcast::<Editor>().unwrap();
1647 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
1648 (
1649 editor.selections.display_ranges(cx),
1650 editor.scroll_position(cx),
1651 )
1652 });
1653 (
1654 item.project_path(cx).unwrap(),
1655 selections[0].start,
1656 scroll_position.y(),
1657 )
1658 })
1659 }
1660 }
1661
1662 #[gpui::test]
1663 async fn test_reopening_closed_items(cx: &mut TestAppContext) {
1664 let app_state = init(cx);
1665 app_state
1666 .fs
1667 .as_fake()
1668 .insert_tree(
1669 "/root",
1670 json!({
1671 "a": {
1672 "file1": "",
1673 "file2": "",
1674 "file3": "",
1675 "file4": "",
1676 },
1677 }),
1678 )
1679 .await;
1680
1681 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1682 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1683 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1684
1685 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1686 let file1 = entries[0].clone();
1687 let file2 = entries[1].clone();
1688 let file3 = entries[2].clone();
1689 let file4 = entries[3].clone();
1690
1691 let file1_item_id = workspace
1692 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1693 .await
1694 .unwrap()
1695 .id();
1696 let file2_item_id = workspace
1697 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1698 .await
1699 .unwrap()
1700 .id();
1701 let file3_item_id = workspace
1702 .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1703 .await
1704 .unwrap()
1705 .id();
1706 let file4_item_id = workspace
1707 .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
1708 .await
1709 .unwrap()
1710 .id();
1711 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1712
1713 // Close all the pane items in some arbitrary order.
1714 workspace
1715 .update(cx, |workspace, cx| {
1716 Pane::close_item(workspace, pane.clone(), file1_item_id, cx)
1717 })
1718 .await
1719 .unwrap();
1720 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1721
1722 workspace
1723 .update(cx, |workspace, cx| {
1724 Pane::close_item(workspace, pane.clone(), file4_item_id, cx)
1725 })
1726 .await
1727 .unwrap();
1728 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1729
1730 workspace
1731 .update(cx, |workspace, cx| {
1732 Pane::close_item(workspace, pane.clone(), file2_item_id, cx)
1733 })
1734 .await
1735 .unwrap();
1736 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1737
1738 workspace
1739 .update(cx, |workspace, cx| {
1740 Pane::close_item(workspace, pane.clone(), file3_item_id, cx)
1741 })
1742 .await
1743 .unwrap();
1744 assert_eq!(active_path(&workspace, cx), None);
1745
1746 // Reopen all the closed items, ensuring they are reopened in the same order
1747 // in which they were closed.
1748 workspace.update(cx, Pane::reopen_closed_item).await;
1749 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1750
1751 workspace.update(cx, Pane::reopen_closed_item).await;
1752 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1753
1754 workspace.update(cx, Pane::reopen_closed_item).await;
1755 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1756
1757 workspace.update(cx, Pane::reopen_closed_item).await;
1758 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1759
1760 // Reopening past the last closed item is a no-op.
1761 workspace.update(cx, Pane::reopen_closed_item).await;
1762 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1763
1764 // Reopening closed items doesn't interfere with navigation history.
1765 workspace
1766 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1767 .await;
1768 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1769
1770 workspace
1771 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1772 .await;
1773 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1774
1775 workspace
1776 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1777 .await;
1778 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1779
1780 workspace
1781 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1782 .await;
1783 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1784
1785 workspace
1786 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1787 .await;
1788 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1789
1790 workspace
1791 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1792 .await;
1793 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1794
1795 workspace
1796 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1797 .await;
1798 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1799
1800 workspace
1801 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1802 .await;
1803 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1804
1805 fn active_path(
1806 workspace: &ViewHandle<Workspace>,
1807 cx: &TestAppContext,
1808 ) -> Option<ProjectPath> {
1809 workspace.read_with(cx, |workspace, cx| {
1810 let item = workspace.active_item(cx)?;
1811 item.project_path(cx)
1812 })
1813 }
1814 }
1815
1816 #[gpui::test]
1817 fn test_bundled_settings_and_themes(cx: &mut MutableAppContext) {
1818 cx.platform()
1819 .fonts()
1820 .add_fonts(&[
1821 Assets
1822 .load("fonts/zed-sans/zed-sans-extended.ttf")
1823 .unwrap()
1824 .to_vec()
1825 .into(),
1826 Assets
1827 .load("fonts/zed-mono/zed-mono-extended.ttf")
1828 .unwrap()
1829 .to_vec()
1830 .into(),
1831 ])
1832 .unwrap();
1833 let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
1834 let settings = Settings::defaults(Assets, cx.font_cache(), &themes);
1835
1836 let mut has_default_theme = false;
1837 for theme_name in themes.list(false).map(|meta| meta.name) {
1838 let theme = themes.get(&theme_name).unwrap();
1839 if theme.meta.name == settings.theme.meta.name {
1840 has_default_theme = true;
1841 }
1842 assert_eq!(theme.meta.name, theme_name);
1843 }
1844 assert!(has_default_theme);
1845 }
1846
1847 #[gpui::test]
1848 fn test_bundled_languages(cx: &mut MutableAppContext) {
1849 let mut languages = LanguageRegistry::new(Task::ready(()));
1850 languages.set_executor(cx.background().clone());
1851 let languages = Arc::new(languages);
1852 let themes = ThemeRegistry::new((), cx.font_cache().clone());
1853 languages::init(languages.clone(), themes);
1854 for name in languages.language_names() {
1855 languages.language_for_name(&name);
1856 }
1857 cx.foreground().run_until_parked();
1858 }
1859
1860 fn init(cx: &mut TestAppContext) -> Arc<AppState> {
1861 cx.foreground().forbid_parking();
1862 cx.update(|cx| {
1863 let mut app_state = AppState::test(cx);
1864 let state = Arc::get_mut(&mut app_state).unwrap();
1865 state.initialize_workspace = initialize_workspace;
1866 state.build_window_options = build_window_options;
1867 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
1868 workspace::init(app_state.clone(), cx);
1869 editor::init(cx);
1870 pane::init(cx);
1871 app_state
1872 })
1873 }
1874
1875 fn rust_lang() -> Arc<language::Language> {
1876 Arc::new(language::Language::new(
1877 language::LanguageConfig {
1878 name: "Rust".into(),
1879 path_suffixes: vec!["rs".to_string()],
1880 ..Default::default()
1881 },
1882 Some(tree_sitter_rust::language()),
1883 ))
1884 }
1885}