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