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