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