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