1mod app_menus;
2pub mod component_preview;
3pub mod edit_prediction_registry;
4#[cfg(target_os = "macos")]
5pub(crate) mod mac_only_instance;
6mod migrate;
7mod open_listener;
8mod quick_action_bar;
9#[cfg(target_os = "windows")]
10pub(crate) mod windows_only_instance;
11
12use agent_ui::{AgentDiffToolbar, AgentPanelDelegate};
13use anyhow::Context as _;
14pub use app_menus::*;
15use assets::Assets;
16use audio::{AudioSettings, REPLAY_DURATION};
17use breadcrumbs::Breadcrumbs;
18use client::zed_urls;
19use collections::VecDeque;
20use debugger_ui::debugger_panel::DebugPanel;
21use editor::ProposedChangesEditorToolbar;
22use editor::{Editor, MultiBuffer};
23use extension_host::ExtensionStore;
24use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag};
25use fs::Fs;
26use futures::future::Either;
27use futures::{StreamExt, channel::mpsc, select_biased};
28use git_ui::commit_view::CommitViewToolbar;
29use git_ui::git_panel::GitPanel;
30use git_ui::project_diff::ProjectDiffToolbar;
31use gpui::{
32 Action, App, AppContext as _, AsyncApp, Context, DismissEvent, Element, Entity, EventEmitter,
33 FRAME_RING, FocusHandle, Focusable, FrameTimings, Hsla, KeyBinding, MouseButton, ParentElement,
34 PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Styled, Task, TitlebarOptions,
35 UpdateGlobal, Window, WindowKind, WindowOptions, actions, get_all_timings, get_frame_timings,
36 hsla, image_cache, point, px, retain_all, rgb, rgba,
37};
38use image_viewer::ImageInfo;
39use language::Capability;
40use language_onboarding::BasedPyrightBanner;
41use language_tools::lsp_button::{self, LspButton};
42use language_tools::lsp_log_view::LspLogToolbarItemView;
43use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
44use migrator::{migrate_keymap, migrate_settings};
45use onboarding::DOCS_URL;
46use onboarding::multibuffer_hint::MultibufferHint;
47pub use open_listener::*;
48use outline_panel::OutlinePanel;
49use paths::{
50 local_debug_file_relative_path, local_settings_file_relative_path,
51 local_tasks_file_relative_path,
52};
53use project::{DirectoryLister, DisableAiSettings, ProjectItem};
54use project_panel::ProjectPanel;
55use prompt_store::PromptBuilder;
56use quick_action_bar::QuickActionBar;
57use recent_projects::open_remote_project;
58use release_channel::{AppCommitSha, ReleaseChannel};
59use rope::Rope;
60use search::project_search::ProjectSearchBar;
61use settings::{
62 BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile,
63 KeymapFileLoadResult, Settings, SettingsStore, VIM_KEYMAP_PATH,
64 initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content,
65 update_settings_file,
66};
67use std::time::Duration;
68use std::{
69 borrow::Cow,
70 path::{Path, PathBuf},
71 sync::Arc,
72 sync::atomic::{self, AtomicBool},
73};
74use terminal_view::terminal_panel::{self, TerminalPanel};
75use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
76use ui::{PopoverMenuHandle, Tooltip, prelude::*};
77use util::markdown::MarkdownString;
78use util::rel_path::RelPath;
79use util::{ResultExt, asset_str};
80use uuid::Uuid;
81use vim_mode_setting::VimModeSetting;
82use workspace::dock::PanelEvent;
83use workspace::notifications::{
84 NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification,
85};
86use workspace::{
87 AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
88 create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
89 open_new,
90};
91use workspace::{
92 CloseIntent, CloseWindow, NotificationFrame, Panel, RestoreBanner, with_active_or_new_workspace,
93};
94use workspace::{Pane, notifications::DetachAndPromptErr};
95use zed_actions::{
96 OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettingsFile, OpenZedUrl,
97 Quit,
98};
99
100actions!(
101 zed,
102 [
103 /// Opens the element inspector for debugging UI.
104 DebugElements,
105 /// Hides the application window.
106 Hide,
107 /// Hides all other application windows.
108 HideOthers,
109 /// Minimizes the current window.
110 Minimize,
111 /// Opens the default settings file.
112 OpenDefaultSettings,
113 /// Opens project-specific settings.
114 OpenProjectSettings,
115 /// Opens the project tasks configuration.
116 OpenProjectTasks,
117 /// Opens the tasks panel.
118 OpenTasks,
119 /// Opens debug tasks configuration.
120 OpenDebugTasks,
121 /// Resets the application database.
122 ResetDatabase,
123 /// Shows all hidden windows.
124 ShowAll,
125 /// Toggles fullscreen mode.
126 ToggleFullScreen,
127 /// Zooms the window.
128 Zoom,
129 /// Triggers a test panic for debugging.
130 TestPanic,
131 /// Triggers a hard crash for debugging.
132 TestCrash,
133 ]
134);
135
136actions!(
137 dev,
138 [
139 /// Stores last 30s of audio from zed staff using the experimental rodio
140 /// audio system (including yourself) on the current call in a tar file
141 /// in the current working directory.
142 CaptureRecentAudio,
143 ]
144);
145
146pub fn init(cx: &mut App) {
147 #[cfg(target_os = "macos")]
148 cx.on_action(|_: &Hide, cx| cx.hide());
149 #[cfg(target_os = "macos")]
150 cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps());
151 #[cfg(target_os = "macos")]
152 cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps());
153 cx.on_action(quit);
154
155 cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx));
156 let flag = cx.wait_for_flag::<PanicFeatureFlag>();
157 cx.spawn(async |cx| {
158 if cx
159 .update(|cx| ReleaseChannel::global(cx) == ReleaseChannel::Dev)
160 .unwrap_or_default()
161 || flag.await
162 {
163 cx.update(|cx| {
164 cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action"));
165 cx.on_action(|_: &TestCrash, _| {
166 unsafe extern "C" {
167 fn puts(s: *const i8);
168 }
169 unsafe {
170 puts(0xabad1d3a as *const i8);
171 }
172 });
173 })
174 .ok();
175 };
176 })
177 .detach();
178 cx.on_action(|_: &OpenLog, cx| {
179 with_active_or_new_workspace(cx, |workspace, window, cx| {
180 open_log_file(workspace, window, cx);
181 });
182 });
183 cx.on_action(|_: &workspace::RevealLogInFileManager, cx| {
184 cx.reveal_path(paths::log_file().as_path());
185 });
186 cx.on_action(|_: &zed_actions::OpenLicenses, cx| {
187 with_active_or_new_workspace(cx, |workspace, window, cx| {
188 open_bundled_file(
189 workspace,
190 asset_str::<Assets>("licenses.md"),
191 "Open Source License Attribution",
192 "Markdown",
193 window,
194 cx,
195 );
196 });
197 });
198 cx.on_action(|_: &zed_actions::OpenTelemetryLog, cx| {
199 with_active_or_new_workspace(cx, |workspace, window, cx| {
200 open_telemetry_log_file(workspace, window, cx);
201 });
202 });
203 cx.on_action(|&zed_actions::OpenKeymapFile, cx| {
204 with_active_or_new_workspace(cx, |_, window, cx| {
205 open_settings_file(
206 paths::keymap_file(),
207 || settings::initial_keymap_content().as_ref().into(),
208 window,
209 cx,
210 );
211 });
212 });
213 cx.on_action(|_: &OpenSettingsFile, cx| {
214 with_active_or_new_workspace(cx, |_, window, cx| {
215 open_settings_file(
216 paths::settings_file(),
217 || settings::initial_user_settings_content().as_ref().into(),
218 window,
219 cx,
220 );
221 });
222 });
223 cx.on_action(|_: &OpenAccountSettings, cx| {
224 with_active_or_new_workspace(cx, |_, _, cx| {
225 cx.open_url(&zed_urls::account_url(cx));
226 });
227 });
228 cx.on_action(|_: &OpenTasks, cx| {
229 with_active_or_new_workspace(cx, |_, window, cx| {
230 open_settings_file(
231 paths::tasks_file(),
232 || settings::initial_tasks_content().as_ref().into(),
233 window,
234 cx,
235 );
236 });
237 });
238 cx.on_action(|_: &OpenDebugTasks, cx| {
239 with_active_or_new_workspace(cx, |_, window, cx| {
240 open_settings_file(
241 paths::debug_scenarios_file(),
242 || settings::initial_debug_tasks_content().as_ref().into(),
243 window,
244 cx,
245 );
246 });
247 });
248 cx.on_action(|_: &OpenDefaultSettings, cx| {
249 with_active_or_new_workspace(cx, |workspace, window, cx| {
250 open_bundled_file(
251 workspace,
252 settings::default_settings(),
253 "Default Settings",
254 "JSON",
255 window,
256 cx,
257 );
258 });
259 });
260 cx.on_action(|_: &zed_actions::OpenDefaultKeymap, cx| {
261 with_active_or_new_workspace(cx, |workspace, window, cx| {
262 open_bundled_file(
263 workspace,
264 settings::default_keymap(),
265 "Default Key Bindings",
266 "JSON",
267 window,
268 cx,
269 );
270 });
271 });
272}
273
274fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {
275 WorkspaceSettings::get_global(cx)
276 .on_last_window_closed
277 .is_quit_app()
278 .then(|| {
279 cx.on_window_closed(|cx| {
280 if cx.windows().is_empty() {
281 cx.quit();
282 }
283 })
284 })
285}
286
287pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowOptions {
288 let display = display_uuid.and_then(|uuid| {
289 cx.displays()
290 .into_iter()
291 .find(|display| display.uuid().ok() == Some(uuid))
292 });
293 let app_id = ReleaseChannel::global(cx).app_id();
294 let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
295 Ok(val) if val == "server" => gpui::WindowDecorations::Server,
296 Ok(val) if val == "client" => gpui::WindowDecorations::Client,
297 _ => gpui::WindowDecorations::Client,
298 };
299
300 let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs;
301
302 WindowOptions {
303 titlebar: Some(TitlebarOptions {
304 title: None,
305 appears_transparent: true,
306 traffic_light_position: Some(point(px(9.0), px(9.0))),
307 }),
308 window_bounds: None,
309 focus: false,
310 show: false,
311 kind: WindowKind::Normal,
312 is_movable: true,
313 display_id: display.map(|display| display.id()),
314 window_background: cx.theme().window_background_appearance(),
315 app_id: Some(app_id.to_owned()),
316 window_decorations: Some(window_decorations),
317 window_min_size: Some(gpui::Size {
318 width: px(360.0),
319 height: px(240.0),
320 }),
321 tabbing_identifier: if use_system_window_tabs {
322 Some(String::from("zed"))
323 } else {
324 None
325 },
326 ..Default::default()
327 }
328}
329
330pub fn initialize_workspace(
331 app_state: Arc<AppState>,
332 prompt_builder: Arc<PromptBuilder>,
333 cx: &mut App,
334) {
335 let mut _on_close_subscription = bind_on_window_closed(cx);
336 cx.observe_global::<SettingsStore>(move |cx| {
337 _on_close_subscription = bind_on_window_closed(cx);
338 })
339 .detach();
340
341 cx.observe_new(move |workspace: &mut Workspace, window, cx| {
342 let Some(window) = window else {
343 return;
344 };
345
346 let workspace_handle = cx.entity();
347 let center_pane = workspace.active_pane().clone();
348 initialize_pane(workspace, ¢er_pane, window, cx);
349
350 cx.subscribe_in(&workspace_handle, window, {
351 move |workspace, _, event, window, cx| match event {
352 workspace::Event::PaneAdded(pane) => {
353 initialize_pane(workspace, pane, window, cx);
354 }
355 workspace::Event::OpenBundledFile {
356 text,
357 title,
358 language,
359 } => open_bundled_file(workspace, text.clone(), title, language, window, cx),
360 _ => {}
361 }
362 })
363 .detach();
364
365 #[cfg(not(target_os = "macos"))]
366 initialize_file_watcher(window, cx);
367
368 if let Some(specs) = window.gpu_specs() {
369 log::info!("Using GPU: {:?}", specs);
370 show_software_emulation_warning_if_needed(specs.clone(), window, cx);
371 if let Some((crash_server, message)) = crashes::CRASH_HANDLER
372 .get()
373 .zip(bincode::serialize(&specs).ok())
374 && let Err(err) = crash_server.send_message(3, message)
375 {
376 log::warn!(
377 "Failed to store active gpu info for crash reporting: {}",
378 err
379 );
380 }
381 }
382
383 let edit_prediction_menu_handle = PopoverMenuHandle::default();
384 let edit_prediction_button = cx.new(|cx| {
385 edit_prediction_button::EditPredictionButton::new(
386 app_state.fs.clone(),
387 app_state.user_store.clone(),
388 edit_prediction_menu_handle.clone(),
389 cx,
390 )
391 });
392 workspace.register_action({
393 move |_, _: &edit_prediction_button::ToggleMenu, window, cx| {
394 edit_prediction_menu_handle.toggle(window, cx);
395 }
396 });
397
398 let search_button = cx.new(|_| search::search_status_button::SearchButton::new());
399 let diagnostic_summary =
400 cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
401 let activity_indicator = activity_indicator::ActivityIndicator::new(
402 workspace,
403 workspace.project().read(cx).languages().clone(),
404 window,
405 cx,
406 );
407 let active_buffer_language =
408 cx.new(|_| language_selector::ActiveBufferLanguage::new(workspace));
409 let active_toolchain_language =
410 cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx));
411 let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
412 let image_info = cx.new(|_cx| ImageInfo::new(workspace));
413
414 let lsp_button_menu_handle = PopoverMenuHandle::default();
415 let lsp_button =
416 cx.new(|cx| LspButton::new(workspace, lsp_button_menu_handle.clone(), window, cx));
417 workspace.register_action({
418 move |_, _: &lsp_button::ToggleMenu, window, cx| {
419 lsp_button_menu_handle.toggle(window, cx);
420 }
421 });
422
423 let cursor_position =
424 cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
425 let line_ending_indicator =
426 cx.new(|_| line_ending_selector::LineEndingIndicator::default());
427 workspace.status_bar().update(cx, |status_bar, cx| {
428 status_bar.add_left_item(search_button, window, cx);
429 status_bar.add_left_item(lsp_button, window, cx);
430 status_bar.add_left_item(diagnostic_summary, window, cx);
431 status_bar.add_left_item(activity_indicator, window, cx);
432 status_bar.add_right_item(edit_prediction_button, window, cx);
433 status_bar.add_right_item(active_buffer_language, window, cx);
434 status_bar.add_right_item(active_toolchain_language, window, cx);
435 status_bar.add_right_item(line_ending_indicator, window, cx);
436 status_bar.add_right_item(vim_mode_indicator, window, cx);
437 status_bar.add_right_item(cursor_position, window, cx);
438 status_bar.add_right_item(image_info, window, cx);
439 });
440
441 let handle = cx.entity().downgrade();
442 window.on_window_should_close(cx, move |window, cx| {
443 handle
444 .update(cx, |workspace, cx| {
445 // We'll handle closing asynchronously
446 workspace.close_window(&CloseWindow, window, cx);
447 false
448 })
449 .unwrap_or(true)
450 });
451
452 initialize_panels(prompt_builder.clone(), window, cx);
453 register_actions(app_state.clone(), workspace, window, cx);
454
455 workspace.focus_handle(cx).focus(window);
456 })
457 .detach();
458}
459
460#[cfg(any(target_os = "linux", target_os = "freebsd"))]
461fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
462 if let Err(e) = fs::fs_watcher::global(|_| {}) {
463 let message = format!(
464 db::indoc! {r#"
465 inotify_init returned {}
466
467 This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux
468 "#},
469 e
470 );
471 let prompt = window.prompt(
472 PromptLevel::Critical,
473 "Could not start inotify",
474 Some(&message),
475 &["Troubleshoot and Quit"],
476 cx,
477 );
478 cx.spawn(async move |_, cx| {
479 if prompt.await == Ok(0) {
480 cx.update(|cx| {
481 cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify");
482 cx.quit();
483 })
484 .ok();
485 }
486 })
487 .detach()
488 }
489}
490
491#[cfg(target_os = "windows")]
492fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
493 if let Err(e) = fs::fs_watcher::global(|_| {}) {
494 let message = format!(
495 db::indoc! {r#"
496 ReadDirectoryChangesW initialization failed: {}
497
498 This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows
499 "#},
500 e
501 );
502 let prompt = window.prompt(
503 PromptLevel::Critical,
504 "Could not start ReadDirectoryChangesW",
505 Some(&message),
506 &["Troubleshoot and Quit"],
507 cx,
508 );
509 cx.spawn(async move |_, cx| {
510 if prompt.await == Ok(0) {
511 cx.update(|cx| {
512 cx.open_url("https://zed.dev/docs/windows");
513 cx.quit()
514 })
515 .ok();
516 }
517 })
518 .detach()
519 }
520}
521
522fn show_software_emulation_warning_if_needed(
523 specs: gpui::GpuSpecs,
524 window: &mut Window,
525 cx: &mut Context<Workspace>,
526) {
527 if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() {
528 let (graphics_api, docs_url, open_url) = if cfg!(target_os = "windows") {
529 (
530 "DirectX",
531 "https://zed.dev/docs/windows",
532 "https://zed.dev/docs/windows",
533 )
534 } else {
535 (
536 "Vulkan",
537 "https://zed.dev/docs/linux",
538 "https://zed.dev/docs/linux#zed-fails-to-open-windows",
539 )
540 };
541 let message = format!(
542 db::indoc! {r#"
543 Zed uses {} for rendering and requires a compatible GPU.
544
545 Currently you are using a software emulated GPU ({}) which
546 will result in awful performance.
547
548 For troubleshooting see: {}
549 Set ZED_ALLOW_EMULATED_GPU=1 env var to permanently override.
550 "#},
551 graphics_api, specs.device_name, docs_url
552 );
553 let prompt = window.prompt(
554 PromptLevel::Critical,
555 "Unsupported GPU",
556 Some(&message),
557 &["Skip", "Troubleshoot and Quit"],
558 cx,
559 );
560 cx.spawn(async move |_, cx| {
561 if prompt.await == Ok(1) {
562 cx.update(|cx| {
563 cx.open_url(open_url);
564 cx.quit();
565 })
566 .ok();
567 }
568 })
569 .detach()
570 }
571}
572
573actions!(timings, [ToggleFocus,]);
574
575enum DataMode {
576 Realtime(Option<FrameTimings>),
577 Capture {
578 selected_index: usize,
579 data: Vec<FrameTimings>,
580 },
581}
582
583struct TimingsPanel {
584 position: workspace::dock::DockPosition,
585 focus_handle: FocusHandle,
586 data: DataMode,
587 width: Option<Pixels>,
588 _refresh: Option<Task<()>>,
589}
590
591impl TimingsPanel {
592 fn new(cx: &mut App) -> Entity<Self> {
593 let entity = cx.new(|cx| Self {
594 position: workspace::dock::DockPosition::Right,
595 focus_handle: cx.focus_handle(),
596 data: DataMode::Realtime(None),
597 width: None,
598 _refresh: Some(Self::begin_listen(cx)),
599 });
600
601 entity
602 }
603
604 fn begin_listen(cx: &mut Context<Self>) -> Task<()> {
605 cx.spawn(async move |this, cx| {
606 loop {
607 let data = get_frame_timings();
608
609 this.update(cx, |this: &mut TimingsPanel, cx| {
610 this.data = DataMode::Realtime(Some(data));
611 cx.notify();
612 });
613
614 cx.background_executor()
615 .timer(Duration::from_micros(1))
616 .await;
617 }
618 })
619 }
620
621 fn get_timings(&self) -> Option<&FrameTimings> {
622 match &self.data {
623 DataMode::Realtime(data) => data.as_ref(),
624 DataMode::Capture {
625 data,
626 selected_index,
627 } => Some(&data[*selected_index]),
628 }
629 }
630
631 fn render_bar(&self, max_value: f32, item: BarChartItem, cx: &App) -> impl IntoElement {
632 let fill_width = (item.value / max_value).max(0.02); // Minimum 2% width for visibility
633 let label = format!(
634 "{}:{}:{}",
635 item.location
636 .file()
637 .rsplit_once("/")
638 .unwrap_or(("", item.location.file()))
639 .1
640 .rsplit_once("\\")
641 .unwrap_or(("", item.location.file()))
642 .1,
643 item.location.line(),
644 item.location.column()
645 );
646
647 h_flex()
648 .gap_2()
649 .w_full()
650 .h(px(32.0)) // Slightly taller for better visibility
651 .child(
652 // Label with flexible width and truncation
653 div()
654 .min_w(px(80.0))
655 .max_w(px(200.0)) // Maximum width for labels
656 .flex_shrink_0()
657 .overflow_hidden()
658 .child(div().text_ellipsis().child(label)),
659 )
660 .child(
661 // Bar container
662 div()
663 .flex_1()
664 .h(px(24.0))
665 .bg(cx.theme().colors().background)
666 .rounded_md()
667 .p(px(2.0))
668 .child(
669 // Bar fill with minimum width
670 div()
671 .h_full()
672 .rounded_sm()
673 .bg(item.color)
674 .min_w(px(4.0)) // Minimum width so tiny values are still visible
675 .w(relative(fill_width)),
676 ),
677 )
678 .child(
679 // Value label - right-aligned
680 div()
681 .min_w(px(60.0))
682 .flex_shrink_0()
683 .text_right()
684 .child(format!("{:.1} {}", fill_width, item.value)),
685 )
686 }
687}
688
689#[derive(IntoElement)]
690struct DiscreteSlider {
691 value: usize,
692 count: usize,
693 on_change: Arc<dyn Fn(usize, &mut Window, &mut App)>,
694}
695
696impl DiscreteSlider {
697 pub fn new(
698 value: usize,
699 count: usize,
700 on_change: impl Fn(usize, &mut Window, &mut App) + 'static,
701 ) -> Self {
702 Self {
703 value: value.min(count - 1),
704 count,
705 on_change: Arc::new(on_change),
706 }
707 }
708}
709
710impl RenderOnce for DiscreteSlider {
711 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
712 h_flex()
713 .id("discrete-slider")
714 .overflow_scroll()
715 .w_full()
716 .p_2()
717 .children((0..self.count).map(|i| {
718 let is_active = i == self.value;
719 let on_change = self.on_change.clone();
720
721 div()
722 .w(px(24.0))
723 .h(px(24.0))
724 .rounded_md()
725 .cursor_pointer()
726 .bg(if is_active {
727 cx.theme().accents().color_for_index(i as u32)
728 } else {
729 cx.theme().colors().element_background
730 })
731 .hover(|this| this.bg(cx.theme().colors().element_hover))
732 .on_mouse_down(MouseButton::Left, {
733 let on_change = Arc::clone(&on_change);
734 move |_, window, cx| {
735 on_change(i, window, cx);
736 }
737 })
738 // This handles drag selection
739 .on_mouse_move({
740 move |event, window, cx| {
741 // If mouse button is held down
742 if event.pressed_button == Some(MouseButton::Left) {
743 on_change(i, window, cx);
744 }
745 }
746 })
747 }))
748 }
749}
750
751struct BarChartItem {
752 location: &'static core::panic::Location<'static>,
753 value: f32,
754 color: Hsla,
755}
756
757impl Panel for TimingsPanel {
758 fn persistent_name() -> &'static str {
759 "Timings"
760 }
761
762 fn panel_key() -> &'static str {
763 "timings-panel"
764 }
765
766 fn position(&self, window: &Window, cx: &App) -> workspace::dock::DockPosition {
767 self.position
768 }
769
770 fn position_is_valid(&self, position: workspace::dock::DockPosition) -> bool {
771 true
772 }
773
774 fn set_position(
775 &mut self,
776 position: workspace::dock::DockPosition,
777 window: &mut Window,
778 cx: &mut Context<Self>,
779 ) {
780 self.position = position;
781 cx.notify();
782 }
783
784 fn size(&self, window: &Window, cx: &App) -> Pixels {
785 self.width.unwrap_or(px(200.0))
786 }
787
788 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
789 self.width = size;
790 cx.notify();
791 }
792
793 fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName> {
794 Some(ui::IconName::Envelope)
795 }
796
797 fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str> {
798 Some("Timings Panel")
799 }
800
801 fn toggle_action(&self) -> Box<dyn Action> {
802 Box::new(ToggleFocus)
803 }
804
805 fn activation_priority(&self) -> u32 {
806 2
807 }
808}
809
810impl Render for TimingsPanel {
811 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
812 v_flex()
813 .id("timings")
814 .w_full()
815 .h_full()
816 .gap_2()
817 .overflow_scroll()
818 .child(
819 Button::new(
820 "switch-mode",
821 match self.data {
822 DataMode::Capture { .. } => "Realtime",
823 DataMode::Realtime(_) => "Capture",
824 },
825 )
826 .style(ButtonStyle::Filled)
827 .on_click(cx.listener(|this, _, window, cx| {
828 match this.data {
829 DataMode::Realtime(_) => {
830 let (data, selected_index) = get_all_timings();
831 this._refresh = None;
832 this.data = DataMode::Capture {
833 selected_index,
834 data,
835 };
836 }
837 DataMode::Capture { .. } => {
838 this._refresh = Some(Self::begin_listen(cx));
839 this.data = DataMode::Realtime(None);
840 }
841 };
842 cx.notify();
843 })),
844 )
845 .when(matches!(self.data, DataMode::Capture { .. }), |this| {
846 let DataMode::Capture {
847 selected_index,
848 data,
849 } = &self.data
850 else {
851 unreachable!();
852 };
853
854 let entity = cx.entity().downgrade();
855 this.child(DiscreteSlider::new(
856 *selected_index,
857 data.len(),
858 move |value, _window, cx| {
859 if let Some(entity) = entity.upgrade() {
860 entity.update(cx, |this, cx| {
861 if let DataMode::Capture { selected_index, .. } = &mut this.data {
862 *selected_index = value;
863 cx.notify();
864 }
865 });
866 }
867 },
868 ))
869 })
870 .when_some(self.get_timings(), |div, e| {
871 div.child(format!("{}", e.frame_time))
872 .children(e.timings.iter().enumerate().map(|(i, (name, value))| {
873 self.render_bar(
874 e.frame_time as f32,
875 BarChartItem {
876 location: name,
877 value: *value as f32,
878 color: cx.theme().accents().color_for_index(i as u32),
879 },
880 cx,
881 )
882 }))
883 })
884 }
885}
886
887impl Focusable for TimingsPanel {
888 fn focus_handle(&self, cx: &App) -> FocusHandle {
889 self.focus_handle.clone()
890 }
891}
892
893impl EventEmitter<workspace::Event> for TimingsPanel {}
894
895impl EventEmitter<PanelEvent> for TimingsPanel {}
896
897fn initialize_panels(
898 prompt_builder: Arc<PromptBuilder>,
899 window: &mut Window,
900 cx: &mut Context<Workspace>,
901) {
902 cx.spawn_in(window, async move |workspace_handle, cx| {
903 let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
904 let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
905 let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
906 let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone());
907 let channels_panel =
908 collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
909 let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
910 workspace_handle.clone(),
911 cx.clone(),
912 );
913 let debug_panel = DebugPanel::load(workspace_handle.clone(), cx);
914 let timings_panel = workspace_handle
915 .update_in(cx, |workspace, window, cx| TimingsPanel::new(cx))
916 .unwrap();
917
918 workspace_handle.update_in(cx, |workspace, window, cx| {
919 workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
920 workspace.toggle_panel_focus::<TimingsPanel>(window, cx);
921 });
922 });
923
924 let (
925 project_panel,
926 outline_panel,
927 terminal_panel,
928 git_panel,
929 channels_panel,
930 notification_panel,
931 debug_panel,
932 ) = futures::try_join!(
933 project_panel,
934 outline_panel,
935 git_panel,
936 terminal_panel,
937 channels_panel,
938 notification_panel,
939 debug_panel,
940 )?;
941
942 workspace_handle.update_in(cx, |workspace, window, cx| {
943 workspace.add_panel(project_panel, window, cx);
944 workspace.add_panel(outline_panel, window, cx);
945 workspace.add_panel(terminal_panel, window, cx);
946 workspace.add_panel(git_panel, window, cx);
947 workspace.add_panel(channels_panel, window, cx);
948 workspace.add_panel(notification_panel, window, cx);
949 workspace.add_panel(debug_panel, window, cx);
950 workspace.add_panel(timings_panel, window, cx);
951 })?;
952
953 fn setup_or_teardown_agent_panel(
954 workspace: &mut Workspace,
955 prompt_builder: Arc<PromptBuilder>,
956 window: &mut Window,
957 cx: &mut Context<Workspace>,
958 ) -> Task<anyhow::Result<()>> {
959 let disable_ai = SettingsStore::global(cx)
960 .get::<DisableAiSettings>(None)
961 .disable_ai
962 || cfg!(test);
963 let existing_panel = workspace.panel::<agent_ui::AgentPanel>(cx);
964 match (disable_ai, existing_panel) {
965 (false, None) => cx.spawn_in(window, async move |workspace, cx| {
966 let panel =
967 agent_ui::AgentPanel::load(workspace.clone(), prompt_builder, cx.clone())
968 .await?;
969 workspace.update_in(cx, |workspace, window, cx| {
970 let disable_ai = SettingsStore::global(cx)
971 .get::<DisableAiSettings>(None)
972 .disable_ai;
973 let have_panel = workspace.panel::<agent_ui::AgentPanel>(cx).is_some();
974 if !disable_ai && !have_panel {
975 workspace.add_panel(panel, window, cx);
976 }
977 })
978 }),
979 (true, Some(existing_panel)) => {
980 workspace.remove_panel::<agent_ui::AgentPanel>(&existing_panel, window, cx);
981 Task::ready(Ok(()))
982 }
983 _ => Task::ready(Ok(())),
984 }
985 }
986
987 workspace_handle
988 .update_in(cx, |workspace, window, cx| {
989 setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx)
990 })?
991 .await?;
992
993 workspace_handle.update_in(cx, |workspace, window, cx| {
994 cx.observe_global_in::<SettingsStore>(window, {
995 let prompt_builder = prompt_builder.clone();
996 move |workspace, window, cx| {
997 setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx)
998 .detach_and_log_err(cx);
999 }
1000 })
1001 .detach();
1002
1003 // Register the actions that are shared between `assistant` and `assistant2`.
1004 //
1005 // We need to do this here instead of within the individual `init`
1006 // functions so that we only register the actions once.
1007 //
1008 // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`.
1009 if !cfg!(test) {
1010 <dyn AgentPanelDelegate>::set_global(
1011 Arc::new(agent_ui::ConcreteAssistantPanelDelegate),
1012 cx,
1013 );
1014
1015 workspace
1016 .register_action(agent_ui::AgentPanel::toggle_focus)
1017 .register_action(agent_ui::InlineAssistant::inline_assist);
1018 }
1019 })?;
1020
1021 anyhow::Ok(())
1022 })
1023 .detach();
1024}
1025
1026fn register_actions(
1027 app_state: Arc<AppState>,
1028 workspace: &mut Workspace,
1029 _: &mut Window,
1030 cx: &mut Context<Workspace>,
1031) {
1032 workspace
1033 .register_action(about)
1034 .register_action(|_, _: &OpenDocs, _, cx| cx.open_url(DOCS_URL))
1035 .register_action(|_, _: &Minimize, window, _| {
1036 window.minimize_window();
1037 })
1038 .register_action(|_, _: &Zoom, window, _| {
1039 window.zoom_window();
1040 })
1041 .register_action(|_, _: &ToggleFullScreen, window, _| {
1042 window.toggle_fullscreen();
1043 })
1044 .register_action(|_, action: &OpenZedUrl, _, cx| {
1045 OpenListener::global(cx).open(RawOpenRequest {
1046 urls: vec![action.url.clone()],
1047 ..Default::default()
1048 })
1049 })
1050 .register_action(|_, action: &OpenBrowser, _window, cx| cx.open_url(&action.url))
1051 .register_action(|workspace, _: &workspace::Open, window, cx| {
1052 telemetry::event!("Project Opened");
1053 let paths = workspace.prompt_for_open_path(
1054 PathPromptOptions {
1055 files: true,
1056 directories: true,
1057 multiple: true,
1058 prompt: None,
1059 },
1060 DirectoryLister::Local(
1061 workspace.project().clone(),
1062 workspace.app_state().fs.clone(),
1063 ),
1064 window,
1065 cx,
1066 );
1067
1068 cx.spawn_in(window, async move |this, cx| {
1069 let Some(paths) = paths.await.log_err().flatten() else {
1070 return;
1071 };
1072
1073 if let Some(task) = this
1074 .update_in(cx, |this, window, cx| {
1075 this.open_workspace_for_paths(false, paths, window, cx)
1076 })
1077 .log_err()
1078 {
1079 task.await.log_err();
1080 }
1081 })
1082 .detach()
1083 })
1084 .register_action(|workspace, action: &zed_actions::OpenRemote, window, cx| {
1085 if !action.from_existing_connection {
1086 cx.propagate();
1087 return;
1088 }
1089 // You need existing remote connection to open it this way
1090 if workspace.project().read(cx).is_local() {
1091 return;
1092 }
1093 telemetry::event!("Project Opened");
1094 let paths = workspace.prompt_for_open_path(
1095 PathPromptOptions {
1096 files: true,
1097 directories: true,
1098 multiple: true,
1099 prompt: None,
1100 },
1101 DirectoryLister::Project(workspace.project().clone()),
1102 window,
1103 cx,
1104 );
1105 cx.spawn_in(window, async move |this, cx| {
1106 let Some(paths) = paths.await.log_err().flatten() else {
1107 return;
1108 };
1109 if let Some(task) = this
1110 .update_in(cx, |this, window, cx| {
1111 open_new_ssh_project_from_project(this, paths, window, cx)
1112 })
1113 .log_err()
1114 {
1115 task.await.log_err();
1116 }
1117 })
1118 .detach()
1119 })
1120 .register_action({
1121 let fs = app_state.fs.clone();
1122 move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| {
1123 if action.persist {
1124 update_settings_file(fs.clone(), cx, move |settings, cx| {
1125 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0);
1126 let _ = settings
1127 .theme
1128 .ui_font_size
1129 .insert(theme::clamp_font_size(ui_font_size).into());
1130 });
1131 } else {
1132 theme::adjust_ui_font_size(cx, |size| size + px(1.0));
1133 }
1134 }
1135 })
1136 .register_action({
1137 let fs = app_state.fs.clone();
1138 move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| {
1139 if action.persist {
1140 update_settings_file(fs.clone(), cx, move |settings, cx| {
1141 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0);
1142 let _ = settings
1143 .theme
1144 .ui_font_size
1145 .insert(theme::clamp_font_size(ui_font_size).into());
1146 });
1147 } else {
1148 theme::adjust_ui_font_size(cx, |size| size - px(1.0));
1149 }
1150 }
1151 })
1152 .register_action({
1153 let fs = app_state.fs.clone();
1154 move |_, action: &zed_actions::ResetUiFontSize, _window, cx| {
1155 if action.persist {
1156 update_settings_file(fs.clone(), cx, move |settings, _| {
1157 settings.theme.ui_font_size = None;
1158 });
1159 } else {
1160 theme::reset_ui_font_size(cx);
1161 }
1162 }
1163 })
1164 .register_action({
1165 let fs = app_state.fs.clone();
1166 move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| {
1167 if action.persist {
1168 update_settings_file(fs.clone(), cx, move |settings, cx| {
1169 let buffer_font_size =
1170 ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0);
1171 let _ = settings
1172 .theme
1173 .buffer_font_size
1174 .insert(theme::clamp_font_size(buffer_font_size).into());
1175 });
1176 } else {
1177 theme::adjust_buffer_font_size(cx, |size| size + px(1.0));
1178 }
1179 }
1180 })
1181 .register_action({
1182 let fs = app_state.fs.clone();
1183 move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| {
1184 if action.persist {
1185 update_settings_file(fs.clone(), cx, move |settings, cx| {
1186 let buffer_font_size =
1187 ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0);
1188 let _ = settings
1189 .theme
1190 .buffer_font_size
1191 .insert(theme::clamp_font_size(buffer_font_size).into());
1192 });
1193 } else {
1194 theme::adjust_buffer_font_size(cx, |size| size - px(1.0));
1195 }
1196 }
1197 })
1198 .register_action({
1199 let fs = app_state.fs.clone();
1200 move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| {
1201 if action.persist {
1202 update_settings_file(fs.clone(), cx, move |settings, _| {
1203 settings.theme.buffer_font_size = None;
1204 });
1205 } else {
1206 theme::reset_buffer_font_size(cx);
1207 }
1208 }
1209 })
1210 .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| {
1211 cx.spawn_in(window, async move |workspace, cx| {
1212 install_cli::register_zed_scheme(cx).await?;
1213 workspace.update_in(cx, |workspace, _, cx| {
1214 struct RegisterZedScheme;
1215
1216 workspace.show_toast(
1217 Toast::new(
1218 NotificationId::unique::<RegisterZedScheme>(),
1219 format!(
1220 "zed:// links will now open in {}.",
1221 ReleaseChannel::global(cx).display_name()
1222 ),
1223 ),
1224 cx,
1225 )
1226 })?;
1227 Ok(())
1228 })
1229 .detach_and_prompt_err(
1230 "Error registering zed:// scheme",
1231 window,
1232 cx,
1233 |_, _, _| None,
1234 );
1235 })
1236 .register_action(open_project_settings_file)
1237 .register_action(open_project_tasks_file)
1238 .register_action(open_project_debug_tasks_file)
1239 .register_action(
1240 |workspace: &mut Workspace,
1241 _: &project_panel::ToggleFocus,
1242 window: &mut Window,
1243 cx: &mut Context<Workspace>| {
1244 workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
1245 },
1246 )
1247 .register_action(
1248 |workspace: &mut Workspace,
1249 _: &outline_panel::ToggleFocus,
1250 window: &mut Window,
1251 cx: &mut Context<Workspace>| {
1252 workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
1253 },
1254 )
1255 .register_action(
1256 |workspace: &mut Workspace,
1257 _: &collab_ui::collab_panel::ToggleFocus,
1258 window: &mut Window,
1259 cx: &mut Context<Workspace>| {
1260 workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(window, cx);
1261 },
1262 )
1263 .register_action(
1264 |workspace: &mut Workspace,
1265 _: &collab_ui::notification_panel::ToggleFocus,
1266 window: &mut Window,
1267 cx: &mut Context<Workspace>| {
1268 workspace.toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(
1269 window, cx,
1270 );
1271 },
1272 )
1273 .register_action(
1274 |workspace: &mut Workspace,
1275 _: &terminal_panel::ToggleFocus,
1276 window: &mut Window,
1277 cx: &mut Context<Workspace>| {
1278 workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
1279 },
1280 )
1281 .register_action({
1282 let app_state = Arc::downgrade(&app_state);
1283 move |_, _: &NewWindow, _, cx| {
1284 if let Some(app_state) = app_state.upgrade() {
1285 open_new(
1286 Default::default(),
1287 app_state,
1288 cx,
1289 |workspace, window, cx| {
1290 cx.activate(true);
1291 Editor::new_file(workspace, &Default::default(), window, cx)
1292 },
1293 )
1294 .detach();
1295 }
1296 }
1297 })
1298 .register_action({
1299 let app_state = Arc::downgrade(&app_state);
1300 move |_, _: &NewFile, _, cx| {
1301 if let Some(app_state) = app_state.upgrade() {
1302 open_new(
1303 Default::default(),
1304 app_state,
1305 cx,
1306 |workspace, window, cx| {
1307 Editor::new_file(workspace, &Default::default(), window, cx)
1308 },
1309 )
1310 .detach();
1311 }
1312 }
1313 })
1314 .register_action(|workspace, _: &CaptureRecentAudio, window, cx| {
1315 capture_recent_audio(workspace, window, cx);
1316 });
1317
1318 #[cfg(not(target_os = "windows"))]
1319 workspace.register_action(install_cli);
1320
1321 if workspace.project().read(cx).is_via_remote_server() {
1322 workspace.register_action({
1323 move |workspace, _: &OpenServerSettings, window, cx| {
1324 let open_server_settings = workspace
1325 .project()
1326 .update(cx, |project, cx| project.open_server_settings(cx));
1327
1328 cx.spawn_in(window, async move |workspace, cx| {
1329 let buffer = open_server_settings.await?;
1330
1331 workspace
1332 .update_in(cx, |workspace, window, cx| {
1333 workspace.open_path(
1334 buffer
1335 .read(cx)
1336 .project_path(cx)
1337 .expect("Settings file must have a location"),
1338 None,
1339 true,
1340 window,
1341 cx,
1342 )
1343 })?
1344 .await?;
1345
1346 anyhow::Ok(())
1347 })
1348 .detach_and_log_err(cx);
1349 }
1350 });
1351 }
1352}
1353
1354fn initialize_pane(
1355 workspace: &Workspace,
1356 pane: &Entity<Pane>,
1357 window: &mut Window,
1358 cx: &mut Context<Workspace>,
1359) {
1360 pane.update(cx, |pane, cx| {
1361 pane.toolbar().update(cx, |toolbar, cx| {
1362 let multibuffer_hint = cx.new(|_| MultibufferHint::new());
1363 toolbar.add_item(multibuffer_hint, window, cx);
1364 let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1365 toolbar.add_item(breadcrumbs, window, cx);
1366 let buffer_search_bar = cx.new(|cx| {
1367 search::BufferSearchBar::new(
1368 Some(workspace.project().read(cx).languages().clone()),
1369 window,
1370 cx,
1371 )
1372 });
1373 toolbar.add_item(buffer_search_bar.clone(), window, cx);
1374 let proposed_change_bar = cx.new(|_| ProposedChangesEditorToolbar::new());
1375 toolbar.add_item(proposed_change_bar, window, cx);
1376 let quick_action_bar =
1377 cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
1378 toolbar.add_item(quick_action_bar, window, cx);
1379 let diagnostic_editor_controls = cx.new(|_| diagnostics::ToolbarControls::new());
1380 toolbar.add_item(diagnostic_editor_controls, window, cx);
1381 let project_search_bar = cx.new(|_| ProjectSearchBar::new());
1382 toolbar.add_item(project_search_bar, window, cx);
1383 let lsp_log_item = cx.new(|_| LspLogToolbarItemView::new());
1384 toolbar.add_item(lsp_log_item, window, cx);
1385 let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new());
1386 toolbar.add_item(dap_log_item, window, cx);
1387 let acp_tools_item = cx.new(|_| acp_tools::AcpToolsToolbarItemView::new());
1388 toolbar.add_item(acp_tools_item, window, cx);
1389 let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new());
1390 toolbar.add_item(syntax_tree_item, window, cx);
1391 let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx));
1392 toolbar.add_item(migration_banner, window, cx);
1393 let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
1394 toolbar.add_item(project_diff_toolbar, window, cx);
1395 let commit_view_toolbar = cx.new(|cx| CommitViewToolbar::new(workspace, cx));
1396 toolbar.add_item(commit_view_toolbar, window, cx);
1397 let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
1398 toolbar.add_item(agent_diff_toolbar, window, cx);
1399 let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx));
1400 toolbar.add_item(basedpyright_banner, window, cx);
1401 })
1402 });
1403}
1404
1405fn about(
1406 _: &mut Workspace,
1407 _: &zed_actions::About,
1408 window: &mut Window,
1409 cx: &mut Context<Workspace>,
1410) {
1411 let release_channel = ReleaseChannel::global(cx).display_name();
1412 let version = env!("CARGO_PKG_VERSION");
1413 let debug = if cfg!(debug_assertions) {
1414 "(debug)"
1415 } else {
1416 ""
1417 };
1418 let message = format!("{release_channel} {version} {debug}");
1419 let detail = AppCommitSha::try_global(cx).map(|sha| sha.full());
1420
1421 let prompt = window.prompt(
1422 PromptLevel::Info,
1423 &message,
1424 detail.as_deref(),
1425 &["Copy", "OK"],
1426 cx,
1427 );
1428 cx.spawn(async move |_, cx| {
1429 if let Ok(0) = prompt.await {
1430 let content = format!("{}\n{}", message, detail.as_deref().unwrap_or(""));
1431 cx.update(|cx| {
1432 cx.write_to_clipboard(gpui::ClipboardItem::new_string(content));
1433 })
1434 .ok();
1435 }
1436 })
1437 .detach();
1438}
1439
1440#[cfg(not(target_os = "windows"))]
1441fn install_cli(
1442 _: &mut Workspace,
1443 _: &install_cli::InstallCliBinary,
1444 window: &mut Window,
1445 cx: &mut Context<Workspace>,
1446) {
1447 install_cli::install_cli_binary(window, cx)
1448}
1449
1450static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);
1451fn quit(_: &Quit, cx: &mut App) {
1452 if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) {
1453 return;
1454 }
1455
1456 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
1457 cx.spawn(async move |cx| {
1458 let mut workspace_windows = cx.update(|cx| {
1459 cx.windows()
1460 .into_iter()
1461 .filter_map(|window| window.downcast::<Workspace>())
1462 .collect::<Vec<_>>()
1463 })?;
1464
1465 // If multiple windows have unsaved changes, and need a save prompt,
1466 // prompt in the active window before switching to a different window.
1467 cx.update(|cx| {
1468 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
1469 })
1470 .log_err();
1471
1472 if should_confirm && let Some(workspace) = workspace_windows.first() {
1473 let answer = workspace
1474 .update(cx, |_, window, cx| {
1475 window.prompt(
1476 PromptLevel::Info,
1477 "Are you sure you want to quit?",
1478 None,
1479 &["Quit", "Cancel"],
1480 cx,
1481 )
1482 })
1483 .log_err();
1484
1485 if let Some(answer) = answer {
1486 WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release);
1487 let answer = answer.await.ok();
1488 WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release);
1489 if answer != Some(0) {
1490 return Ok(());
1491 }
1492 }
1493 }
1494
1495 // If the user cancels any save prompt, then keep the app open.
1496 for window in workspace_windows {
1497 if let Some(should_close) = window
1498 .update(cx, |workspace, window, cx| {
1499 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
1500 })
1501 .log_err()
1502 && !should_close.await?
1503 {
1504 return Ok(());
1505 }
1506 }
1507 cx.update(|cx| cx.quit())?;
1508 anyhow::Ok(())
1509 })
1510 .detach_and_log_err(cx);
1511}
1512
1513fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1514 const MAX_LINES: usize = 1000;
1515 workspace
1516 .with_local_workspace(window, cx, move |workspace, window, cx| {
1517 let app_state = workspace.app_state();
1518 let languages = app_state.languages.clone();
1519 let fs = app_state.fs.clone();
1520 cx.spawn_in(window, async move |workspace, cx| {
1521 let (old_log, new_log, log_language) = futures::join!(
1522 fs.load(paths::old_log_file()),
1523 fs.load(paths::log_file()),
1524 languages.language_for_name("log")
1525 );
1526 let log = match (old_log, new_log) {
1527 (Err(_), Err(_)) => None,
1528 (old_log, new_log) => {
1529 let mut lines = VecDeque::with_capacity(MAX_LINES);
1530 for line in old_log
1531 .iter()
1532 .flat_map(|log| log.lines())
1533 .chain(new_log.iter().flat_map(|log| log.lines()))
1534 {
1535 if lines.len() == MAX_LINES {
1536 lines.pop_front();
1537 }
1538 lines.push_back(line);
1539 }
1540 Some(
1541 lines
1542 .into_iter()
1543 .flat_map(|line| [line, "\n"])
1544 .collect::<String>(),
1545 )
1546 }
1547 };
1548 let log_language = log_language.ok();
1549
1550 workspace
1551 .update_in(cx, |workspace, window, cx| {
1552 let Some(log) = log else {
1553 struct OpenLogError;
1554
1555 workspace.show_notification(
1556 NotificationId::unique::<OpenLogError>(),
1557 cx,
1558 |cx| {
1559 cx.new(|cx| {
1560 MessageNotification::new(
1561 format!(
1562 "Unable to access/open log file at path {:?}",
1563 paths::log_file().as_path()
1564 ),
1565 cx,
1566 )
1567 })
1568 },
1569 );
1570 return;
1571 };
1572 let project = workspace.project().clone();
1573 let buffer = project.update(cx, |project, cx| {
1574 project.create_local_buffer(&log, log_language, false, cx)
1575 });
1576
1577 let buffer = cx
1578 .new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into()));
1579 let editor = cx.new(|cx| {
1580 let mut editor =
1581 Editor::for_multibuffer(buffer, Some(project), window, cx);
1582 editor.set_read_only(true);
1583 editor.set_breadcrumb_header(format!(
1584 "Last {} lines in {}",
1585 MAX_LINES,
1586 paths::log_file().display()
1587 ));
1588 editor
1589 });
1590
1591 editor.update(cx, |editor, cx| {
1592 let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
1593 editor.change_selections(Default::default(), window, cx, |s| {
1594 s.select_ranges(Some(
1595 last_multi_buffer_offset..last_multi_buffer_offset,
1596 ));
1597 })
1598 });
1599
1600 workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
1601 })
1602 .log_err();
1603 })
1604 .detach();
1605 })
1606 .detach();
1607}
1608
1609pub fn handle_settings_file_changes(
1610 mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
1611 mut global_settings_file_rx: mpsc::UnboundedReceiver<String>,
1612 cx: &mut App,
1613 settings_changed: impl Fn(Option<anyhow::Error>, &mut App) + 'static,
1614) {
1615 MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);
1616
1617 // Helper function to process settings content
1618 let process_settings = move |content: String,
1619 is_user: bool,
1620 store: &mut SettingsStore,
1621 cx: &mut App|
1622 -> bool {
1623 let id = NotificationId::Named("failed-to-migrate-settings".into());
1624 // Apply migrations to both user and global settings
1625 let (processed_content, content_migrated) = match migrate_settings(&content) {
1626 Ok(result) => {
1627 dismiss_app_notification(&id, cx);
1628 if let Some(migrated_content) = result {
1629 (migrated_content, true)
1630 } else {
1631 (content, false)
1632 }
1633 }
1634 Err(err) => {
1635 show_app_notification(id, cx, move |cx| {
1636 cx.new(|cx| {
1637 MessageNotification::new(
1638 format!(
1639 "Failed to migrate settings\n\
1640 {err}"
1641 ),
1642 cx,
1643 )
1644 .primary_message("Open Settings File")
1645 .primary_icon(IconName::Settings)
1646 .primary_on_click(|window, cx| {
1647 window.dispatch_action(zed_actions::OpenSettingsFile.boxed_clone(), cx);
1648 cx.emit(DismissEvent);
1649 })
1650 })
1651 });
1652 // notify user here
1653 (content, false)
1654 }
1655 };
1656
1657 let result = if is_user {
1658 store.set_user_settings(&processed_content, cx)
1659 } else {
1660 store.set_global_settings(&processed_content, cx)
1661 };
1662
1663 if let Err(err) = &result {
1664 let settings_type = if is_user { "user" } else { "global" };
1665 log::error!("Failed to load {} settings: {err}", settings_type);
1666 }
1667
1668 settings_changed(result.err(), cx);
1669
1670 content_migrated
1671 };
1672
1673 // Initial load of both settings files
1674 let global_content = cx
1675 .background_executor()
1676 .block(global_settings_file_rx.next())
1677 .unwrap();
1678 let user_content = cx
1679 .background_executor()
1680 .block(user_settings_file_rx.next())
1681 .unwrap();
1682
1683 SettingsStore::update_global(cx, |store, cx| {
1684 process_settings(global_content, false, store, cx);
1685 process_settings(user_content, true, store, cx);
1686 });
1687
1688 // Watch for changes in both files
1689 cx.spawn(async move |cx| {
1690 let mut settings_streams = futures::stream::select(
1691 global_settings_file_rx.map(Either::Left),
1692 user_settings_file_rx.map(Either::Right),
1693 );
1694
1695 while let Some(content) = settings_streams.next().await {
1696 let (content, is_user) = match content {
1697 Either::Left(content) => (content, false),
1698 Either::Right(content) => (content, true),
1699 };
1700
1701 let result = cx.update_global(|store: &mut SettingsStore, cx| {
1702 let migrating_in_memory = process_settings(content, is_user, store, cx);
1703 if let Some(notifier) = MigrationNotification::try_global(cx) {
1704 notifier.update(cx, |_, cx| {
1705 cx.emit(MigrationEvent::ContentChanged {
1706 migration_type: MigrationType::Settings,
1707 migrating_in_memory,
1708 });
1709 });
1710 }
1711 cx.refresh_windows();
1712 });
1713
1714 if result.is_err() {
1715 break; // App dropped
1716 }
1717 }
1718 })
1719 .detach();
1720}
1721
1722pub fn handle_keymap_file_changes(
1723 mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
1724 cx: &mut App,
1725) {
1726 BaseKeymap::register(cx);
1727 vim_mode_setting::init(cx);
1728
1729 let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
1730 let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded();
1731 let mut old_base_keymap = *BaseKeymap::get_global(cx);
1732 let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
1733 let mut old_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
1734
1735 cx.observe_global::<SettingsStore>(move |cx| {
1736 let new_base_keymap = *BaseKeymap::get_global(cx);
1737 let new_vim_enabled = VimModeSetting::get_global(cx).0;
1738 let new_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
1739
1740 if new_base_keymap != old_base_keymap
1741 || new_vim_enabled != old_vim_enabled
1742 || new_helix_enabled != old_helix_enabled
1743 {
1744 old_base_keymap = new_base_keymap;
1745 old_vim_enabled = new_vim_enabled;
1746 old_helix_enabled = new_helix_enabled;
1747
1748 base_keymap_tx.unbounded_send(()).unwrap();
1749 }
1750 })
1751 .detach();
1752
1753 #[cfg(target_os = "windows")]
1754 {
1755 let mut current_layout_id = cx.keyboard_layout().id().to_string();
1756 cx.on_keyboard_layout_change(move |cx| {
1757 let next_layout_id = cx.keyboard_layout().id();
1758 if next_layout_id != current_layout_id {
1759 current_layout_id = next_layout_id.to_string();
1760 keyboard_layout_tx.unbounded_send(()).ok();
1761 }
1762 })
1763 .detach();
1764 }
1765
1766 #[cfg(not(target_os = "windows"))]
1767 {
1768 let mut current_mapping = cx.keyboard_mapper().get_key_equivalents().cloned();
1769 cx.on_keyboard_layout_change(move |cx| {
1770 let next_mapping = cx.keyboard_mapper().get_key_equivalents();
1771 if current_mapping.as_ref() != next_mapping {
1772 current_mapping = next_mapping.cloned();
1773 keyboard_layout_tx.unbounded_send(()).ok();
1774 }
1775 })
1776 .detach();
1777 }
1778
1779 load_default_keymap(cx);
1780
1781 struct KeymapParseErrorNotification;
1782 let notification_id = NotificationId::unique::<KeymapParseErrorNotification>();
1783
1784 cx.spawn(async move |cx| {
1785 let mut user_keymap_content = String::new();
1786 let mut migrating_in_memory = false;
1787 loop {
1788 select_biased! {
1789 _ = base_keymap_rx.next() => {},
1790 _ = keyboard_layout_rx.next() => {},
1791 content = user_keymap_file_rx.next() => {
1792 if let Some(content) = content {
1793 if let Ok(Some(migrated_content)) = migrate_keymap(&content) {
1794 user_keymap_content = migrated_content;
1795 migrating_in_memory = true;
1796 } else {
1797 user_keymap_content = content;
1798 migrating_in_memory = false;
1799 }
1800 }
1801 }
1802 };
1803 cx.update(|cx| {
1804 if let Some(notifier) = MigrationNotification::try_global(cx) {
1805 notifier.update(cx, |_, cx| {
1806 cx.emit(MigrationEvent::ContentChanged {
1807 migration_type: MigrationType::Keymap,
1808 migrating_in_memory,
1809 });
1810 });
1811 }
1812 let load_result = KeymapFile::load(&user_keymap_content, cx);
1813 match load_result {
1814 KeymapFileLoadResult::Success { key_bindings } => {
1815 reload_keymaps(cx, key_bindings);
1816 dismiss_app_notification(¬ification_id.clone(), cx);
1817 }
1818 KeymapFileLoadResult::SomeFailedToLoad {
1819 key_bindings,
1820 error_message,
1821 } => {
1822 if !key_bindings.is_empty() {
1823 reload_keymaps(cx, key_bindings);
1824 }
1825 show_keymap_file_load_error(notification_id.clone(), error_message, cx);
1826 }
1827 KeymapFileLoadResult::JsonParseFailure { error } => {
1828 show_keymap_file_json_error(notification_id.clone(), &error, cx)
1829 }
1830 }
1831 })
1832 .ok();
1833 }
1834 })
1835 .detach();
1836}
1837
1838fn show_keymap_file_json_error(
1839 notification_id: NotificationId,
1840 error: &anyhow::Error,
1841 cx: &mut App,
1842) {
1843 let message: SharedString =
1844 format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
1845 show_app_notification(notification_id, cx, move |cx| {
1846 cx.new(|cx| {
1847 MessageNotification::new(message.clone(), cx)
1848 .primary_message("Open Keymap File")
1849 .primary_on_click(|window, cx| {
1850 window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
1851 cx.emit(DismissEvent);
1852 })
1853 })
1854 });
1855}
1856
1857fn show_keymap_file_load_error(
1858 notification_id: NotificationId,
1859 error_message: MarkdownString,
1860 cx: &mut App,
1861) {
1862 show_markdown_app_notification(
1863 notification_id,
1864 error_message,
1865 "Open Keymap File".into(),
1866 |window, cx| {
1867 window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
1868 cx.emit(DismissEvent);
1869 },
1870 cx,
1871 )
1872}
1873
1874fn show_markdown_app_notification<F>(
1875 notification_id: NotificationId,
1876 message: MarkdownString,
1877 primary_button_message: SharedString,
1878 primary_button_on_click: F,
1879 cx: &mut App,
1880) where
1881 F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
1882{
1883 let parsed_markdown = cx.background_spawn(async move {
1884 let file_location_directory = None;
1885 let language_registry = None;
1886 markdown_preview::markdown_parser::parse_markdown(
1887 &message.0,
1888 file_location_directory,
1889 language_registry,
1890 )
1891 .await
1892 });
1893
1894 cx.spawn(async move |cx| {
1895 let parsed_markdown = Arc::new(parsed_markdown.await);
1896 let primary_button_message = primary_button_message.clone();
1897 let primary_button_on_click = Arc::new(primary_button_on_click);
1898 cx.update(|cx| {
1899 show_app_notification(notification_id, cx, move |cx| {
1900 let workspace_handle = cx.entity().downgrade();
1901 let parsed_markdown = parsed_markdown.clone();
1902 let primary_button_message = primary_button_message.clone();
1903 let primary_button_on_click = primary_button_on_click.clone();
1904 cx.new(move |cx| {
1905 MessageNotification::new_from_builder(cx, move |window, cx| {
1906 image_cache(retain_all("notification-cache"))
1907 .text_xs()
1908 .child(markdown_preview::markdown_renderer::render_parsed_markdown(
1909 &parsed_markdown.clone(),
1910 Some(workspace_handle.clone()),
1911 window,
1912 cx,
1913 ))
1914 .into_any()
1915 })
1916 .primary_message(primary_button_message)
1917 .primary_on_click_arc(primary_button_on_click)
1918 })
1919 })
1920 })
1921 .ok();
1922 })
1923 .detach();
1924}
1925
1926fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec<KeyBinding>) {
1927 cx.clear_key_bindings();
1928 load_default_keymap(cx);
1929
1930 for key_binding in &mut user_key_bindings {
1931 key_binding.set_meta(KeybindSource::User.meta());
1932 }
1933 cx.bind_keys(user_key_bindings);
1934
1935 let menus = app_menus(cx);
1936 cx.set_menus(menus);
1937 // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
1938 #[cfg(not(target_os = "windows"))]
1939 cx.set_dock_menu(vec![gpui::MenuItem::action(
1940 "New Window",
1941 workspace::NewWindow,
1942 )]);
1943 // todo: nicer api here?
1944 keymap_editor::KeymapEventChannel::trigger_keymap_changed(cx);
1945}
1946
1947pub fn load_default_keymap(cx: &mut App) {
1948 let base_keymap = *BaseKeymap::get_global(cx);
1949 if base_keymap == BaseKeymap::None {
1950 return;
1951 }
1952
1953 cx.bind_keys(
1954 KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, Some(KeybindSource::Default), cx).unwrap(),
1955 );
1956
1957 if let Some(asset_path) = base_keymap.asset_path() {
1958 cx.bind_keys(KeymapFile::load_asset(asset_path, Some(KeybindSource::Base), cx).unwrap());
1959 }
1960
1961 if VimModeSetting::get_global(cx).0 || vim_mode_setting::HelixModeSetting::get_global(cx).0 {
1962 cx.bind_keys(
1963 KeymapFile::load_asset(VIM_KEYMAP_PATH, Some(KeybindSource::Vim), cx).unwrap(),
1964 );
1965 }
1966}
1967
1968pub fn handle_settings_changed(error: Option<anyhow::Error>, cx: &mut App) {
1969 struct SettingsParseErrorNotification;
1970 let id = NotificationId::unique::<SettingsParseErrorNotification>();
1971
1972 match error {
1973 Some(error) => {
1974 if let Some(InvalidSettingsError::LocalSettings { .. }) =
1975 error.downcast_ref::<InvalidSettingsError>()
1976 {
1977 // Local settings errors are displayed by the projects
1978 return;
1979 }
1980 show_app_notification(id, cx, move |cx| {
1981 cx.new(|cx| {
1982 MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
1983 .primary_message("Open Settings File")
1984 .primary_icon(IconName::Settings)
1985 .primary_on_click(|window, cx| {
1986 window.dispatch_action(zed_actions::OpenSettingsFile.boxed_clone(), cx);
1987 cx.emit(DismissEvent);
1988 })
1989 })
1990 });
1991 }
1992 None => {
1993 dismiss_app_notification(&id, cx);
1994 }
1995 }
1996}
1997
1998pub fn open_new_ssh_project_from_project(
1999 workspace: &mut Workspace,
2000 paths: Vec<PathBuf>,
2001 window: &mut Window,
2002 cx: &mut Context<Workspace>,
2003) -> Task<anyhow::Result<()>> {
2004 let app_state = workspace.app_state().clone();
2005 let Some(ssh_client) = workspace.project().read(cx).remote_client() else {
2006 return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
2007 };
2008 let connection_options = ssh_client.read(cx).connection_options();
2009 cx.spawn_in(window, async move |_, cx| {
2010 open_remote_project(
2011 connection_options,
2012 paths,
2013 app_state,
2014 workspace::OpenOptions {
2015 open_new_workspace: Some(true),
2016 ..Default::default()
2017 },
2018 cx,
2019 )
2020 .await
2021 })
2022}
2023
2024fn open_project_settings_file(
2025 workspace: &mut Workspace,
2026 _: &OpenProjectSettings,
2027 window: &mut Window,
2028 cx: &mut Context<Workspace>,
2029) {
2030 open_local_file(
2031 workspace,
2032 local_settings_file_relative_path(),
2033 initial_project_settings_content(),
2034 window,
2035 cx,
2036 )
2037}
2038
2039fn open_project_tasks_file(
2040 workspace: &mut Workspace,
2041 _: &OpenProjectTasks,
2042 window: &mut Window,
2043 cx: &mut Context<Workspace>,
2044) {
2045 open_local_file(
2046 workspace,
2047 local_tasks_file_relative_path(),
2048 initial_tasks_content(),
2049 window,
2050 cx,
2051 )
2052}
2053
2054fn open_project_debug_tasks_file(
2055 workspace: &mut Workspace,
2056 _: &zed_actions::OpenProjectDebugTasks,
2057 window: &mut Window,
2058 cx: &mut Context<Workspace>,
2059) {
2060 open_local_file(
2061 workspace,
2062 local_debug_file_relative_path(),
2063 initial_local_debug_tasks_content(),
2064 window,
2065 cx,
2066 )
2067}
2068
2069fn open_local_file(
2070 workspace: &mut Workspace,
2071 settings_relative_path: &'static RelPath,
2072 initial_contents: Cow<'static, str>,
2073 window: &mut Window,
2074 cx: &mut Context<Workspace>,
2075) {
2076 let project = workspace.project().clone();
2077 let worktree = project
2078 .read(cx)
2079 .visible_worktrees(cx)
2080 .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
2081 if let Some(worktree) = worktree {
2082 let tree_id = worktree.read(cx).id();
2083 cx.spawn_in(window, async move |workspace, cx| {
2084 // Check if the file actually exists on disk (even if it's excluded from worktree)
2085 let file_exists = {
2086 let full_path = worktree.read_with(cx, |tree, _| {
2087 tree.abs_path().join(settings_relative_path.as_std_path())
2088 })?;
2089
2090 let fs = project.read_with(cx, |project, _| project.fs().clone())?;
2091
2092 fs.metadata(&full_path)
2093 .await
2094 .ok()
2095 .flatten()
2096 .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo)
2097 };
2098
2099 if !file_exists {
2100 if let Some(dir_path) = settings_relative_path.parent()
2101 && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())?
2102 {
2103 project
2104 .update(cx, |project, cx| {
2105 project.create_entry((tree_id, dir_path), true, cx)
2106 })?
2107 .await
2108 .context("worktree was removed")?;
2109 }
2110
2111 if worktree.read_with(cx, |tree, _| {
2112 tree.entry_for_path(settings_relative_path).is_none()
2113 })? {
2114 project
2115 .update(cx, |project, cx| {
2116 project.create_entry((tree_id, settings_relative_path), false, cx)
2117 })?
2118 .await
2119 .context("worktree was removed")?;
2120 }
2121 }
2122
2123 let editor = workspace
2124 .update_in(cx, |workspace, window, cx| {
2125 workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
2126 })?
2127 .await?
2128 .downcast::<Editor>()
2129 .context("unexpected item type: expected editor item")?;
2130
2131 editor
2132 .downgrade()
2133 .update(cx, |editor, cx| {
2134 if let Some(buffer) = editor.buffer().read(cx).as_singleton()
2135 && buffer.read(cx).is_empty()
2136 {
2137 buffer.update(cx, |buffer, cx| {
2138 buffer.edit([(0..0, initial_contents)], None, cx)
2139 });
2140 }
2141 })
2142 .ok();
2143
2144 anyhow::Ok(())
2145 })
2146 .detach();
2147 } else {
2148 struct NoOpenFolders;
2149
2150 workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
2151 cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
2152 })
2153 }
2154}
2155
2156fn open_telemetry_log_file(
2157 workspace: &mut Workspace,
2158 window: &mut Window,
2159 cx: &mut Context<Workspace>,
2160) {
2161 workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
2162 let app_state = workspace.app_state().clone();
2163 cx.spawn_in(window, async move |workspace, cx| {
2164 async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
2165 let path = client::telemetry::Telemetry::log_file_path();
2166 app_state.fs.load(&path).await.log_err()
2167 }
2168
2169 let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
2170
2171 const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
2172 let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
2173 if let Some(newline_offset) = log[start_offset..].find('\n') {
2174 start_offset += newline_offset + 1;
2175 }
2176 let log_suffix = &log[start_offset..];
2177 let header = concat!(
2178 "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
2179 "// Telemetry can be disabled via the `settings.json` file.\n",
2180 "// Here is the data that has been reported for the current session:\n",
2181 );
2182 let content = format!("{}\n{}", header, log_suffix);
2183 let json = app_state.languages.language_for_name("JSON").await.log_err();
2184
2185 workspace.update_in( cx, |workspace, window, cx| {
2186 let project = workspace.project().clone();
2187 let buffer = project.update(cx, |project, cx| project.create_local_buffer(&content, json,false, cx));
2188 let buffer = cx.new(|cx| {
2189 MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
2190 });
2191 workspace.add_item_to_active_pane(
2192 Box::new(cx.new(|cx| {
2193 let mut editor = Editor::for_multibuffer(buffer, Some(project), window, cx);
2194 editor.set_read_only(true);
2195 editor.set_breadcrumb_header("Telemetry Log".into());
2196 editor
2197 })),
2198 None,
2199 true,
2200 window, cx,
2201 );
2202 }).log_err()?;
2203
2204 Some(())
2205 })
2206 .detach();
2207 }).detach();
2208}
2209
2210fn open_bundled_file(
2211 workspace: &Workspace,
2212 text: Cow<'static, str>,
2213 title: &'static str,
2214 language: &'static str,
2215 window: &mut Window,
2216 cx: &mut Context<Workspace>,
2217) {
2218 let language = workspace.app_state().languages.language_for_name(language);
2219 cx.spawn_in(window, async move |workspace, cx| {
2220 let language = language.await.log_err();
2221 workspace
2222 .update_in(cx, |workspace, window, cx| {
2223 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
2224 let project = workspace.project();
2225 let buffer = project.update(cx, move |project, cx| {
2226 let buffer =
2227 project.create_local_buffer(text.as_ref(), language, false, cx);
2228 buffer.update(cx, |buffer, cx| {
2229 buffer.set_capability(Capability::ReadOnly, cx);
2230 });
2231 buffer
2232 });
2233 let buffer =
2234 cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
2235 workspace.add_item_to_active_pane(
2236 Box::new(cx.new(|cx| {
2237 let mut editor =
2238 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2239 editor.set_read_only(true);
2240 editor.set_breadcrumb_header(title.into());
2241 editor
2242 })),
2243 None,
2244 true,
2245 window,
2246 cx,
2247 );
2248 })
2249 })?
2250 .await
2251 })
2252 .detach_and_log_err(cx);
2253}
2254
2255fn open_settings_file(
2256 abs_path: &'static Path,
2257 default_content: impl FnOnce() -> Rope + Send + 'static,
2258 window: &mut Window,
2259 cx: &mut Context<Workspace>,
2260) {
2261 cx.spawn_in(window, async move |workspace, cx| {
2262 let (worktree_creation_task, settings_open_task) = workspace
2263 .update_in(cx, |workspace, window, cx| {
2264 workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
2265 let worktree_creation_task = workspace.project().update(cx, |project, cx| {
2266 // Set up a dedicated worktree for settings, since
2267 // otherwise we're dropping and re-starting LSP servers
2268 // for each file inside on every settings file
2269 // close/open
2270
2271 // TODO: Do note that all other external files (e.g.
2272 // drag and drop from OS) still have their worktrees
2273 // released on file close, causing LSP servers'
2274 // restarts.
2275 project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
2276 });
2277 let settings_open_task =
2278 create_and_open_local_file(abs_path, window, cx, default_content);
2279 (worktree_creation_task, settings_open_task)
2280 })
2281 })?
2282 .await?;
2283 let _ = worktree_creation_task.await?;
2284 let _ = settings_open_task.await?;
2285 anyhow::Ok(())
2286 })
2287 .detach_and_log_err(cx);
2288}
2289
2290fn capture_recent_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
2291 struct CaptureRecentAudioNotification {
2292 focus_handle: gpui::FocusHandle,
2293 save_result: Option<Result<(PathBuf, Duration), anyhow::Error>>,
2294 _save_task: Task<anyhow::Result<()>>,
2295 }
2296
2297 impl gpui::EventEmitter<DismissEvent> for CaptureRecentAudioNotification {}
2298 impl gpui::EventEmitter<SuppressEvent> for CaptureRecentAudioNotification {}
2299 impl gpui::Focusable for CaptureRecentAudioNotification {
2300 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
2301 self.focus_handle.clone()
2302 }
2303 }
2304 impl workspace::notifications::Notification for CaptureRecentAudioNotification {}
2305
2306 impl Render for CaptureRecentAudioNotification {
2307 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2308 let message = match &self.save_result {
2309 None => format!(
2310 "Saving up to {} seconds of recent audio",
2311 REPLAY_DURATION.as_secs(),
2312 ),
2313 Some(Ok((path, duration))) => format!(
2314 "Saved {} seconds of all audio to {}",
2315 duration.as_secs(),
2316 path.display(),
2317 ),
2318 Some(Err(e)) => format!("Error saving audio replays: {e:?}"),
2319 };
2320
2321 NotificationFrame::new()
2322 .with_title(Some("Saved Audio"))
2323 .show_suppress_button(false)
2324 .on_close(cx.listener(|_, _, _, cx| {
2325 cx.emit(DismissEvent);
2326 }))
2327 .with_content(message)
2328 }
2329 }
2330
2331 impl CaptureRecentAudioNotification {
2332 fn new(cx: &mut Context<Self>) -> Self {
2333 if AudioSettings::get_global(cx).rodio_audio {
2334 let executor = cx.background_executor().clone();
2335 let save_task = cx.default_global::<audio::Audio>().save_replays(executor);
2336 let _save_task = cx.spawn(async move |this, cx| {
2337 let res = save_task.await;
2338 this.update(cx, |this, cx| {
2339 this.save_result = Some(res);
2340 cx.notify();
2341 })
2342 });
2343
2344 Self {
2345 focus_handle: cx.focus_handle(),
2346 _save_task,
2347 save_result: None,
2348 }
2349 } else {
2350 Self {
2351 focus_handle: cx.focus_handle(),
2352 _save_task: Task::ready(Ok(())),
2353 save_result: Some(Err(anyhow::anyhow!(
2354 "Capturing recent audio is only supported on the experimental rodio audio pipeline"
2355 ))),
2356 }
2357 }
2358 }
2359 }
2360
2361 workspace.show_notification(
2362 NotificationId::unique::<CaptureRecentAudioNotification>(),
2363 cx,
2364 |cx| cx.new(CaptureRecentAudioNotification::new),
2365 );
2366}
2367
2368/// Eagerly loads the active theme and icon theme based on the selections in the
2369/// theme settings.
2370///
2371/// This fast path exists to load these themes as soon as possible so the user
2372/// doesn't see the default themes while waiting on extensions to load.
2373pub(crate) fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &mut App) {
2374 let extension_store = ExtensionStore::global(cx);
2375 let theme_registry = ThemeRegistry::global(cx);
2376 let theme_settings = ThemeSettings::get_global(cx);
2377 let appearance = SystemAppearance::global(cx).0;
2378
2379 enum LoadTarget {
2380 Theme(PathBuf),
2381 IconTheme((PathBuf, PathBuf)),
2382 }
2383
2384 let theme_name = theme_settings.theme.name(appearance);
2385 let icon_theme_name = theme_settings.icon_theme.name(appearance);
2386 let themes_to_load = [
2387 theme_registry
2388 .get(&theme_name.0)
2389 .is_err()
2390 .then(|| {
2391 extension_store
2392 .read(cx)
2393 .path_to_extension_theme(&theme_name.0)
2394 })
2395 .flatten()
2396 .map(LoadTarget::Theme),
2397 theme_registry
2398 .get_icon_theme(&icon_theme_name.0)
2399 .is_err()
2400 .then(|| {
2401 extension_store
2402 .read(cx)
2403 .path_to_extension_icon_theme(&icon_theme_name.0)
2404 })
2405 .flatten()
2406 .map(LoadTarget::IconTheme),
2407 ];
2408
2409 enum ReloadTarget {
2410 Theme,
2411 IconTheme,
2412 }
2413
2414 let executor = cx.background_executor();
2415 let reload_tasks = parking_lot::Mutex::new(Vec::with_capacity(themes_to_load.len()));
2416
2417 let mut themes_to_load = themes_to_load.into_iter().flatten().peekable();
2418
2419 if themes_to_load.peek().is_none() {
2420 return;
2421 }
2422
2423 executor.block(executor.scoped(|scope| {
2424 for load_target in themes_to_load {
2425 let theme_registry = &theme_registry;
2426 let reload_tasks = &reload_tasks;
2427 let fs = fs.clone();
2428
2429 scope.spawn(async {
2430 match load_target {
2431 LoadTarget::Theme(theme_path) => {
2432 if theme_registry
2433 .load_user_theme(&theme_path, fs)
2434 .await
2435 .log_err()
2436 .is_some()
2437 {
2438 reload_tasks.lock().push(ReloadTarget::Theme);
2439 }
2440 }
2441 LoadTarget::IconTheme((icon_theme_path, icons_root_path)) => {
2442 if theme_registry
2443 .load_icon_theme(&icon_theme_path, &icons_root_path, fs)
2444 .await
2445 .log_err()
2446 .is_some()
2447 {
2448 reload_tasks.lock().push(ReloadTarget::IconTheme);
2449 }
2450 }
2451 }
2452 });
2453 }
2454 }));
2455
2456 for reload_target in reload_tasks.into_inner() {
2457 match reload_target {
2458 ReloadTarget::Theme => GlobalTheme::reload_theme(cx),
2459 ReloadTarget::IconTheme => GlobalTheme::reload_icon_theme(cx),
2460 };
2461 }
2462}
2463
2464#[cfg(test)]
2465mod tests {
2466 use super::*;
2467 use assets::Assets;
2468 use collections::HashSet;
2469 use editor::{DisplayPoint, Editor, SelectionEffects, display_map::DisplayRow};
2470 use gpui::{
2471 Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion,
2472 TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions,
2473 };
2474 use language::{LanguageMatcher, LanguageRegistry};
2475 use pretty_assertions::{assert_eq, assert_ne};
2476 use project::{Project, ProjectPath};
2477 use serde_json::json;
2478 use settings::{SettingsStore, watch_config_file};
2479 use std::{
2480 path::{Path, PathBuf},
2481 time::Duration,
2482 };
2483 use theme::ThemeRegistry;
2484 use util::{
2485 path,
2486 rel_path::{RelPath, rel_path},
2487 };
2488 use workspace::{
2489 NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
2490 WorkspaceHandle,
2491 item::SaveOptions,
2492 item::{Item, ItemHandle},
2493 open_new, open_paths, pane,
2494 };
2495
2496 #[gpui::test]
2497 async fn test_open_non_existing_file(cx: &mut TestAppContext) {
2498 let app_state = init_test(cx);
2499 app_state
2500 .fs
2501 .as_fake()
2502 .insert_tree(
2503 path!("/root"),
2504 json!({
2505 "a": {
2506 },
2507 }),
2508 )
2509 .await;
2510
2511 cx.update(|cx| {
2512 open_paths(
2513 &[PathBuf::from(path!("/root/a/new"))],
2514 app_state.clone(),
2515 workspace::OpenOptions::default(),
2516 cx,
2517 )
2518 })
2519 .await
2520 .unwrap();
2521 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2522
2523 let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
2524 workspace
2525 .update(cx, |workspace, _, cx| {
2526 assert!(workspace.active_item_as::<Editor>(cx).is_some())
2527 })
2528 .unwrap();
2529 }
2530
2531 #[gpui::test]
2532 async fn test_open_paths_action(cx: &mut TestAppContext) {
2533 let app_state = init_test(cx);
2534 app_state
2535 .fs
2536 .as_fake()
2537 .insert_tree(
2538 "/root",
2539 json!({
2540 "a": {
2541 "aa": null,
2542 "ab": null,
2543 },
2544 "b": {
2545 "ba": null,
2546 "bb": null,
2547 },
2548 "c": {
2549 "ca": null,
2550 "cb": null,
2551 },
2552 "d": {
2553 "da": null,
2554 "db": null,
2555 },
2556 "e": {
2557 "ea": null,
2558 "eb": null,
2559 }
2560 }),
2561 )
2562 .await;
2563
2564 cx.update(|cx| {
2565 open_paths(
2566 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
2567 app_state.clone(),
2568 workspace::OpenOptions::default(),
2569 cx,
2570 )
2571 })
2572 .await
2573 .unwrap();
2574 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2575
2576 cx.update(|cx| {
2577 open_paths(
2578 &[PathBuf::from("/root/a")],
2579 app_state.clone(),
2580 workspace::OpenOptions::default(),
2581 cx,
2582 )
2583 })
2584 .await
2585 .unwrap();
2586 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2587 let workspace_1 = cx
2588 .read(|cx| cx.windows()[0].downcast::<Workspace>())
2589 .unwrap();
2590 cx.run_until_parked();
2591 workspace_1
2592 .update(cx, |workspace, window, cx| {
2593 assert_eq!(workspace.worktrees(cx).count(), 2);
2594 assert!(workspace.left_dock().read(cx).is_open());
2595 assert!(
2596 workspace
2597 .active_pane()
2598 .read(cx)
2599 .focus_handle(cx)
2600 .is_focused(window)
2601 );
2602 })
2603 .unwrap();
2604
2605 cx.update(|cx| {
2606 open_paths(
2607 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
2608 app_state.clone(),
2609 workspace::OpenOptions::default(),
2610 cx,
2611 )
2612 })
2613 .await
2614 .unwrap();
2615 assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2616
2617 // Replace existing windows
2618 let window = cx
2619 .update(|cx| cx.windows()[0].downcast::<Workspace>())
2620 .unwrap();
2621 cx.update(|cx| {
2622 open_paths(
2623 &[PathBuf::from("/root/e")],
2624 app_state,
2625 workspace::OpenOptions {
2626 replace_window: Some(window),
2627 ..Default::default()
2628 },
2629 cx,
2630 )
2631 })
2632 .await
2633 .unwrap();
2634 cx.background_executor.run_until_parked();
2635 assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2636 let workspace_1 = cx
2637 .update(|cx| cx.windows()[0].downcast::<Workspace>())
2638 .unwrap();
2639 workspace_1
2640 .update(cx, |workspace, window, cx| {
2641 assert_eq!(
2642 workspace
2643 .worktrees(cx)
2644 .map(|w| w.read(cx).abs_path())
2645 .collect::<Vec<_>>(),
2646 &[Path::new("/root/e").into()]
2647 );
2648 assert!(workspace.left_dock().read(cx).is_open());
2649 assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
2650 })
2651 .unwrap();
2652 }
2653
2654 #[gpui::test]
2655 async fn test_open_add_new(cx: &mut TestAppContext) {
2656 let app_state = init_test(cx);
2657 app_state
2658 .fs
2659 .as_fake()
2660 .insert_tree(
2661 path!("/root"),
2662 json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
2663 )
2664 .await;
2665
2666 cx.update(|cx| {
2667 open_paths(
2668 &[PathBuf::from(path!("/root/dir"))],
2669 app_state.clone(),
2670 workspace::OpenOptions::default(),
2671 cx,
2672 )
2673 })
2674 .await
2675 .unwrap();
2676 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2677
2678 cx.update(|cx| {
2679 open_paths(
2680 &[PathBuf::from(path!("/root/a"))],
2681 app_state.clone(),
2682 workspace::OpenOptions {
2683 open_new_workspace: Some(false),
2684 ..Default::default()
2685 },
2686 cx,
2687 )
2688 })
2689 .await
2690 .unwrap();
2691 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2692
2693 cx.update(|cx| {
2694 open_paths(
2695 &[PathBuf::from(path!("/root/dir/c"))],
2696 app_state.clone(),
2697 workspace::OpenOptions {
2698 open_new_workspace: Some(true),
2699 ..Default::default()
2700 },
2701 cx,
2702 )
2703 })
2704 .await
2705 .unwrap();
2706 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2707 }
2708
2709 #[gpui::test]
2710 async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
2711 let app_state = init_test(cx);
2712 app_state
2713 .fs
2714 .as_fake()
2715 .insert_tree(
2716 path!("/root"),
2717 json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
2718 )
2719 .await;
2720
2721 cx.update(|cx| {
2722 open_paths(
2723 &[PathBuf::from(path!("/root/dir1/a"))],
2724 app_state.clone(),
2725 workspace::OpenOptions::default(),
2726 cx,
2727 )
2728 })
2729 .await
2730 .unwrap();
2731 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2732 let window1 = cx.update(|cx| cx.active_window().unwrap());
2733
2734 cx.update(|cx| {
2735 open_paths(
2736 &[PathBuf::from(path!("/root/dir2/c"))],
2737 app_state.clone(),
2738 workspace::OpenOptions::default(),
2739 cx,
2740 )
2741 })
2742 .await
2743 .unwrap();
2744 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2745
2746 cx.update(|cx| {
2747 open_paths(
2748 &[PathBuf::from(path!("/root/dir2"))],
2749 app_state.clone(),
2750 workspace::OpenOptions::default(),
2751 cx,
2752 )
2753 })
2754 .await
2755 .unwrap();
2756 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2757 let window2 = cx.update(|cx| cx.active_window().unwrap());
2758 assert!(window1 != window2);
2759 cx.update_window(window1, |_, window, _| window.activate_window())
2760 .unwrap();
2761
2762 cx.update(|cx| {
2763 open_paths(
2764 &[PathBuf::from(path!("/root/dir2/c"))],
2765 app_state.clone(),
2766 workspace::OpenOptions::default(),
2767 cx,
2768 )
2769 })
2770 .await
2771 .unwrap();
2772 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2773 // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
2774 assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
2775 }
2776
2777 #[gpui::test]
2778 async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
2779 let executor = cx.executor();
2780 let app_state = init_test(cx);
2781
2782 cx.update(|cx| {
2783 SettingsStore::update_global(cx, |store, cx| {
2784 store.update_user_settings(cx, |settings| {
2785 settings
2786 .session
2787 .get_or_insert_default()
2788 .restore_unsaved_buffers = Some(false)
2789 });
2790 });
2791 });
2792
2793 app_state
2794 .fs
2795 .as_fake()
2796 .insert_tree(path!("/root"), json!({"a": "hey"}))
2797 .await;
2798
2799 cx.update(|cx| {
2800 open_paths(
2801 &[PathBuf::from(path!("/root/a"))],
2802 app_state.clone(),
2803 workspace::OpenOptions::default(),
2804 cx,
2805 )
2806 })
2807 .await
2808 .unwrap();
2809 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2810
2811 // When opening the workspace, the window is not in a edited state.
2812 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2813
2814 let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2815 cx.update(|cx| window.read(cx).unwrap().is_edited())
2816 };
2817 let pane = window
2818 .read_with(cx, |workspace, _| workspace.active_pane().clone())
2819 .unwrap();
2820 let editor = window
2821 .read_with(cx, |workspace, cx| {
2822 workspace
2823 .active_item(cx)
2824 .unwrap()
2825 .downcast::<Editor>()
2826 .unwrap()
2827 })
2828 .unwrap();
2829
2830 assert!(!window_is_edited(window, cx));
2831
2832 // Editing a buffer marks the window as edited.
2833 window
2834 .update(cx, |_, window, cx| {
2835 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2836 })
2837 .unwrap();
2838
2839 assert!(window_is_edited(window, cx));
2840
2841 // Undoing the edit restores the window's edited state.
2842 window
2843 .update(cx, |_, window, cx| {
2844 editor.update(cx, |editor, cx| {
2845 editor.undo(&Default::default(), window, cx)
2846 });
2847 })
2848 .unwrap();
2849 assert!(!window_is_edited(window, cx));
2850
2851 // Redoing the edit marks the window as edited again.
2852 window
2853 .update(cx, |_, window, cx| {
2854 editor.update(cx, |editor, cx| {
2855 editor.redo(&Default::default(), window, cx)
2856 });
2857 })
2858 .unwrap();
2859 assert!(window_is_edited(window, cx));
2860 let weak = editor.downgrade();
2861
2862 // Closing the item restores the window's edited state.
2863 let close = window
2864 .update(cx, |_, window, cx| {
2865 pane.update(cx, |pane, cx| {
2866 drop(editor);
2867 pane.close_active_item(&Default::default(), window, cx)
2868 })
2869 })
2870 .unwrap();
2871 executor.run_until_parked();
2872
2873 cx.simulate_prompt_answer("Don't Save");
2874 close.await.unwrap();
2875
2876 // Advance the clock to ensure that the item has been serialized and dropped from the queue
2877 cx.executor().advance_clock(Duration::from_secs(1));
2878
2879 weak.assert_released();
2880 assert!(!window_is_edited(window, cx));
2881 // Opening the buffer again doesn't impact the window's edited state.
2882 cx.update(|cx| {
2883 open_paths(
2884 &[PathBuf::from(path!("/root/a"))],
2885 app_state,
2886 workspace::OpenOptions::default(),
2887 cx,
2888 )
2889 })
2890 .await
2891 .unwrap();
2892 executor.run_until_parked();
2893
2894 window
2895 .update(cx, |workspace, _, cx| {
2896 let editor = workspace
2897 .active_item(cx)
2898 .unwrap()
2899 .downcast::<Editor>()
2900 .unwrap();
2901
2902 editor.update(cx, |editor, cx| {
2903 assert_eq!(editor.text(cx), "hey");
2904 });
2905 })
2906 .unwrap();
2907
2908 let editor = window
2909 .read_with(cx, |workspace, cx| {
2910 workspace
2911 .active_item(cx)
2912 .unwrap()
2913 .downcast::<Editor>()
2914 .unwrap()
2915 })
2916 .unwrap();
2917 assert!(!window_is_edited(window, cx));
2918
2919 // Editing the buffer marks the window as edited.
2920 window
2921 .update(cx, |_, window, cx| {
2922 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2923 })
2924 .unwrap();
2925 executor.run_until_parked();
2926 assert!(window_is_edited(window, cx));
2927
2928 // Ensure closing the window via the mouse gets preempted due to the
2929 // buffer having unsaved changes.
2930 assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2931 executor.run_until_parked();
2932 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2933
2934 // The window is successfully closed after the user dismisses the prompt.
2935 cx.simulate_prompt_answer("Don't Save");
2936 executor.run_until_parked();
2937 assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2938 }
2939
2940 #[gpui::test]
2941 async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
2942 let app_state = init_test(cx);
2943 app_state
2944 .fs
2945 .as_fake()
2946 .insert_tree(path!("/root"), json!({"a": "hey"}))
2947 .await;
2948
2949 cx.update(|cx| {
2950 open_paths(
2951 &[PathBuf::from(path!("/root/a"))],
2952 app_state.clone(),
2953 workspace::OpenOptions::default(),
2954 cx,
2955 )
2956 })
2957 .await
2958 .unwrap();
2959
2960 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2961
2962 // When opening the workspace, the window is not in a edited state.
2963 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2964
2965 let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2966 cx.update(|cx| window.read(cx).unwrap().is_edited())
2967 };
2968
2969 let editor = window
2970 .read_with(cx, |workspace, cx| {
2971 workspace
2972 .active_item(cx)
2973 .unwrap()
2974 .downcast::<Editor>()
2975 .unwrap()
2976 })
2977 .unwrap();
2978
2979 assert!(!window_is_edited(window, cx));
2980
2981 // Editing a buffer marks the window as edited.
2982 window
2983 .update(cx, |_, window, cx| {
2984 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2985 })
2986 .unwrap();
2987
2988 assert!(window_is_edited(window, cx));
2989 cx.run_until_parked();
2990
2991 // Advance the clock to make sure the workspace is serialized
2992 cx.executor().advance_clock(Duration::from_secs(1));
2993
2994 // When closing the window, no prompt shows up and the window is closed.
2995 // buffer having unsaved changes.
2996 assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2997 cx.run_until_parked();
2998 assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2999
3000 // When we now reopen the window, the edited state and the edited buffer are back
3001 cx.update(|cx| {
3002 open_paths(
3003 &[PathBuf::from(path!("/root/a"))],
3004 app_state.clone(),
3005 workspace::OpenOptions::default(),
3006 cx,
3007 )
3008 })
3009 .await
3010 .unwrap();
3011
3012 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3013 assert!(cx.update(|cx| cx.active_window().is_some()));
3014
3015 cx.run_until_parked();
3016
3017 // When opening the workspace, the window is not in a edited state.
3018 let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
3019 assert!(window_is_edited(window, cx));
3020
3021 window
3022 .update(cx, |workspace, _, cx| {
3023 let editor = workspace
3024 .active_item(cx)
3025 .unwrap()
3026 .downcast::<editor::Editor>()
3027 .unwrap();
3028 editor.update(cx, |editor, cx| {
3029 assert_eq!(editor.text(cx), "EDIThey");
3030 assert!(editor.is_dirty(cx));
3031 });
3032
3033 editor
3034 })
3035 .unwrap();
3036 }
3037
3038 #[gpui::test]
3039 async fn test_new_empty_workspace(cx: &mut TestAppContext) {
3040 let app_state = init_test(cx);
3041 cx.update(|cx| {
3042 open_new(
3043 Default::default(),
3044 app_state.clone(),
3045 cx,
3046 |workspace, window, cx| {
3047 Editor::new_file(workspace, &Default::default(), window, cx)
3048 },
3049 )
3050 })
3051 .await
3052 .unwrap();
3053 cx.run_until_parked();
3054
3055 let workspace = cx
3056 .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
3057 .unwrap();
3058
3059 let editor = workspace
3060 .update(cx, |workspace, _, cx| {
3061 let editor = workspace
3062 .active_item(cx)
3063 .unwrap()
3064 .downcast::<editor::Editor>()
3065 .unwrap();
3066 editor.update(cx, |editor, cx| {
3067 assert!(editor.text(cx).is_empty());
3068 assert!(!editor.is_dirty(cx));
3069 });
3070
3071 editor
3072 })
3073 .unwrap();
3074
3075 let save_task = workspace
3076 .update(cx, |workspace, window, cx| {
3077 workspace.save_active_item(SaveIntent::Save, window, cx)
3078 })
3079 .unwrap();
3080 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3081 cx.background_executor.run_until_parked();
3082 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
3083 save_task.await.unwrap();
3084 workspace
3085 .update(cx, |_, _, cx| {
3086 editor.update(cx, |editor, cx| {
3087 assert!(!editor.is_dirty(cx));
3088 assert_eq!(editor.title(cx), "the-new-name");
3089 });
3090 })
3091 .unwrap();
3092 }
3093
3094 #[gpui::test]
3095 async fn test_open_entry(cx: &mut TestAppContext) {
3096 let app_state = init_test(cx);
3097 app_state
3098 .fs
3099 .as_fake()
3100 .insert_tree(
3101 path!("/root"),
3102 json!({
3103 "a": {
3104 "file1": "contents 1",
3105 "file2": "contents 2",
3106 "file3": "contents 3",
3107 },
3108 }),
3109 )
3110 .await;
3111
3112 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3113 project.update(cx, |project, _cx| {
3114 project.languages().add(markdown_language())
3115 });
3116 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3117 let workspace = window.root(cx).unwrap();
3118
3119 let entries = cx.read(|cx| workspace.file_project_paths(cx));
3120 let file1 = entries[0].clone();
3121 let file2 = entries[1].clone();
3122 let file3 = entries[2].clone();
3123
3124 // Open the first entry
3125 let entry_1 = window
3126 .update(cx, |w, window, cx| {
3127 w.open_path(file1.clone(), None, true, window, cx)
3128 })
3129 .unwrap()
3130 .await
3131 .unwrap();
3132 cx.read(|cx| {
3133 let pane = workspace.read(cx).active_pane().read(cx);
3134 assert_eq!(
3135 pane.active_item().unwrap().project_path(cx),
3136 Some(file1.clone())
3137 );
3138 assert_eq!(pane.items_len(), 1);
3139 });
3140
3141 // Open the second entry
3142 window
3143 .update(cx, |w, window, cx| {
3144 w.open_path(file2.clone(), None, true, window, cx)
3145 })
3146 .unwrap()
3147 .await
3148 .unwrap();
3149 cx.read(|cx| {
3150 let pane = workspace.read(cx).active_pane().read(cx);
3151 assert_eq!(
3152 pane.active_item().unwrap().project_path(cx),
3153 Some(file2.clone())
3154 );
3155 assert_eq!(pane.items_len(), 2);
3156 });
3157
3158 // Open the first entry again. The existing pane item is activated.
3159 let entry_1b = window
3160 .update(cx, |w, window, cx| {
3161 w.open_path(file1.clone(), None, true, window, cx)
3162 })
3163 .unwrap()
3164 .await
3165 .unwrap();
3166 assert_eq!(entry_1.item_id(), entry_1b.item_id());
3167
3168 cx.read(|cx| {
3169 let pane = workspace.read(cx).active_pane().read(cx);
3170 assert_eq!(
3171 pane.active_item().unwrap().project_path(cx),
3172 Some(file1.clone())
3173 );
3174 assert_eq!(pane.items_len(), 2);
3175 });
3176
3177 // Split the pane with the first entry, then open the second entry again.
3178 let (task1, task2) = window
3179 .update(cx, |w, window, cx| {
3180 (
3181 w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx),
3182 w.open_path(file2.clone(), None, true, window, cx),
3183 )
3184 })
3185 .unwrap();
3186 task1.await.unwrap();
3187 task2.await.unwrap();
3188
3189 window
3190 .read_with(cx, |w, cx| {
3191 assert_eq!(
3192 w.active_pane()
3193 .read(cx)
3194 .active_item()
3195 .unwrap()
3196 .project_path(cx),
3197 Some(file2.clone())
3198 );
3199 })
3200 .unwrap();
3201
3202 // Open the third entry twice concurrently. Only one pane item is added.
3203 let (t1, t2) = window
3204 .update(cx, |w, window, cx| {
3205 (
3206 w.open_path(file3.clone(), None, true, window, cx),
3207 w.open_path(file3.clone(), None, true, window, cx),
3208 )
3209 })
3210 .unwrap();
3211 t1.await.unwrap();
3212 t2.await.unwrap();
3213 cx.read(|cx| {
3214 let pane = workspace.read(cx).active_pane().read(cx);
3215 assert_eq!(
3216 pane.active_item().unwrap().project_path(cx),
3217 Some(file3.clone())
3218 );
3219 let pane_entries = pane
3220 .items()
3221 .map(|i| i.project_path(cx).unwrap())
3222 .collect::<Vec<_>>();
3223 assert_eq!(pane_entries, &[file1, file2, file3]);
3224 });
3225 }
3226
3227 #[gpui::test]
3228 async fn test_open_paths(cx: &mut TestAppContext) {
3229 let app_state = init_test(cx);
3230
3231 app_state
3232 .fs
3233 .as_fake()
3234 .insert_tree(
3235 path!("/"),
3236 json!({
3237 "dir1": {
3238 "a.txt": ""
3239 },
3240 "dir2": {
3241 "b.txt": ""
3242 },
3243 "dir3": {
3244 "c.txt": ""
3245 },
3246 "d.txt": ""
3247 }),
3248 )
3249 .await;
3250
3251 cx.update(|cx| {
3252 open_paths(
3253 &[PathBuf::from(path!("/dir1/"))],
3254 app_state,
3255 workspace::OpenOptions::default(),
3256 cx,
3257 )
3258 })
3259 .await
3260 .unwrap();
3261 cx.run_until_parked();
3262 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3263 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
3264 let workspace = window.root(cx).unwrap();
3265
3266 #[track_caller]
3267 fn assert_project_panel_selection(
3268 workspace: &Workspace,
3269 expected_worktree_path: &Path,
3270 expected_entry_path: &RelPath,
3271 cx: &App,
3272 ) {
3273 let project_panel = [
3274 workspace.left_dock().read(cx).panel::<ProjectPanel>(),
3275 workspace.right_dock().read(cx).panel::<ProjectPanel>(),
3276 workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
3277 ]
3278 .into_iter()
3279 .find_map(std::convert::identity)
3280 .expect("found no project panels")
3281 .read(cx);
3282 let (selected_worktree, selected_entry) = project_panel
3283 .selected_entry(cx)
3284 .expect("project panel should have a selected entry");
3285 assert_eq!(
3286 selected_worktree.abs_path().as_ref(),
3287 expected_worktree_path,
3288 "Unexpected project panel selected worktree path"
3289 );
3290 assert_eq!(
3291 selected_entry.path.as_ref(),
3292 expected_entry_path,
3293 "Unexpected project panel selected entry path"
3294 );
3295 }
3296
3297 // Open a file within an existing worktree.
3298 window
3299 .update(cx, |workspace, window, cx| {
3300 workspace.open_paths(
3301 vec![path!("/dir1/a.txt").into()],
3302 OpenOptions {
3303 visible: Some(OpenVisible::All),
3304 ..Default::default()
3305 },
3306 None,
3307 window,
3308 cx,
3309 )
3310 })
3311 .unwrap()
3312 .await;
3313 cx.run_until_parked();
3314 cx.read(|cx| {
3315 let workspace = workspace.read(cx);
3316 assert_project_panel_selection(
3317 workspace,
3318 Path::new(path!("/dir1")),
3319 rel_path("a.txt"),
3320 cx,
3321 );
3322 assert_eq!(
3323 workspace
3324 .active_pane()
3325 .read(cx)
3326 .active_item()
3327 .unwrap()
3328 .act_as::<Editor>(cx)
3329 .unwrap()
3330 .read(cx)
3331 .title(cx),
3332 "a.txt"
3333 );
3334 });
3335
3336 // Open a file outside of any existing worktree.
3337 window
3338 .update(cx, |workspace, window, cx| {
3339 workspace.open_paths(
3340 vec![path!("/dir2/b.txt").into()],
3341 OpenOptions {
3342 visible: Some(OpenVisible::All),
3343 ..Default::default()
3344 },
3345 None,
3346 window,
3347 cx,
3348 )
3349 })
3350 .unwrap()
3351 .await;
3352 cx.run_until_parked();
3353 cx.read(|cx| {
3354 let workspace = workspace.read(cx);
3355 assert_project_panel_selection(
3356 workspace,
3357 Path::new(path!("/dir2/b.txt")),
3358 rel_path(""),
3359 cx,
3360 );
3361 let worktree_roots = workspace
3362 .worktrees(cx)
3363 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3364 .collect::<HashSet<_>>();
3365 assert_eq!(
3366 worktree_roots,
3367 vec![path!("/dir1"), path!("/dir2/b.txt")]
3368 .into_iter()
3369 .map(Path::new)
3370 .collect(),
3371 );
3372 assert_eq!(
3373 workspace
3374 .active_pane()
3375 .read(cx)
3376 .active_item()
3377 .unwrap()
3378 .act_as::<Editor>(cx)
3379 .unwrap()
3380 .read(cx)
3381 .title(cx),
3382 "b.txt"
3383 );
3384 });
3385
3386 // Ensure opening a directory and one of its children only adds one worktree.
3387 window
3388 .update(cx, |workspace, window, cx| {
3389 workspace.open_paths(
3390 vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
3391 OpenOptions {
3392 visible: Some(OpenVisible::All),
3393 ..Default::default()
3394 },
3395 None,
3396 window,
3397 cx,
3398 )
3399 })
3400 .unwrap()
3401 .await;
3402 cx.run_until_parked();
3403 cx.read(|cx| {
3404 let workspace = workspace.read(cx);
3405 assert_project_panel_selection(
3406 workspace,
3407 Path::new(path!("/dir3")),
3408 rel_path("c.txt"),
3409 cx,
3410 );
3411 let worktree_roots = workspace
3412 .worktrees(cx)
3413 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3414 .collect::<HashSet<_>>();
3415 assert_eq!(
3416 worktree_roots,
3417 vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
3418 .into_iter()
3419 .map(Path::new)
3420 .collect(),
3421 );
3422 assert_eq!(
3423 workspace
3424 .active_pane()
3425 .read(cx)
3426 .active_item()
3427 .unwrap()
3428 .act_as::<Editor>(cx)
3429 .unwrap()
3430 .read(cx)
3431 .title(cx),
3432 "c.txt"
3433 );
3434 });
3435
3436 // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
3437 window
3438 .update(cx, |workspace, window, cx| {
3439 workspace.open_paths(
3440 vec![path!("/d.txt").into()],
3441 OpenOptions {
3442 visible: Some(OpenVisible::None),
3443 ..Default::default()
3444 },
3445 None,
3446 window,
3447 cx,
3448 )
3449 })
3450 .unwrap()
3451 .await;
3452 cx.run_until_parked();
3453 cx.read(|cx| {
3454 let workspace = workspace.read(cx);
3455 assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx);
3456 let worktree_roots = workspace
3457 .worktrees(cx)
3458 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3459 .collect::<HashSet<_>>();
3460 assert_eq!(
3461 worktree_roots,
3462 vec![
3463 path!("/dir1"),
3464 path!("/dir2/b.txt"),
3465 path!("/dir3"),
3466 path!("/d.txt")
3467 ]
3468 .into_iter()
3469 .map(Path::new)
3470 .collect(),
3471 );
3472
3473 let visible_worktree_roots = workspace
3474 .visible_worktrees(cx)
3475 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3476 .collect::<HashSet<_>>();
3477 assert_eq!(
3478 visible_worktree_roots,
3479 vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
3480 .into_iter()
3481 .map(Path::new)
3482 .collect(),
3483 );
3484
3485 assert_eq!(
3486 workspace
3487 .active_pane()
3488 .read(cx)
3489 .active_item()
3490 .unwrap()
3491 .act_as::<Editor>(cx)
3492 .unwrap()
3493 .read(cx)
3494 .title(cx),
3495 "d.txt"
3496 );
3497 });
3498 }
3499
3500 #[gpui::test]
3501 async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
3502 let app_state = init_test(cx);
3503 cx.update(|cx| {
3504 cx.update_global::<SettingsStore, _>(|store, cx| {
3505 store.update_user_settings(cx, |project_settings| {
3506 project_settings.project.worktree.file_scan_exclusions =
3507 Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3508 });
3509 });
3510 });
3511 app_state
3512 .fs
3513 .as_fake()
3514 .insert_tree(
3515 path!("/root"),
3516 json!({
3517 ".gitignore": "ignored_dir\n",
3518 ".git": {
3519 "HEAD": "ref: refs/heads/main",
3520 },
3521 "regular_dir": {
3522 "file": "regular file contents",
3523 },
3524 "ignored_dir": {
3525 "ignored_subdir": {
3526 "file": "ignored subfile contents",
3527 },
3528 "file": "ignored file contents",
3529 },
3530 "excluded_dir": {
3531 "file": "excluded file contents",
3532 "ignored_subdir": {
3533 "file": "ignored subfile contents",
3534 },
3535 },
3536 }),
3537 )
3538 .await;
3539
3540 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3541 project.update(cx, |project, _cx| {
3542 project.languages().add(markdown_language())
3543 });
3544 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3545 let workspace = window.root(cx).unwrap();
3546
3547 let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
3548 let paths_to_open = [
3549 PathBuf::from(path!("/root/excluded_dir/file")),
3550 PathBuf::from(path!("/root/.git/HEAD")),
3551 PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
3552 ];
3553 let (opened_workspace, new_items) = cx
3554 .update(|cx| {
3555 workspace::open_paths(
3556 &paths_to_open,
3557 app_state,
3558 workspace::OpenOptions::default(),
3559 cx,
3560 )
3561 })
3562 .await
3563 .unwrap();
3564
3565 assert_eq!(
3566 opened_workspace.root(cx).unwrap().entity_id(),
3567 workspace.entity_id(),
3568 "Excluded files in subfolders of a workspace root should be opened in the workspace"
3569 );
3570 let mut opened_paths = cx.read(|cx| {
3571 assert_eq!(
3572 new_items.len(),
3573 paths_to_open.len(),
3574 "Expect to get the same number of opened items as submitted paths to open"
3575 );
3576 new_items
3577 .iter()
3578 .zip(paths_to_open.iter())
3579 .map(|(i, path)| {
3580 match i {
3581 Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)),
3582 Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
3583 None => None,
3584 }
3585 .flatten()
3586 })
3587 .collect::<Vec<_>>()
3588 });
3589 opened_paths.sort();
3590 assert_eq!(
3591 opened_paths,
3592 vec![
3593 None,
3594 Some(rel_path(".git/HEAD").into()),
3595 Some(rel_path("excluded_dir/file").into()),
3596 ],
3597 "Excluded files should get opened, excluded dir should not get opened"
3598 );
3599
3600 let entries = cx.read(|cx| workspace.file_project_paths(cx));
3601 assert_eq!(
3602 initial_entries, entries,
3603 "Workspace entries should not change after opening excluded files and directories paths"
3604 );
3605
3606 cx.read(|cx| {
3607 let pane = workspace.read(cx).active_pane().read(cx);
3608 let mut opened_buffer_paths = pane
3609 .items()
3610 .map(|i| {
3611 i.project_path(cx)
3612 .expect("all excluded files that got open should have a path")
3613 .path
3614 })
3615 .collect::<Vec<_>>();
3616 opened_buffer_paths.sort();
3617 assert_eq!(
3618 opened_buffer_paths,
3619 vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()],
3620 "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
3621 );
3622 });
3623 }
3624
3625 #[gpui::test]
3626 async fn test_save_conflicting_item(cx: &mut TestAppContext) {
3627 let app_state = init_test(cx);
3628 app_state
3629 .fs
3630 .as_fake()
3631 .insert_tree(path!("/root"), json!({ "a.txt": "" }))
3632 .await;
3633
3634 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3635 project.update(cx, |project, _cx| {
3636 project.languages().add(markdown_language())
3637 });
3638 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3639 let workspace = window.root(cx).unwrap();
3640
3641 // Open a file within an existing worktree.
3642 window
3643 .update(cx, |workspace, window, cx| {
3644 workspace.open_paths(
3645 vec![PathBuf::from(path!("/root/a.txt"))],
3646 OpenOptions {
3647 visible: Some(OpenVisible::All),
3648 ..Default::default()
3649 },
3650 None,
3651 window,
3652 cx,
3653 )
3654 })
3655 .unwrap()
3656 .await;
3657 let editor = cx.read(|cx| {
3658 let pane = workspace.read(cx).active_pane().read(cx);
3659 let item = pane.active_item().unwrap();
3660 item.downcast::<Editor>().unwrap()
3661 });
3662
3663 window
3664 .update(cx, |_, window, cx| {
3665 editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
3666 })
3667 .unwrap();
3668
3669 app_state
3670 .fs
3671 .as_fake()
3672 .insert_file(path!("/root/a.txt"), b"changed".to_vec())
3673 .await;
3674
3675 cx.run_until_parked();
3676 cx.read(|cx| assert!(editor.is_dirty(cx)));
3677 cx.read(|cx| assert!(editor.has_conflict(cx)));
3678
3679 let save_task = window
3680 .update(cx, |workspace, window, cx| {
3681 workspace.save_active_item(SaveIntent::Save, window, cx)
3682 })
3683 .unwrap();
3684 cx.background_executor.run_until_parked();
3685 cx.simulate_prompt_answer("Overwrite");
3686 save_task.await.unwrap();
3687 window
3688 .update(cx, |_, _, cx| {
3689 editor.update(cx, |editor, cx| {
3690 assert!(!editor.is_dirty(cx));
3691 assert!(!editor.has_conflict(cx));
3692 });
3693 })
3694 .unwrap();
3695 }
3696
3697 #[gpui::test]
3698 async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
3699 let app_state = init_test(cx);
3700 app_state
3701 .fs
3702 .create_dir(Path::new(path!("/root")))
3703 .await
3704 .unwrap();
3705
3706 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3707 project.update(cx, |project, _| {
3708 project.languages().add(markdown_language());
3709 project.languages().add(rust_lang());
3710 });
3711 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3712 let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
3713
3714 // Create a new untitled buffer
3715 cx.dispatch_action(window.into(), NewFile);
3716 let editor = window
3717 .read_with(cx, |workspace, cx| {
3718 workspace
3719 .active_item(cx)
3720 .unwrap()
3721 .downcast::<Editor>()
3722 .unwrap()
3723 })
3724 .unwrap();
3725
3726 window
3727 .update(cx, |_, window, cx| {
3728 editor.update(cx, |editor, cx| {
3729 assert!(!editor.is_dirty(cx));
3730 assert_eq!(editor.title(cx), "untitled");
3731 assert!(Arc::ptr_eq(
3732 &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3733 &languages::PLAIN_TEXT
3734 ));
3735 editor.handle_input("hi", window, cx);
3736 assert!(editor.is_dirty(cx));
3737 });
3738 })
3739 .unwrap();
3740
3741 // Save the buffer. This prompts for a filename.
3742 let save_task = window
3743 .update(cx, |workspace, window, cx| {
3744 workspace.save_active_item(SaveIntent::Save, window, cx)
3745 })
3746 .unwrap();
3747 cx.background_executor.run_until_parked();
3748 cx.simulate_new_path_selection(|parent_dir| {
3749 assert_eq!(parent_dir, Path::new(path!("/root")));
3750 Some(parent_dir.join("the-new-name.rs"))
3751 });
3752 cx.read(|cx| {
3753 assert!(editor.is_dirty(cx));
3754 assert_eq!(editor.read(cx).title(cx), "hi");
3755 });
3756
3757 // When the save completes, the buffer's title is updated and the language is assigned based
3758 // on the path.
3759 save_task.await.unwrap();
3760 window
3761 .update(cx, |_, _, cx| {
3762 editor.update(cx, |editor, cx| {
3763 assert!(!editor.is_dirty(cx));
3764 assert_eq!(editor.title(cx), "the-new-name.rs");
3765 assert_eq!(
3766 editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3767 "Rust".into()
3768 );
3769 });
3770 })
3771 .unwrap();
3772
3773 // Edit the file and save it again. This time, there is no filename prompt.
3774 window
3775 .update(cx, |_, window, cx| {
3776 editor.update(cx, |editor, cx| {
3777 editor.handle_input(" there", window, cx);
3778 assert!(editor.is_dirty(cx));
3779 });
3780 })
3781 .unwrap();
3782
3783 let save_task = window
3784 .update(cx, |workspace, window, cx| {
3785 workspace.save_active_item(SaveIntent::Save, window, cx)
3786 })
3787 .unwrap();
3788 save_task.await.unwrap();
3789
3790 assert!(!cx.did_prompt_for_new_path());
3791 window
3792 .update(cx, |_, _, cx| {
3793 editor.update(cx, |editor, cx| {
3794 assert!(!editor.is_dirty(cx));
3795 assert_eq!(editor.title(cx), "the-new-name.rs")
3796 });
3797 })
3798 .unwrap();
3799
3800 // Open the same newly-created file in another pane item. The new editor should reuse
3801 // the same buffer.
3802 cx.dispatch_action(window.into(), NewFile);
3803 window
3804 .update(cx, |workspace, window, cx| {
3805 workspace.split_and_clone(
3806 workspace.active_pane().clone(),
3807 SplitDirection::Right,
3808 window,
3809 cx,
3810 )
3811 })
3812 .unwrap()
3813 .await
3814 .unwrap();
3815 window
3816 .update(cx, |workspace, window, cx| {
3817 workspace.open_path(
3818 (worktree.read(cx).id(), rel_path("the-new-name.rs")),
3819 None,
3820 true,
3821 window,
3822 cx,
3823 )
3824 })
3825 .unwrap()
3826 .await
3827 .unwrap();
3828 let editor2 = window
3829 .update(cx, |workspace, _, cx| {
3830 workspace
3831 .active_item(cx)
3832 .unwrap()
3833 .downcast::<Editor>()
3834 .unwrap()
3835 })
3836 .unwrap();
3837 cx.read(|cx| {
3838 assert_eq!(
3839 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
3840 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
3841 );
3842 })
3843 }
3844
3845 #[gpui::test]
3846 async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
3847 let app_state = init_test(cx);
3848 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3849
3850 let project = Project::test(app_state.fs.clone(), [], cx).await;
3851 project.update(cx, |project, _| {
3852 project.languages().add(rust_lang());
3853 project.languages().add(markdown_language());
3854 });
3855 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3856
3857 // Create a new untitled buffer
3858 cx.dispatch_action(window.into(), NewFile);
3859 let editor = window
3860 .read_with(cx, |workspace, cx| {
3861 workspace
3862 .active_item(cx)
3863 .unwrap()
3864 .downcast::<Editor>()
3865 .unwrap()
3866 })
3867 .unwrap();
3868 window
3869 .update(cx, |_, window, cx| {
3870 editor.update(cx, |editor, cx| {
3871 assert!(Arc::ptr_eq(
3872 &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3873 &languages::PLAIN_TEXT
3874 ));
3875 editor.handle_input("hi", window, cx);
3876 assert!(editor.is_dirty(cx));
3877 });
3878 })
3879 .unwrap();
3880
3881 // Save the buffer. This prompts for a filename.
3882 let save_task = window
3883 .update(cx, |workspace, window, cx| {
3884 workspace.save_active_item(SaveIntent::Save, window, cx)
3885 })
3886 .unwrap();
3887 cx.background_executor.run_until_parked();
3888 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
3889 save_task.await.unwrap();
3890 // The buffer is not dirty anymore and the language is assigned based on the path.
3891 window
3892 .update(cx, |_, _, cx| {
3893 editor.update(cx, |editor, cx| {
3894 assert!(!editor.is_dirty(cx));
3895 assert_eq!(
3896 editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3897 "Rust".into()
3898 )
3899 });
3900 })
3901 .unwrap();
3902 }
3903
3904 #[gpui::test]
3905 async fn test_pane_actions(cx: &mut TestAppContext) {
3906 let app_state = init_test(cx);
3907 app_state
3908 .fs
3909 .as_fake()
3910 .insert_tree(
3911 path!("/root"),
3912 json!({
3913 "a": {
3914 "file1": "contents 1",
3915 "file2": "contents 2",
3916 "file3": "contents 3",
3917 },
3918 }),
3919 )
3920 .await;
3921
3922 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3923 project.update(cx, |project, _cx| {
3924 project.languages().add(markdown_language())
3925 });
3926 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3927 let workspace = window.root(cx).unwrap();
3928
3929 let entries = cx.read(|cx| workspace.file_project_paths(cx));
3930 let file1 = entries[0].clone();
3931
3932 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
3933
3934 window
3935 .update(cx, |w, window, cx| {
3936 w.open_path(file1.clone(), None, true, window, cx)
3937 })
3938 .unwrap()
3939 .await
3940 .unwrap();
3941
3942 let (editor_1, buffer) = window
3943 .update(cx, |_, window, cx| {
3944 pane_1.update(cx, |pane_1, cx| {
3945 let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
3946 assert_eq!(editor.project_path(cx), Some(file1.clone()));
3947 let buffer = editor.update(cx, |editor, cx| {
3948 editor.insert("dirt", window, cx);
3949 editor.buffer().downgrade()
3950 });
3951 (editor.downgrade(), buffer)
3952 })
3953 })
3954 .unwrap();
3955
3956 cx.dispatch_action(window.into(), pane::SplitRight);
3957 let editor_2 = cx.update(|cx| {
3958 let pane_2 = workspace.read(cx).active_pane().clone();
3959 assert_ne!(pane_1, pane_2);
3960
3961 let pane2_item = pane_2.read(cx).active_item().unwrap();
3962 assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
3963
3964 pane2_item.downcast::<Editor>().unwrap().downgrade()
3965 });
3966 cx.dispatch_action(
3967 window.into(),
3968 workspace::CloseActiveItem {
3969 save_intent: None,
3970 close_pinned: false,
3971 },
3972 );
3973
3974 cx.background_executor.run_until_parked();
3975 window
3976 .read_with(cx, |workspace, _| {
3977 assert_eq!(workspace.panes().len(), 1);
3978 assert_eq!(workspace.active_pane(), &pane_1);
3979 })
3980 .unwrap();
3981
3982 cx.dispatch_action(
3983 window.into(),
3984 workspace::CloseActiveItem {
3985 save_intent: None,
3986 close_pinned: false,
3987 },
3988 );
3989 cx.background_executor.run_until_parked();
3990 cx.simulate_prompt_answer("Don't Save");
3991 cx.background_executor.run_until_parked();
3992
3993 window
3994 .update(cx, |workspace, _, cx| {
3995 assert_eq!(workspace.panes().len(), 1);
3996 assert!(workspace.active_item(cx).is_none());
3997 })
3998 .unwrap();
3999
4000 cx.background_executor
4001 .advance_clock(SERIALIZATION_THROTTLE_TIME);
4002 cx.update(|_| {});
4003 editor_1.assert_released();
4004 editor_2.assert_released();
4005 buffer.assert_released();
4006 }
4007
4008 #[gpui::test]
4009 async fn test_navigation(cx: &mut TestAppContext) {
4010 let app_state = init_test(cx);
4011 app_state
4012 .fs
4013 .as_fake()
4014 .insert_tree(
4015 path!("/root"),
4016 json!({
4017 "a": {
4018 "file1": "contents 1\n".repeat(20),
4019 "file2": "contents 2\n".repeat(20),
4020 "file3": "contents 3\n".repeat(20),
4021 },
4022 }),
4023 )
4024 .await;
4025
4026 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4027 project.update(cx, |project, _cx| {
4028 project.languages().add(markdown_language())
4029 });
4030 let workspace =
4031 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4032 let pane = workspace
4033 .read_with(cx, |workspace, _| workspace.active_pane().clone())
4034 .unwrap();
4035
4036 let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
4037 let file1 = entries[0].clone();
4038 let file2 = entries[1].clone();
4039 let file3 = entries[2].clone();
4040
4041 let editor1 = workspace
4042 .update(cx, |w, window, cx| {
4043 w.open_path(file1.clone(), None, true, window, cx)
4044 })
4045 .unwrap()
4046 .await
4047 .unwrap()
4048 .downcast::<Editor>()
4049 .unwrap();
4050 workspace
4051 .update(cx, |_, window, cx| {
4052 editor1.update(cx, |editor, cx| {
4053 editor.change_selections(Default::default(), window, cx, |s| {
4054 s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
4055 ..DisplayPoint::new(DisplayRow(10), 0)])
4056 });
4057 });
4058 })
4059 .unwrap();
4060
4061 let editor2 = workspace
4062 .update(cx, |w, window, cx| {
4063 w.open_path(file2.clone(), None, true, window, cx)
4064 })
4065 .unwrap()
4066 .await
4067 .unwrap()
4068 .downcast::<Editor>()
4069 .unwrap();
4070 let editor3 = workspace
4071 .update(cx, |w, window, cx| {
4072 w.open_path(file3.clone(), None, true, window, cx)
4073 })
4074 .unwrap()
4075 .await
4076 .unwrap()
4077 .downcast::<Editor>()
4078 .unwrap();
4079
4080 workspace
4081 .update(cx, |_, window, cx| {
4082 editor3.update(cx, |editor, cx| {
4083 editor.change_selections(Default::default(), window, cx, |s| {
4084 s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
4085 ..DisplayPoint::new(DisplayRow(12), 0)])
4086 });
4087 editor.newline(&Default::default(), window, cx);
4088 editor.newline(&Default::default(), window, cx);
4089 editor.move_down(&Default::default(), window, cx);
4090 editor.move_down(&Default::default(), window, cx);
4091 editor.save(
4092 SaveOptions {
4093 format: true,
4094 autosave: false,
4095 },
4096 project.clone(),
4097 window,
4098 cx,
4099 )
4100 })
4101 })
4102 .unwrap()
4103 .await
4104 .unwrap();
4105 workspace
4106 .update(cx, |_, window, cx| {
4107 editor3.update(cx, |editor, cx| {
4108 editor.set_scroll_position(point(0., 12.5), window, cx)
4109 });
4110 })
4111 .unwrap();
4112 assert_eq!(
4113 active_location(&workspace, cx),
4114 (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
4115 );
4116
4117 workspace
4118 .update(cx, |w, window, cx| {
4119 w.go_back(w.active_pane().downgrade(), window, cx)
4120 })
4121 .unwrap()
4122 .await
4123 .unwrap();
4124 assert_eq!(
4125 active_location(&workspace, cx),
4126 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4127 );
4128
4129 workspace
4130 .update(cx, |w, window, cx| {
4131 w.go_back(w.active_pane().downgrade(), window, cx)
4132 })
4133 .unwrap()
4134 .await
4135 .unwrap();
4136 assert_eq!(
4137 active_location(&workspace, cx),
4138 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4139 );
4140
4141 workspace
4142 .update(cx, |w, window, cx| {
4143 w.go_back(w.active_pane().downgrade(), window, cx)
4144 })
4145 .unwrap()
4146 .await
4147 .unwrap();
4148 assert_eq!(
4149 active_location(&workspace, cx),
4150 (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
4151 );
4152
4153 workspace
4154 .update(cx, |w, window, cx| {
4155 w.go_back(w.active_pane().downgrade(), window, cx)
4156 })
4157 .unwrap()
4158 .await
4159 .unwrap();
4160 assert_eq!(
4161 active_location(&workspace, cx),
4162 (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4163 );
4164
4165 // Go back one more time and ensure we don't navigate past the first item in the history.
4166 workspace
4167 .update(cx, |w, window, cx| {
4168 w.go_back(w.active_pane().downgrade(), window, cx)
4169 })
4170 .unwrap()
4171 .await
4172 .unwrap();
4173 assert_eq!(
4174 active_location(&workspace, cx),
4175 (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4176 );
4177
4178 workspace
4179 .update(cx, |w, window, cx| {
4180 w.go_forward(w.active_pane().downgrade(), window, cx)
4181 })
4182 .unwrap()
4183 .await
4184 .unwrap();
4185 assert_eq!(
4186 active_location(&workspace, cx),
4187 (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
4188 );
4189
4190 workspace
4191 .update(cx, |w, window, cx| {
4192 w.go_forward(w.active_pane().downgrade(), window, cx)
4193 })
4194 .unwrap()
4195 .await
4196 .unwrap();
4197 assert_eq!(
4198 active_location(&workspace, cx),
4199 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4200 );
4201
4202 // Go forward to an item that has been closed, ensuring it gets re-opened at the same
4203 // location.
4204 workspace
4205 .update(cx, |_, window, cx| {
4206 pane.update(cx, |pane, cx| {
4207 let editor3_id = editor3.entity_id();
4208 drop(editor3);
4209 pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
4210 })
4211 })
4212 .unwrap()
4213 .await
4214 .unwrap();
4215 workspace
4216 .update(cx, |w, window, cx| {
4217 w.go_forward(w.active_pane().downgrade(), window, cx)
4218 })
4219 .unwrap()
4220 .await
4221 .unwrap();
4222 assert_eq!(
4223 active_location(&workspace, cx),
4224 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4225 );
4226
4227 workspace
4228 .update(cx, |w, window, cx| {
4229 w.go_forward(w.active_pane().downgrade(), window, cx)
4230 })
4231 .unwrap()
4232 .await
4233 .unwrap();
4234 assert_eq!(
4235 active_location(&workspace, cx),
4236 (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
4237 );
4238
4239 workspace
4240 .update(cx, |w, window, cx| {
4241 w.go_back(w.active_pane().downgrade(), window, cx)
4242 })
4243 .unwrap()
4244 .await
4245 .unwrap();
4246 assert_eq!(
4247 active_location(&workspace, cx),
4248 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4249 );
4250
4251 // Go back to an item that has been closed and removed from disk
4252 workspace
4253 .update(cx, |_, window, cx| {
4254 pane.update(cx, |pane, cx| {
4255 let editor2_id = editor2.entity_id();
4256 drop(editor2);
4257 pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
4258 })
4259 })
4260 .unwrap()
4261 .await
4262 .unwrap();
4263 app_state
4264 .fs
4265 .remove_file(Path::new(path!("/root/a/file2")), Default::default())
4266 .await
4267 .unwrap();
4268 cx.background_executor.run_until_parked();
4269
4270 workspace
4271 .update(cx, |w, window, cx| {
4272 w.go_back(w.active_pane().downgrade(), window, cx)
4273 })
4274 .unwrap()
4275 .await
4276 .unwrap();
4277 assert_eq!(
4278 active_location(&workspace, cx),
4279 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4280 );
4281 workspace
4282 .update(cx, |w, window, cx| {
4283 w.go_forward(w.active_pane().downgrade(), window, cx)
4284 })
4285 .unwrap()
4286 .await
4287 .unwrap();
4288 assert_eq!(
4289 active_location(&workspace, cx),
4290 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4291 );
4292
4293 // Modify file to collapse multiple nav history entries into the same location.
4294 // Ensure we don't visit the same location twice when navigating.
4295 workspace
4296 .update(cx, |_, window, cx| {
4297 editor1.update(cx, |editor, cx| {
4298 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4299 s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
4300 ..DisplayPoint::new(DisplayRow(15), 0)])
4301 })
4302 });
4303 })
4304 .unwrap();
4305 for _ in 0..5 {
4306 workspace
4307 .update(cx, |_, window, cx| {
4308 editor1.update(cx, |editor, cx| {
4309 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4310 s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
4311 ..DisplayPoint::new(DisplayRow(3), 0)])
4312 });
4313 });
4314 })
4315 .unwrap();
4316
4317 workspace
4318 .update(cx, |_, window, cx| {
4319 editor1.update(cx, |editor, cx| {
4320 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4321 s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
4322 ..DisplayPoint::new(DisplayRow(13), 0)])
4323 })
4324 });
4325 })
4326 .unwrap();
4327 }
4328 workspace
4329 .update(cx, |_, window, cx| {
4330 editor1.update(cx, |editor, cx| {
4331 editor.transact(window, cx, |editor, window, cx| {
4332 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4333 s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
4334 ..DisplayPoint::new(DisplayRow(14), 0)])
4335 });
4336 editor.insert("", window, cx);
4337 })
4338 });
4339 })
4340 .unwrap();
4341
4342 workspace
4343 .update(cx, |_, window, cx| {
4344 editor1.update(cx, |editor, cx| {
4345 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4346 s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
4347 ..DisplayPoint::new(DisplayRow(1), 0)])
4348 })
4349 });
4350 })
4351 .unwrap();
4352 workspace
4353 .update(cx, |w, window, cx| {
4354 w.go_back(w.active_pane().downgrade(), window, cx)
4355 })
4356 .unwrap()
4357 .await
4358 .unwrap();
4359 assert_eq!(
4360 active_location(&workspace, cx),
4361 (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
4362 );
4363 workspace
4364 .update(cx, |w, window, cx| {
4365 w.go_back(w.active_pane().downgrade(), window, cx)
4366 })
4367 .unwrap()
4368 .await
4369 .unwrap();
4370 assert_eq!(
4371 active_location(&workspace, cx),
4372 (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
4373 );
4374
4375 fn active_location(
4376 workspace: &WindowHandle<Workspace>,
4377 cx: &mut TestAppContext,
4378 ) -> (ProjectPath, DisplayPoint, f64) {
4379 workspace
4380 .update(cx, |workspace, _, cx| {
4381 let item = workspace.active_item(cx).unwrap();
4382 let editor = item.downcast::<Editor>().unwrap();
4383 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
4384 (
4385 editor.selections.display_ranges(cx),
4386 editor.scroll_position(cx),
4387 )
4388 });
4389 (
4390 item.project_path(cx).unwrap(),
4391 selections[0].start,
4392 scroll_position.y,
4393 )
4394 })
4395 .unwrap()
4396 }
4397 }
4398
4399 #[gpui::test]
4400 async fn test_reopening_closed_items(cx: &mut TestAppContext) {
4401 let app_state = init_test(cx);
4402 app_state
4403 .fs
4404 .as_fake()
4405 .insert_tree(
4406 path!("/root"),
4407 json!({
4408 "a": {
4409 "file1": "",
4410 "file2": "",
4411 "file3": "",
4412 "file4": "",
4413 },
4414 }),
4415 )
4416 .await;
4417
4418 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4419 project.update(cx, |project, _cx| {
4420 project.languages().add(markdown_language())
4421 });
4422 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
4423 let pane = workspace
4424 .read_with(cx, |workspace, _| workspace.active_pane().clone())
4425 .unwrap();
4426
4427 let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
4428 let file1 = entries[0].clone();
4429 let file2 = entries[1].clone();
4430 let file3 = entries[2].clone();
4431 let file4 = entries[3].clone();
4432
4433 let file1_item_id = workspace
4434 .update(cx, |w, window, cx| {
4435 w.open_path(file1.clone(), None, true, window, cx)
4436 })
4437 .unwrap()
4438 .await
4439 .unwrap()
4440 .item_id();
4441 let file2_item_id = workspace
4442 .update(cx, |w, window, cx| {
4443 w.open_path(file2.clone(), None, true, window, cx)
4444 })
4445 .unwrap()
4446 .await
4447 .unwrap()
4448 .item_id();
4449 let file3_item_id = workspace
4450 .update(cx, |w, window, cx| {
4451 w.open_path(file3.clone(), None, true, window, cx)
4452 })
4453 .unwrap()
4454 .await
4455 .unwrap()
4456 .item_id();
4457 let file4_item_id = workspace
4458 .update(cx, |w, window, cx| {
4459 w.open_path(file4.clone(), None, true, window, cx)
4460 })
4461 .unwrap()
4462 .await
4463 .unwrap()
4464 .item_id();
4465 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4466
4467 // Close all the pane items in some arbitrary order.
4468 workspace
4469 .update(cx, |_, window, cx| {
4470 pane.update(cx, |pane, cx| {
4471 pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
4472 })
4473 })
4474 .unwrap()
4475 .await
4476 .unwrap();
4477 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4478
4479 workspace
4480 .update(cx, |_, window, cx| {
4481 pane.update(cx, |pane, cx| {
4482 pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx)
4483 })
4484 })
4485 .unwrap()
4486 .await
4487 .unwrap();
4488 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4489
4490 workspace
4491 .update(cx, |_, window, cx| {
4492 pane.update(cx, |pane, cx| {
4493 pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
4494 })
4495 })
4496 .unwrap()
4497 .await
4498 .unwrap();
4499 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4500 workspace
4501 .update(cx, |_, window, cx| {
4502 pane.update(cx, |pane, cx| {
4503 pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
4504 })
4505 })
4506 .unwrap()
4507 .await
4508 .unwrap();
4509
4510 assert_eq!(active_path(&workspace, cx), None);
4511
4512 // Reopen all the closed items, ensuring they are reopened in the same order
4513 // in which they were closed.
4514 workspace
4515 .update(cx, Workspace::reopen_closed_item)
4516 .unwrap()
4517 .await
4518 .unwrap();
4519 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4520
4521 workspace
4522 .update(cx, Workspace::reopen_closed_item)
4523 .unwrap()
4524 .await
4525 .unwrap();
4526 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4527
4528 workspace
4529 .update(cx, Workspace::reopen_closed_item)
4530 .unwrap()
4531 .await
4532 .unwrap();
4533 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4534
4535 workspace
4536 .update(cx, Workspace::reopen_closed_item)
4537 .unwrap()
4538 .await
4539 .unwrap();
4540 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4541
4542 // Reopening past the last closed item is a no-op.
4543 workspace
4544 .update(cx, Workspace::reopen_closed_item)
4545 .unwrap()
4546 .await
4547 .unwrap();
4548 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4549
4550 // Reopening closed items doesn't interfere with navigation history.
4551 workspace
4552 .update(cx, |workspace, window, cx| {
4553 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4554 })
4555 .unwrap()
4556 .await
4557 .unwrap();
4558 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4559
4560 workspace
4561 .update(cx, |workspace, window, cx| {
4562 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4563 })
4564 .unwrap()
4565 .await
4566 .unwrap();
4567 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4568
4569 workspace
4570 .update(cx, |workspace, window, cx| {
4571 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4572 })
4573 .unwrap()
4574 .await
4575 .unwrap();
4576 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4577
4578 workspace
4579 .update(cx, |workspace, window, cx| {
4580 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4581 })
4582 .unwrap()
4583 .await
4584 .unwrap();
4585 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4586
4587 workspace
4588 .update(cx, |workspace, window, cx| {
4589 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4590 })
4591 .unwrap()
4592 .await
4593 .unwrap();
4594 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4595
4596 workspace
4597 .update(cx, |workspace, window, cx| {
4598 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4599 })
4600 .unwrap()
4601 .await
4602 .unwrap();
4603 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4604
4605 workspace
4606 .update(cx, |workspace, window, cx| {
4607 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4608 })
4609 .unwrap()
4610 .await
4611 .unwrap();
4612 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4613
4614 workspace
4615 .update(cx, |workspace, window, cx| {
4616 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4617 })
4618 .unwrap()
4619 .await
4620 .unwrap();
4621 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4622
4623 fn active_path(
4624 workspace: &WindowHandle<Workspace>,
4625 cx: &TestAppContext,
4626 ) -> Option<ProjectPath> {
4627 workspace
4628 .read_with(cx, |workspace, cx| {
4629 let item = workspace.active_item(cx)?;
4630 item.project_path(cx)
4631 })
4632 .unwrap()
4633 }
4634 }
4635
4636 fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
4637 cx.update(|cx| {
4638 let app_state = AppState::test(cx);
4639
4640 theme::init(theme::LoadThemes::JustBase, cx);
4641 client::init(&app_state.client, cx);
4642 language::init(cx);
4643 workspace::init(app_state.clone(), cx);
4644 onboarding::init(cx);
4645 Project::init_settings(cx);
4646 app_state
4647 })
4648 }
4649
4650 actions!(test_only, [ActionA, ActionB]);
4651
4652 #[gpui::test]
4653 async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
4654 let executor = cx.executor();
4655 let app_state = init_keymap_test(cx);
4656 let project = Project::test(app_state.fs.clone(), [], cx).await;
4657 let workspace =
4658 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4659
4660 // From the Atom keymap
4661 use workspace::ActivatePreviousPane;
4662 // From the JetBrains keymap
4663 use workspace::ActivatePreviousItem;
4664
4665 app_state
4666 .fs
4667 .save(
4668 "/settings.json".as_ref(),
4669 &r#"{"base_keymap": "Atom"}"#.into(),
4670 Default::default(),
4671 )
4672 .await
4673 .unwrap();
4674
4675 app_state
4676 .fs
4677 .save(
4678 "/keymap.json".as_ref(),
4679 &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4680 Default::default(),
4681 )
4682 .await
4683 .unwrap();
4684 executor.run_until_parked();
4685 cx.update(|cx| {
4686 let settings_rx = watch_config_file(
4687 &executor,
4688 app_state.fs.clone(),
4689 PathBuf::from("/settings.json"),
4690 );
4691 let keymap_rx = watch_config_file(
4692 &executor,
4693 app_state.fs.clone(),
4694 PathBuf::from("/keymap.json"),
4695 );
4696 let global_settings_rx = watch_config_file(
4697 &executor,
4698 app_state.fs.clone(),
4699 PathBuf::from("/global_settings.json"),
4700 );
4701 handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
4702 handle_keymap_file_changes(keymap_rx, cx);
4703 });
4704 workspace
4705 .update(cx, |workspace, _, cx| {
4706 workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4707 workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4708 workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
4709 workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
4710 cx.notify();
4711 })
4712 .unwrap();
4713 executor.run_until_parked();
4714 // Test loading the keymap base at all
4715 assert_key_bindings_for(
4716 workspace.into(),
4717 cx,
4718 vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4719 line!(),
4720 );
4721
4722 // Test modifying the users keymap, while retaining the base keymap
4723 app_state
4724 .fs
4725 .save(
4726 "/keymap.json".as_ref(),
4727 &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(),
4728 Default::default(),
4729 )
4730 .await
4731 .unwrap();
4732
4733 executor.run_until_parked();
4734
4735 assert_key_bindings_for(
4736 workspace.into(),
4737 cx,
4738 vec![("backspace", &ActionB), ("k", &ActivatePreviousPane)],
4739 line!(),
4740 );
4741
4742 // Test modifying the base, while retaining the users keymap
4743 app_state
4744 .fs
4745 .save(
4746 "/settings.json".as_ref(),
4747 &r#"{"base_keymap": "JetBrains"}"#.into(),
4748 Default::default(),
4749 )
4750 .await
4751 .unwrap();
4752
4753 executor.run_until_parked();
4754
4755 assert_key_bindings_for(
4756 workspace.into(),
4757 cx,
4758 vec![("backspace", &ActionB), ("{", &ActivatePreviousItem)],
4759 line!(),
4760 );
4761 }
4762
4763 #[gpui::test]
4764 async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
4765 let executor = cx.executor();
4766 let app_state = init_keymap_test(cx);
4767 let project = Project::test(app_state.fs.clone(), [], cx).await;
4768 let workspace =
4769 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4770
4771 // From the Atom keymap
4772 use workspace::ActivatePreviousPane;
4773 // From the JetBrains keymap
4774 use diagnostics::Deploy;
4775
4776 workspace
4777 .update(cx, |workspace, _, _| {
4778 workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4779 workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4780 workspace.register_action(|_, _: &Deploy, _window, _cx| {});
4781 })
4782 .unwrap();
4783 app_state
4784 .fs
4785 .save(
4786 "/settings.json".as_ref(),
4787 &r#"{"base_keymap": "Atom"}"#.into(),
4788 Default::default(),
4789 )
4790 .await
4791 .unwrap();
4792 app_state
4793 .fs
4794 .save(
4795 "/keymap.json".as_ref(),
4796 &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4797 Default::default(),
4798 )
4799 .await
4800 .unwrap();
4801
4802 cx.update(|cx| {
4803 let settings_rx = watch_config_file(
4804 &executor,
4805 app_state.fs.clone(),
4806 PathBuf::from("/settings.json"),
4807 );
4808 let keymap_rx = watch_config_file(
4809 &executor,
4810 app_state.fs.clone(),
4811 PathBuf::from("/keymap.json"),
4812 );
4813
4814 let global_settings_rx = watch_config_file(
4815 &executor,
4816 app_state.fs.clone(),
4817 PathBuf::from("/global_settings.json"),
4818 );
4819 handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
4820 handle_keymap_file_changes(keymap_rx, cx);
4821 });
4822
4823 cx.background_executor.run_until_parked();
4824
4825 cx.background_executor.run_until_parked();
4826 // Test loading the keymap base at all
4827 assert_key_bindings_for(
4828 workspace.into(),
4829 cx,
4830 vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4831 line!(),
4832 );
4833
4834 // Test disabling the key binding for the base keymap
4835 app_state
4836 .fs
4837 .save(
4838 "/keymap.json".as_ref(),
4839 &r#"[{"bindings": {"backspace": null}}]"#.into(),
4840 Default::default(),
4841 )
4842 .await
4843 .unwrap();
4844
4845 cx.background_executor.run_until_parked();
4846
4847 assert_key_bindings_for(
4848 workspace.into(),
4849 cx,
4850 vec![("k", &ActivatePreviousPane)],
4851 line!(),
4852 );
4853
4854 // Test modifying the base, while retaining the users keymap
4855 app_state
4856 .fs
4857 .save(
4858 "/settings.json".as_ref(),
4859 &r#"{"base_keymap": "JetBrains"}"#.into(),
4860 Default::default(),
4861 )
4862 .await
4863 .unwrap();
4864
4865 cx.background_executor.run_until_parked();
4866
4867 assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!());
4868 }
4869
4870 #[gpui::test]
4871 async fn test_generate_keymap_json_schema_for_registered_actions(
4872 cx: &mut gpui::TestAppContext,
4873 ) {
4874 init_keymap_test(cx);
4875 cx.update(|cx| {
4876 // Make sure it doesn't panic.
4877 KeymapFile::generate_json_schema_for_registered_actions(cx);
4878 });
4879 }
4880
4881 /// Actions that don't build from empty input won't work from command palette invocation.
4882 #[gpui::test]
4883 async fn test_actions_build_with_empty_input(cx: &mut gpui::TestAppContext) {
4884 init_keymap_test(cx);
4885 cx.update(|cx| {
4886 let all_actions = cx.all_action_names();
4887 let mut failing_names = Vec::new();
4888 let mut errors = Vec::new();
4889 for action in all_actions {
4890 match action.to_string().as_str() {
4891 "vim::FindCommand"
4892 | "vim::Literal"
4893 | "vim::ResizePane"
4894 | "vim::PushObject"
4895 | "vim::PushFindForward"
4896 | "vim::PushFindBackward"
4897 | "vim::PushSneak"
4898 | "vim::PushSneakBackward"
4899 | "vim::PushChangeSurrounds"
4900 | "vim::PushJump"
4901 | "vim::PushDigraph"
4902 | "vim::PushLiteral"
4903 | "vim::PushHelixNext"
4904 | "vim::PushHelixPrevious"
4905 | "vim::Number"
4906 | "vim::SelectRegister"
4907 | "git::StageAndNext"
4908 | "git::UnstageAndNext"
4909 | "terminal::SendText"
4910 | "terminal::SendKeystroke"
4911 | "app_menu::OpenApplicationMenu"
4912 | "picker::ConfirmInput"
4913 | "editor::HandleInput"
4914 | "editor::FoldAtLevel"
4915 | "pane::ActivateItem"
4916 | "workspace::ActivatePane"
4917 | "workspace::MoveItemToPane"
4918 | "workspace::MoveItemToPaneInDirection"
4919 | "workspace::NewFileSplit"
4920 | "workspace::OpenTerminal"
4921 | "workspace::SendKeystrokes"
4922 | "agent::NewNativeAgentThreadFromSummary"
4923 | "action::Sequence"
4924 | "zed::OpenBrowser"
4925 | "zed::OpenZedUrl"
4926 | "settings_editor::FocusFile" => {}
4927 _ => {
4928 let result = cx.build_action(action, None);
4929 match &result {
4930 Ok(_) => {}
4931 Err(err) => {
4932 failing_names.push(action);
4933 errors.push(format!("{action} failed to build: {err:?}"));
4934 }
4935 }
4936 }
4937 }
4938 }
4939 if !errors.is_empty() {
4940 panic!(
4941 "Failed to build actions using {{}} as input: {:?}. Errors:\n{}",
4942 failing_names,
4943 errors.join("\n")
4944 );
4945 }
4946 });
4947 }
4948
4949 /// Checks that action namespaces are the expected set. The purpose of this is to prevent typos
4950 /// and let you know when introducing a new namespace.
4951 #[gpui::test]
4952 async fn test_action_namespaces(cx: &mut gpui::TestAppContext) {
4953 use itertools::Itertools;
4954
4955 init_keymap_test(cx);
4956 cx.update(|cx| {
4957 let all_actions = cx.all_action_names();
4958
4959 let mut actions_without_namespace = Vec::new();
4960 let all_namespaces = all_actions
4961 .iter()
4962 .filter_map(|action_name| {
4963 let namespace = action_name
4964 .split("::")
4965 .collect::<Vec<_>>()
4966 .into_iter()
4967 .rev()
4968 .skip(1)
4969 .rev()
4970 .join("::");
4971 if namespace.is_empty() {
4972 actions_without_namespace.push(*action_name);
4973 }
4974 if &namespace == "test_only" || &namespace == "stories" {
4975 None
4976 } else {
4977 Some(namespace)
4978 }
4979 })
4980 .sorted()
4981 .dedup()
4982 .collect::<Vec<_>>();
4983 assert_eq!(actions_without_namespace, Vec::<&str>::new());
4984
4985 let expected_namespaces = vec![
4986 "action",
4987 "activity_indicator",
4988 "agent",
4989 #[cfg(not(target_os = "macos"))]
4990 "app_menu",
4991 "assistant",
4992 "assistant2",
4993 "auto_update",
4994 "branches",
4995 "buffer_search",
4996 "channel_modal",
4997 "cli",
4998 "client",
4999 "collab",
5000 "collab_panel",
5001 "command_palette",
5002 "console",
5003 "context_server",
5004 "copilot",
5005 "debug_panel",
5006 "debugger",
5007 "dev",
5008 "diagnostics",
5009 "edit_prediction",
5010 "editor",
5011 "feedback",
5012 "file_finder",
5013 "git",
5014 "git_onboarding",
5015 "git_panel",
5016 "go_to_line",
5017 "icon_theme_selector",
5018 "journal",
5019 "keymap_editor",
5020 "keystroke_input",
5021 "language_selector",
5022 "line_ending_selector",
5023 "lsp_tool",
5024 "markdown",
5025 "menu",
5026 "notebook",
5027 "notification_panel",
5028 "onboarding",
5029 "outline",
5030 "outline_panel",
5031 "pane",
5032 "panel",
5033 "picker",
5034 "project_panel",
5035 "project_search",
5036 "project_symbols",
5037 "projects",
5038 "repl",
5039 "rules_library",
5040 "search",
5041 "settings_editor",
5042 "settings_profile_selector",
5043 "snippets",
5044 "stash_picker",
5045 "supermaven",
5046 "svg",
5047 "syntax_tree_view",
5048 "tab_switcher",
5049 "task",
5050 "terminal",
5051 "terminal_panel",
5052 "theme_selector",
5053 "toast",
5054 "toolchain",
5055 "variable_list",
5056 "vim",
5057 "window",
5058 "workspace",
5059 "zed",
5060 "zed_actions",
5061 "zed_predict_onboarding",
5062 "zeta",
5063 ];
5064 assert_eq!(
5065 all_namespaces,
5066 expected_namespaces
5067 .into_iter()
5068 .map(|namespace| namespace.to_string())
5069 .sorted()
5070 .collect::<Vec<_>>()
5071 );
5072 });
5073 }
5074
5075 #[gpui::test]
5076 fn test_bundled_settings_and_themes(cx: &mut App) {
5077 cx.text_system()
5078 .add_fonts(vec![
5079 Assets
5080 .load("fonts/lilex/Lilex-Regular.ttf")
5081 .unwrap()
5082 .unwrap(),
5083 Assets
5084 .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf")
5085 .unwrap()
5086 .unwrap(),
5087 ])
5088 .unwrap();
5089 let themes = ThemeRegistry::default();
5090 settings::init(cx);
5091 theme::init(theme::LoadThemes::JustBase, cx);
5092
5093 let mut has_default_theme = false;
5094 for theme_name in themes.list().into_iter().map(|meta| meta.name) {
5095 let theme = themes.get(&theme_name).unwrap();
5096 assert_eq!(theme.name, theme_name);
5097 if theme.name.as_ref() == "One Dark" {
5098 has_default_theme = true;
5099 }
5100 }
5101 assert!(has_default_theme);
5102 }
5103
5104 #[gpui::test]
5105 async fn test_bundled_files_editor(cx: &mut TestAppContext) {
5106 let app_state = init_test(cx);
5107 cx.update(init);
5108
5109 let project = Project::test(app_state.fs.clone(), [], cx).await;
5110 let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
5111
5112 cx.update(|cx| {
5113 cx.dispatch_action(&OpenDefaultSettings);
5114 });
5115 cx.run_until_parked();
5116
5117 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
5118
5119 let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
5120 let active_editor = workspace
5121 .update(cx, |workspace, _, cx| {
5122 workspace.active_item_as::<Editor>(cx)
5123 })
5124 .unwrap();
5125 assert!(
5126 active_editor.is_some(),
5127 "Settings action should have opened an editor with the default file contents"
5128 );
5129
5130 let active_editor = active_editor.unwrap();
5131 assert!(
5132 active_editor.read_with(cx, |editor, cx| editor.read_only(cx)),
5133 "Default settings should be readonly"
5134 );
5135 assert!(
5136 active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()),
5137 "The underlying buffer should also be readonly for the shipped default settings"
5138 );
5139 }
5140
5141 #[gpui::test]
5142 async fn test_bundled_languages(cx: &mut TestAppContext) {
5143 let fs = fs::FakeFs::new(cx.background_executor.clone());
5144 env_logger::builder().is_test(true).try_init().ok();
5145 let settings = cx.update(SettingsStore::test);
5146 cx.set_global(settings);
5147 let languages = LanguageRegistry::test(cx.executor());
5148 let languages = Arc::new(languages);
5149 let node_runtime = node_runtime::NodeRuntime::unavailable();
5150 cx.update(|cx| {
5151 languages::init(languages.clone(), fs, node_runtime, cx);
5152 });
5153 for name in languages.language_names() {
5154 languages
5155 .language_for_name(name.as_ref())
5156 .await
5157 .with_context(|| format!("language name {name}"))
5158 .unwrap();
5159 }
5160 cx.run_until_parked();
5161 }
5162
5163 pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
5164 init_test_with_state(cx, cx.update(AppState::test))
5165 }
5166
5167 fn init_test_with_state(
5168 cx: &mut TestAppContext,
5169 mut app_state: Arc<AppState>,
5170 ) -> Arc<AppState> {
5171 cx.update(move |cx| {
5172 env_logger::builder().is_test(true).try_init().ok();
5173
5174 let state = Arc::get_mut(&mut app_state).unwrap();
5175 state.build_window_options = build_window_options;
5176
5177 app_state.languages.add(markdown_language());
5178
5179 gpui_tokio::init(cx);
5180 vim_mode_setting::init(cx);
5181 theme::init(theme::LoadThemes::JustBase, cx);
5182 audio::init(cx);
5183 channel::init(&app_state.client, app_state.user_store.clone(), cx);
5184 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
5185 notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
5186 workspace::init(app_state.clone(), cx);
5187 Project::init_settings(cx);
5188 release_channel::init(SemanticVersion::default(), cx);
5189 command_palette::init(cx);
5190 language::init(cx);
5191 editor::init(cx);
5192 collab_ui::init(&app_state, cx);
5193 git_ui::init(cx);
5194 project_panel::init(cx);
5195 outline_panel::init(cx);
5196 terminal_view::init(cx);
5197 copilot::copilot_chat::init(
5198 app_state.fs.clone(),
5199 app_state.client.http_client(),
5200 copilot::copilot_chat::CopilotChatConfiguration::default(),
5201 cx,
5202 );
5203 image_viewer::init(cx);
5204 language_model::init(app_state.client.clone(), cx);
5205 language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
5206 web_search::init(cx);
5207 web_search_providers::init(app_state.client.clone(), cx);
5208 let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
5209 agent_ui::init(
5210 app_state.fs.clone(),
5211 app_state.client.clone(),
5212 prompt_builder.clone(),
5213 app_state.languages.clone(),
5214 false,
5215 cx,
5216 );
5217 repl::init(app_state.fs.clone(), cx);
5218 repl::notebook::init(cx);
5219 tasks_ui::init(cx);
5220 project::debugger::breakpoint_store::BreakpointStore::init(
5221 &app_state.client.clone().into(),
5222 );
5223 project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
5224 debugger_ui::init(cx);
5225 initialize_workspace(app_state.clone(), prompt_builder, cx);
5226 search::init(cx);
5227 app_state
5228 })
5229 }
5230
5231 fn rust_lang() -> Arc<language::Language> {
5232 Arc::new(language::Language::new(
5233 language::LanguageConfig {
5234 name: "Rust".into(),
5235 matcher: LanguageMatcher {
5236 path_suffixes: vec!["rs".to_string()],
5237 ..Default::default()
5238 },
5239 ..Default::default()
5240 },
5241 Some(tree_sitter_rust::LANGUAGE.into()),
5242 ))
5243 }
5244
5245 fn markdown_language() -> Arc<language::Language> {
5246 Arc::new(language::Language::new(
5247 language::LanguageConfig {
5248 name: "Markdown".into(),
5249 matcher: LanguageMatcher {
5250 path_suffixes: vec!["md".to_string()],
5251 ..Default::default()
5252 },
5253 ..Default::default()
5254 },
5255 Some(tree_sitter_md::LANGUAGE.into()),
5256 ))
5257 }
5258
5259 #[track_caller]
5260 fn assert_key_bindings_for(
5261 window: AnyWindowHandle,
5262 cx: &TestAppContext,
5263 actions: Vec<(&'static str, &dyn Action)>,
5264 line: u32,
5265 ) {
5266 let available_actions = cx
5267 .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
5268 .unwrap();
5269 for (key, action) in actions {
5270 let bindings = cx
5271 .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
5272 .unwrap();
5273 // assert that...
5274 assert!(
5275 available_actions.iter().any(|bound_action| {
5276 // actions match...
5277 bound_action.partial_eq(action)
5278 }),
5279 "On {} Failed to find {}",
5280 line,
5281 action.name(),
5282 );
5283 assert!(
5284 // and key strokes contain the given key
5285 bindings
5286 .into_iter()
5287 .any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)),
5288 "On {} Failed to find {} with key binding {}",
5289 line,
5290 action.name(),
5291 key
5292 );
5293 }
5294 }
5295
5296 #[gpui::test]
5297 async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
5298 // Use the proper initialization for runtime state
5299 let app_state = init_keymap_test(cx);
5300
5301 eprintln!("Running test_opening_project_settings_when_excluded");
5302
5303 // 1. Set up a project with some project settings
5304 let settings_init =
5305 r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
5306 app_state
5307 .fs
5308 .as_fake()
5309 .insert_tree(
5310 Path::new("/root"),
5311 json!({
5312 ".zed": {
5313 "settings.json": settings_init
5314 }
5315 }),
5316 )
5317 .await;
5318
5319 eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");
5320
5321 // 2. Create a project with the file system and load it
5322 let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;
5323
5324 // Save original settings content for comparison
5325 let original_settings = app_state
5326 .fs
5327 .load(Path::new("/root/.zed/settings.json"))
5328 .await
5329 .unwrap();
5330
5331 let original_settings_str = original_settings.clone();
5332
5333 // Verify settings exist on disk and have expected content
5334 eprintln!("Original settings content: {}", original_settings_str);
5335 assert!(
5336 original_settings_str.contains("UNIQUEVALUE"),
5337 "Test setup failed - settings file doesn't contain our marker"
5338 );
5339
5340 // 3. Add .zed to file scan exclusions in user settings
5341 cx.update_global::<SettingsStore, _>(|store, cx| {
5342 store.update_user_settings(cx, |worktree_settings| {
5343 worktree_settings.project.worktree.file_scan_exclusions =
5344 Some(vec![".zed".to_string()]);
5345 });
5346 });
5347
5348 eprintln!("Added .zed to file_scan_exclusions in settings");
5349
5350 // 4. Run tasks to apply settings
5351 cx.background_executor.run_until_parked();
5352
5353 // 5. Critical: Verify .zed is actually excluded from worktree
5354 let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap());
5355
5356 let has_zed_entry =
5357 cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some());
5358
5359 eprintln!(
5360 "Is .zed directory visible in worktree after exclusion: {}",
5361 has_zed_entry
5362 );
5363
5364 // This assertion verifies the test is set up correctly to show the bug
5365 // If .zed is not excluded, the test will fail here
5366 assert!(
5367 !has_zed_entry,
5368 "Test precondition failed: .zed directory should be excluded but was found in worktree"
5369 );
5370
5371 // 6. Create workspace and trigger the actual function that causes the bug
5372 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5373 window
5374 .update(cx, |workspace, window, cx| {
5375 // Call the exact function that contains the bug
5376 eprintln!("About to call open_project_settings_file");
5377 open_project_settings_file(workspace, &OpenProjectSettings, window, cx);
5378 })
5379 .unwrap();
5380
5381 // 7. Run background tasks until completion
5382 cx.background_executor.run_until_parked();
5383
5384 // 8. Verify file contents after calling function
5385 let new_content = app_state
5386 .fs
5387 .load(Path::new("/root/.zed/settings.json"))
5388 .await
5389 .unwrap();
5390
5391 let new_content_str = new_content;
5392 eprintln!("New settings content: {}", new_content_str);
5393
5394 // The bug causes the settings to be overwritten with empty settings
5395 // So if the unique value is no longer present, the bug has been reproduced
5396 let bug_exists = !new_content_str.contains("UNIQUEVALUE");
5397 eprintln!("Bug reproduced: {}", bug_exists);
5398
5399 // This assertion should fail if the bug exists - showing the bug is real
5400 assert!(
5401 new_content_str.contains("UNIQUEVALUE"),
5402 "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
5403 );
5404 }
5405}