1mod app_menus;
2mod assets;
3pub mod languages;
4mod only_instance;
5mod open_listener;
6
7pub use app_menus::*;
8pub use assets::*;
9use assistant::AssistantPanel;
10use breadcrumbs::Breadcrumbs;
11use collections::VecDeque;
12use editor::{Editor, MultiBuffer};
13use gpui::{
14 actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions, View,
15 ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions,
16};
17pub use only_instance::*;
18pub use open_listener::*;
19
20use anyhow::{anyhow, Context as _};
21use futures::{channel::mpsc, StreamExt};
22use project_panel::ProjectPanel;
23use quick_action_bar::QuickActionBar;
24use search::project_search::ProjectSearchBar;
25use settings::{initial_local_settings_content, load_default_keymap, KeymapFile, Settings};
26use std::{borrow::Cow, ops::Deref, sync::Arc};
27use terminal_view::terminal_panel::TerminalPanel;
28use util::{
29 asset_str,
30 channel::{AppCommitSha, ReleaseChannel},
31 paths::{self, LOCAL_SETTINGS_RELATIVE_PATH},
32 ResultExt,
33};
34use uuid::Uuid;
35use workspace::Pane;
36use workspace::{
37 create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
38 open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings,
39};
40use zed_actions::{OpenBrowser, OpenSettings, OpenZedURL, Quit};
41
42actions!(
43 zed,
44 [
45 About,
46 DebugElements,
47 DecreaseBufferFontSize,
48 Hide,
49 HideOthers,
50 IncreaseBufferFontSize,
51 Minimize,
52 OpenDefaultKeymap,
53 OpenDefaultSettings,
54 OpenKeymap,
55 OpenLicenses,
56 OpenLocalSettings,
57 OpenLog,
58 OpenTelemetryLog,
59 ResetBufferFontSize,
60 ResetDatabase,
61 ShowAll,
62 ToggleFullScreen,
63 Zoom,
64 ]
65);
66
67pub fn build_window_options(
68 bounds: Option<WindowBounds>,
69 display_uuid: Option<Uuid>,
70 cx: &mut AppContext,
71) -> WindowOptions {
72 let bounds = bounds.unwrap_or(WindowBounds::Maximized);
73 let display = display_uuid.and_then(|uuid| {
74 cx.displays()
75 .into_iter()
76 .find(|display| display.uuid().ok() == Some(uuid))
77 });
78
79 WindowOptions {
80 bounds,
81 titlebar: Some(TitlebarOptions {
82 title: None,
83 appears_transparent: true,
84 traffic_light_position: Some(point(px(8.), px(8.))),
85 }),
86 center: false,
87 focus: false,
88 show: false,
89 kind: WindowKind::Normal,
90 is_movable: true,
91 display_id: display.map(|display| display.id()),
92 }
93}
94
95pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
96 cx.observe_new_views(move |workspace: &mut Workspace, cx| {
97 let workspace_handle = cx.view().clone();
98 let center_pane = workspace.active_pane().clone();
99 initialize_pane(workspace, ¢er_pane, cx);
100 cx.subscribe(&workspace_handle, {
101 move |workspace, _, event, cx| {
102 if let workspace::Event::PaneAdded(pane) = event {
103 initialize_pane(workspace, pane, cx);
104 }
105 }
106 })
107 .detach();
108
109 // cx.emit(workspace2::Event::PaneAdded(
110 // workspace.active_pane().clone(),
111 // ));
112
113 // let collab_titlebar_item =
114 // cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
115 // workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
116
117 let copilot =
118 cx.build_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
119 let diagnostic_summary =
120 cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
121 let activity_indicator =
122 activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
123 let active_buffer_language =
124 cx.build_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
125 let vim_mode_indicator = cx.build_view(|cx| vim::ModeIndicator::new(cx));
126 let feedback_button = cx
127 .build_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace));
128 let cursor_position = cx.build_view(|_| editor::items::CursorPosition::new());
129 workspace.status_bar().update(cx, |status_bar, cx| {
130 status_bar.add_left_item(diagnostic_summary, cx);
131 status_bar.add_left_item(activity_indicator, cx);
132 status_bar.add_right_item(feedback_button, cx);
133 // status_bar.add_right_item(copilot, cx);
134 status_bar.add_right_item(copilot, cx);
135 status_bar.add_right_item(active_buffer_language, cx);
136 status_bar.add_right_item(vim_mode_indicator, cx);
137 status_bar.add_right_item(cursor_position, cx);
138 });
139
140 auto_update::notify_of_any_new_update(cx);
141
142 vim::observe_keystrokes(cx);
143
144 let handle = cx.view().downgrade();
145 cx.on_window_should_close(move |cx| {
146 handle
147 .update(cx, |workspace, cx| {
148 workspace.close_window(&Default::default(), cx);
149 false
150 })
151 .unwrap_or(true)
152 });
153
154 cx.spawn(|workspace_handle, mut cx| async move {
155 let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
156 let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
157 let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
158 let channels_panel =
159 collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
160 let chat_panel =
161 collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
162 let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
163 workspace_handle.clone(),
164 cx.clone(),
165 );
166 let (
167 project_panel,
168 terminal_panel,
169 assistant_panel,
170 channels_panel,
171 chat_panel,
172 notification_panel,
173 ) = futures::try_join!(
174 project_panel,
175 terminal_panel,
176 assistant_panel,
177 channels_panel,
178 chat_panel,
179 notification_panel,
180 )?;
181
182 workspace_handle.update(&mut cx, |workspace, cx| {
183 workspace.add_panel(project_panel, cx);
184 workspace.add_panel(terminal_panel, cx);
185 workspace.add_panel(assistant_panel, cx);
186 workspace.add_panel(channels_panel, cx);
187 workspace.add_panel(chat_panel, cx);
188 workspace.add_panel(notification_panel, cx);
189
190 // if !was_deserialized
191 // && workspace
192 // .project()
193 // .read(cx)
194 // .visible_worktrees(cx)
195 // .any(|tree| {
196 // tree.read(cx)
197 // .root_entry()
198 // .map_or(false, |entry| entry.is_dir())
199 // })
200 // {
201 // workspace.toggle_dock(project_panel_position, cx);
202 // }
203 cx.focus_self();
204 })
205 })
206 .detach();
207
208 workspace
209 .register_action(about)
210 .register_action(|_, _: &Hide, cx| {
211 cx.hide();
212 })
213 .register_action(|_, _: &HideOthers, cx| {
214 cx.hide_other_apps();
215 })
216 .register_action(|_, _: &ShowAll, cx| {
217 cx.unhide_other_apps();
218 })
219 .register_action(|_, _: &Minimize, cx| {
220 cx.minimize_window();
221 })
222 .register_action(|_, _: &Zoom, cx| {
223 cx.zoom_window();
224 })
225 .register_action(|_, _: &ToggleFullScreen, cx| {
226 cx.toggle_full_screen();
227 })
228 .register_action(quit)
229 .register_action(|_, action: &OpenZedURL, cx| {
230 cx.global::<Arc<OpenListener>>()
231 .open_urls(&[action.url.clone()])
232 })
233 .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url))
234 .register_action(move |_, _: &IncreaseBufferFontSize, cx| {
235 theme::adjust_font_size(cx, |size| *size += px(1.0))
236 })
237 .register_action(move |_, _: &DecreaseBufferFontSize, cx| {
238 theme::adjust_font_size(cx, |size| *size -= px(1.0))
239 })
240 .register_action(move |_, _: &ResetBufferFontSize, cx| theme::reset_font_size(cx))
241 .register_action(|_, _: &install_cli::Install, cx| {
242 cx.spawn(|_, cx| async move {
243 install_cli::install_cli(cx.deref())
244 .await
245 .context("error creating CLI symlink")
246 })
247 .detach_and_log_err(cx);
248 })
249 .register_action(|workspace, _: &OpenLog, cx| {
250 open_log_file(workspace, cx);
251 })
252 .register_action(|workspace, _: &OpenLicenses, cx| {
253 open_bundled_file(
254 workspace,
255 asset_str::<Assets>("licenses.md"),
256 "Open Source License Attribution",
257 "Markdown",
258 cx,
259 );
260 })
261 .register_action(
262 move |workspace: &mut Workspace,
263 _: &OpenTelemetryLog,
264 cx: &mut ViewContext<Workspace>| {
265 open_telemetry_log_file(workspace, cx);
266 },
267 )
268 .register_action(
269 move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
270 create_and_open_local_file(&paths::KEYMAP, cx, Default::default)
271 .detach_and_log_err(cx);
272 },
273 )
274 .register_action(
275 move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
276 create_and_open_local_file(&paths::SETTINGS, cx, || {
277 settings::initial_user_settings_content().as_ref().into()
278 })
279 .detach_and_log_err(cx);
280 },
281 )
282 .register_action(open_local_settings_file)
283 .register_action(
284 move |workspace: &mut Workspace,
285 _: &OpenDefaultKeymap,
286 cx: &mut ViewContext<Workspace>| {
287 open_bundled_file(
288 workspace,
289 settings::default_keymap(),
290 "Default Key Bindings",
291 "JSON",
292 cx,
293 );
294 },
295 )
296 .register_action(
297 move |workspace: &mut Workspace,
298 _: &OpenDefaultSettings,
299 cx: &mut ViewContext<Workspace>| {
300 open_bundled_file(
301 workspace,
302 settings::default_settings(),
303 "Default Settings",
304 "JSON",
305 cx,
306 );
307 },
308 )
309 //todo!()
310 // cx.add_action({
311 // move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
312 // let app_state = workspace.app_state().clone();
313 // let markdown = app_state.languages.language_for_name("JSON");
314 // let window = cx.window();
315 // cx.spawn(|workspace, mut cx| async move {
316 // let markdown = markdown.await.log_err();
317 // let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| {
318 // anyhow!("could not debug elements for window {}", window.id())
319 // })?)
320 // .unwrap();
321 // workspace
322 // .update(&mut cx, |workspace, cx| {
323 // workspace.with_local_workspace(cx, move |workspace, cx| {
324 // let project = workspace.project().clone();
325 // let buffer = project
326 // .update(cx, |project, cx| {
327 // project.create_buffer(&content, markdown, cx)
328 // })
329 // .expect("creating buffers on a local workspace always succeeds");
330 // let buffer = cx.add_model(|cx| {
331 // MultiBuffer::singleton(buffer, cx)
332 // .with_title("Debug Elements".into())
333 // });
334 // workspace.add_item(
335 // Box::new(cx.add_view(|cx| {
336 // Editor::for_multibuffer(buffer, Some(project.clone()), cx)
337 // })),
338 // cx,
339 // );
340 // })
341 // })?
342 // .await
343 // })
344 // .detach_and_log_err(cx);
345 // }
346 // });
347 // .register_action(
348 // |workspace: &mut Workspace,
349 // _: &project_panel::ToggleFocus,
350 // cx: &mut ViewContext<Workspace>| {
351 // workspace.toggle_panel_focus::<ProjectPanel>(cx);
352 // },
353 // );
354 // cx.add_action(
355 // |workspace: &mut Workspace,
356 // _: &collab_ui::collab_panel::ToggleFocus,
357 // cx: &mut ViewContext<Workspace>| {
358 // workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
359 // },
360 // );
361 // cx.add_action(
362 // |workspace: &mut Workspace,
363 // _: &collab_ui::chat_panel::ToggleFocus,
364 // cx: &mut ViewContext<Workspace>| {
365 // workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(cx);
366 // },
367 // );
368 // cx.add_action(
369 // |workspace: &mut Workspace,
370 // _: &collab_ui::notification_panel::ToggleFocus,
371 // cx: &mut ViewContext<Workspace>| {
372 // workspace.toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(cx);
373 // },
374 // );
375 // cx.add_action(
376 // |workspace: &mut Workspace,
377 // _: &terminal_panel::ToggleFocus,
378 // cx: &mut ViewContext<Workspace>| {
379 // workspace.toggle_panel_focus::<TerminalPanel>(cx);
380 // },
381 // );
382 .register_action({
383 let app_state = Arc::downgrade(&app_state);
384 move |_, _: &NewWindow, cx| {
385 if let Some(app_state) = app_state.upgrade() {
386 open_new(&app_state, cx, |workspace, cx| {
387 Editor::new_file(workspace, &Default::default(), cx)
388 })
389 .detach();
390 }
391 }
392 })
393 .register_action({
394 let app_state = Arc::downgrade(&app_state);
395 move |_, _: &NewFile, cx| {
396 if let Some(app_state) = app_state.upgrade() {
397 open_new(&app_state, cx, |workspace, cx| {
398 Editor::new_file(workspace, &Default::default(), cx)
399 })
400 .detach();
401 }
402 }
403 });
404
405 workspace.focus_handle(cx).focus(cx);
406 //todo!()
407 // load_default_keymap(cx);
408 })
409 .detach();
410}
411
412fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
413 pane.update(cx, |pane, cx| {
414 pane.toolbar().update(cx, |toolbar, cx| {
415 let breadcrumbs = cx.build_view(|_| Breadcrumbs::new());
416 toolbar.add_item(breadcrumbs, cx);
417 let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
418 toolbar.add_item(buffer_search_bar.clone(), cx);
419
420 let quick_action_bar =
421 cx.build_view(|_| QuickActionBar::new(buffer_search_bar, workspace));
422 toolbar.add_item(quick_action_bar, cx);
423 let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new());
424 toolbar.add_item(diagnostic_editor_controls, cx);
425 let project_search_bar = cx.build_view(|_| ProjectSearchBar::new());
426 toolbar.add_item(project_search_bar, cx);
427 let lsp_log_item = cx.build_view(|_| language_tools::LspLogToolbarItemView::new());
428 toolbar.add_item(lsp_log_item, cx);
429 let syntax_tree_item =
430 cx.build_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
431 toolbar.add_item(syntax_tree_item, cx);
432 })
433 });
434}
435
436fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
437 use std::fmt::Write as _;
438
439 let app_name = cx.global::<ReleaseChannel>().display_name();
440 let version = env!("CARGO_PKG_VERSION");
441 let mut message = format!("{app_name} {version}");
442 if let Some(sha) = cx.try_global::<AppCommitSha>() {
443 write!(&mut message, "\n\n{}", sha.0).unwrap();
444 }
445
446 let prompt = cx.prompt(PromptLevel::Info, &message, &["OK"]);
447 cx.foreground_executor()
448 .spawn(async {
449 prompt.await.ok();
450 })
451 .detach();
452}
453
454fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext<Workspace>) {
455 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
456 cx.spawn(|_, mut cx| async move {
457 let mut workspace_windows = cx.update(|_, cx| {
458 cx.windows()
459 .into_iter()
460 .filter_map(|window| window.downcast::<Workspace>())
461 .collect::<Vec<_>>()
462 })?;
463
464 // If multiple windows have unsaved changes, and need a save prompt,
465 // prompt in the active window before switching to a different window.
466 cx.update(|_, cx| {
467 workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
468 })
469 .log_err();
470
471 if let (true, Some(_)) = (should_confirm, workspace_windows.first().copied()) {
472 let answer = cx
473 .update(|_, cx| {
474 cx.prompt(
475 PromptLevel::Info,
476 "Are you sure you want to quit?",
477 &["Quit", "Cancel"],
478 )
479 })
480 .log_err();
481
482 if let Some(answer) = answer {
483 let answer = answer.await.ok();
484 if answer != Some(0) {
485 return Ok(());
486 }
487 }
488 }
489
490 // If the user cancels any save prompt, then keep the app open.
491 for window in workspace_windows {
492 if let Some(should_close) = window
493 .update(&mut cx, |workspace, cx| {
494 workspace.prepare_to_close(true, cx)
495 })
496 .log_err()
497 {
498 if !should_close.await? {
499 return Ok(());
500 }
501 }
502 }
503 cx.update(|_, cx| {
504 cx.quit();
505 })?;
506 anyhow::Ok(())
507 })
508 .detach_and_log_err(cx);
509}
510
511fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
512 const MAX_LINES: usize = 1000;
513 workspace
514 .with_local_workspace(cx, move |workspace, cx| {
515 let fs = workspace.app_state().fs.clone();
516 cx.spawn(|workspace, mut cx| async move {
517 let (old_log, new_log) =
518 futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG));
519
520 let mut lines = VecDeque::with_capacity(MAX_LINES);
521 for line in old_log
522 .iter()
523 .flat_map(|log| log.lines())
524 .chain(new_log.iter().flat_map(|log| log.lines()))
525 {
526 if lines.len() == MAX_LINES {
527 lines.pop_front();
528 }
529 lines.push_back(line);
530 }
531 let log = lines
532 .into_iter()
533 .flat_map(|line| [line, "\n"])
534 .collect::<String>();
535
536 workspace
537 .update(&mut cx, |workspace, cx| {
538 let project = workspace.project().clone();
539 let buffer = project
540 .update(cx, |project, cx| project.create_buffer("", None, cx))
541 .expect("creating buffers on a local workspace always succeeds");
542 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
543
544 let buffer = cx.build_model(|cx| {
545 MultiBuffer::singleton(buffer, cx).with_title("Log".into())
546 });
547 workspace.add_item(
548 Box::new(cx.build_view(|cx| {
549 Editor::for_multibuffer(buffer, Some(project), cx)
550 })),
551 cx,
552 );
553 })
554 .log_err();
555 })
556 .detach();
557 })
558 .detach();
559}
560
561pub fn handle_keymap_file_changes(
562 mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
563 cx: &mut AppContext,
564) {
565 cx.spawn(move |cx| async move {
566 // let mut settings_subscription = None;
567 while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
568 if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() {
569 cx.update(|cx| reload_keymaps(cx, &keymap_content)).ok();
570
571 // todo!()
572 // let mut old_base_keymap = cx.read(|cx| *settings::get::<BaseKeymap>(cx));
573 // drop(settings_subscription);
574 // settings_subscription = Some(cx.update(|cx| {
575 // cx.observe_global::<SettingsStore, _>(move |cx| {
576 // let new_base_keymap = *settings::get::<BaseKeymap>(cx);
577 // if new_base_keymap != old_base_keymap {
578 // old_base_keymap = new_base_keymap.clone();
579 // reload_keymaps(cx, &keymap_content);
580 // }
581 // })
582 // }));
583 }
584 }
585 })
586 .detach();
587}
588
589fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
590 // todo!()
591 // cx.clear_bindings();
592 load_default_keymap(cx);
593 keymap_content.clone().add_to_cx(cx).log_err();
594 cx.set_menus(app_menus());
595}
596
597fn open_local_settings_file(
598 workspace: &mut Workspace,
599 _: &OpenLocalSettings,
600 cx: &mut ViewContext<Workspace>,
601) {
602 let project = workspace.project().clone();
603 let worktree = project
604 .read(cx)
605 .visible_worktrees(cx)
606 .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
607 if let Some(worktree) = worktree {
608 let tree_id = worktree.read(cx).id();
609 cx.spawn(|workspace, mut cx| async move {
610 let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH;
611
612 if let Some(dir_path) = file_path.parent() {
613 if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
614 project
615 .update(&mut cx, |project, cx| {
616 project.create_entry((tree_id, dir_path), true, cx)
617 })?
618 .await
619 .context("worktree was removed")?;
620 }
621 }
622
623 if worktree.update(&mut cx, |tree, _| tree.entry_for_path(file_path).is_none())? {
624 project
625 .update(&mut cx, |project, cx| {
626 project.create_entry((tree_id, file_path), false, cx)
627 })?
628 .await
629 .context("worktree was removed")?;
630 }
631
632 let editor = workspace
633 .update(&mut cx, |workspace, cx| {
634 workspace.open_path((tree_id, file_path), None, true, cx)
635 })?
636 .await?
637 .downcast::<Editor>()
638 .ok_or_else(|| anyhow!("unexpected item type"))?;
639
640 editor
641 .downgrade()
642 .update(&mut cx, |editor, cx| {
643 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
644 if buffer.read(cx).is_empty() {
645 buffer.update(cx, |buffer, cx| {
646 buffer.edit([(0..0, initial_local_settings_content())], None, cx)
647 });
648 }
649 }
650 })
651 .ok();
652
653 anyhow::Ok(())
654 })
655 .detach();
656 } else {
657 workspace.show_notification(0, cx, |cx| {
658 cx.build_view(|_| MessageNotification::new("This project has no folders open."))
659 })
660 }
661}
662
663fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
664 workspace.with_local_workspace(cx, move |workspace, cx| {
665 let app_state = workspace.app_state().clone();
666 cx.spawn(|workspace, mut cx| async move {
667 async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
668 let path = app_state.client.telemetry().log_file_path()?;
669 app_state.fs.load(&path).await.log_err()
670 }
671
672 let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
673
674 const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
675 let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
676 if let Some(newline_offset) = log[start_offset..].find('\n') {
677 start_offset += newline_offset + 1;
678 }
679 let log_suffix = &log[start_offset..];
680 let json = app_state.languages.language_for_name("JSON").await.log_err();
681
682 workspace.update(&mut cx, |workspace, cx| {
683 let project = workspace.project().clone();
684 let buffer = project
685 .update(cx, |project, cx| project.create_buffer("", None, cx))
686 .expect("creating buffers on a local workspace always succeeds");
687 buffer.update(cx, |buffer, cx| {
688 buffer.set_language(json, cx);
689 buffer.edit(
690 [(
691 0..0,
692 concat!(
693 "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
694 "// Telemetry can be disabled via the `settings.json` file.\n",
695 "// Here is the data that has been reported for the current session:\n",
696 "\n"
697 ),
698 )],
699 None,
700 cx,
701 );
702 buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
703 });
704
705 let buffer = cx.build_model(|cx| {
706 MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
707 });
708 workspace.add_item(
709 Box::new(cx.build_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
710 cx,
711 );
712 }).log_err()?;
713
714 Some(())
715 })
716 .detach();
717 }).detach();
718}
719
720fn open_bundled_file(
721 workspace: &mut Workspace,
722 text: Cow<'static, str>,
723 title: &'static str,
724 language: &'static str,
725 cx: &mut ViewContext<Workspace>,
726) {
727 let language = workspace.app_state().languages.language_for_name(language);
728 cx.spawn(|workspace, mut cx| async move {
729 let language = language.await.log_err();
730 workspace
731 .update(&mut cx, |workspace, cx| {
732 workspace.with_local_workspace(cx, |workspace, cx| {
733 let project = workspace.project();
734 let buffer = project.update(cx, move |project, cx| {
735 project
736 .create_buffer(text.as_ref(), language, cx)
737 .expect("creating buffers on a local workspace always succeeds")
738 });
739 let buffer = cx.build_model(|cx| {
740 MultiBuffer::singleton(buffer, cx).with_title(title.into())
741 });
742 workspace.add_item(
743 Box::new(cx.build_view(|cx| {
744 Editor::for_multibuffer(buffer, Some(project.clone()), cx)
745 })),
746 cx,
747 );
748 })
749 })?
750 .await
751 })
752 .detach_and_log_err(cx);
753}
754
755// todo!()
756// #[cfg(test)]
757// mod tests {
758// use super::*;
759// use assets::Assets;
760// use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
761// use fs::{FakeFs, Fs};
762// use gpui::{
763// actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle,
764// AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
765// };
766// use language::LanguageRegistry;
767// use project::{project_settings::ProjectSettings, Project, ProjectPath};
768// use serde_json::json;
769// use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
770// use std::{
771// collections::HashSet,
772// path::{Path, PathBuf},
773// };
774// use theme::{ThemeRegistry, ThemeSettings};
775// use workspace::{
776// item::{Item, ItemHandle},
777// open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle,
778// };
779
780// #[gpui::test]
781// async fn test_open_paths_action(cx: &mut TestAppContext) {
782// let app_state = init_test(cx);
783// app_state
784// .fs
785// .as_fake()
786// .insert_tree(
787// "/root",
788// json!({
789// "a": {
790// "aa": null,
791// "ab": null,
792// },
793// "b": {
794// "ba": null,
795// "bb": null,
796// },
797// "c": {
798// "ca": null,
799// "cb": null,
800// },
801// "d": {
802// "da": null,
803// "db": null,
804// },
805// }),
806// )
807// .await;
808
809// cx.update(|cx| {
810// open_paths(
811// &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
812// &app_state,
813// None,
814// cx,
815// )
816// })
817// .await
818// .unwrap();
819// assert_eq!(cx.windows().len(), 1);
820
821// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
822// .await
823// .unwrap();
824// assert_eq!(cx.windows().len(), 1);
825// let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
826// workspace_1.update(cx, |workspace, cx| {
827// assert_eq!(workspace.worktrees(cx).count(), 2);
828// assert!(workspace.left_dock().read(cx).is_open());
829// assert!(workspace.active_pane().is_focused(cx));
830// });
831
832// cx.update(|cx| {
833// open_paths(
834// &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
835// &app_state,
836// None,
837// cx,
838// )
839// })
840// .await
841// .unwrap();
842// assert_eq!(cx.windows().len(), 2);
843
844// // Replace existing windows
845// let window = cx.windows()[0].downcast::<Workspace>().unwrap();
846// cx.update(|cx| {
847// open_paths(
848// &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
849// &app_state,
850// Some(window),
851// cx,
852// )
853// })
854// .await
855// .unwrap();
856// assert_eq!(cx.windows().len(), 2);
857// let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
858// workspace_1.update(cx, |workspace, cx| {
859// assert_eq!(
860// workspace
861// .worktrees(cx)
862// .map(|w| w.read(cx).abs_path())
863// .collect::<Vec<_>>(),
864// &[Path::new("/root/c").into(), Path::new("/root/d").into()]
865// );
866// assert!(workspace.left_dock().read(cx).is_open());
867// assert!(workspace.active_pane().is_focused(cx));
868// });
869// }
870
871// #[gpui::test]
872// async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
873// let app_state = init_test(cx);
874// app_state
875// .fs
876// .as_fake()
877// .insert_tree("/root", json!({"a": "hey"}))
878// .await;
879
880// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
881// .await
882// .unwrap();
883// assert_eq!(cx.windows().len(), 1);
884
885// // When opening the workspace, the window is not in a edited state.
886// let window = cx.windows()[0].downcast::<Workspace>().unwrap();
887// let workspace = window.root(cx);
888// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
889// let editor = workspace.read_with(cx, |workspace, cx| {
890// workspace
891// .active_item(cx)
892// .unwrap()
893// .downcast::<Editor>()
894// .unwrap()
895// });
896// assert!(!window.is_edited(cx));
897
898// // Editing a buffer marks the window as edited.
899// editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
900// assert!(window.is_edited(cx));
901
902// // Undoing the edit restores the window's edited state.
903// editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
904// assert!(!window.is_edited(cx));
905
906// // Redoing the edit marks the window as edited again.
907// editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
908// assert!(window.is_edited(cx));
909
910// // Closing the item restores the window's edited state.
911// let close = pane.update(cx, |pane, cx| {
912// drop(editor);
913// pane.close_active_item(&Default::default(), cx).unwrap()
914// });
915// executor.run_until_parked();
916
917// window.simulate_prompt_answer(1, cx);
918// close.await.unwrap();
919// assert!(!window.is_edited(cx));
920
921// // Opening the buffer again doesn't impact the window's edited state.
922// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
923// .await
924// .unwrap();
925// let editor = workspace.read_with(cx, |workspace, cx| {
926// workspace
927// .active_item(cx)
928// .unwrap()
929// .downcast::<Editor>()
930// .unwrap()
931// });
932// assert!(!window.is_edited(cx));
933
934// // Editing the buffer marks the window as edited.
935// editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
936// assert!(window.is_edited(cx));
937
938// // Ensure closing the window via the mouse gets preempted due to the
939// // buffer having unsaved changes.
940// assert!(!window.simulate_close(cx));
941// executor.run_until_parked();
942// assert_eq!(cx.windows().len(), 1);
943
944// // The window is successfully closed after the user dismisses the prompt.
945// window.simulate_prompt_answer(1, cx);
946// executor.run_until_parked();
947// assert_eq!(cx.windows().len(), 0);
948// }
949
950// #[gpui::test]
951// async fn test_new_empty_workspace(cx: &mut TestAppContext) {
952// let app_state = init_test(cx);
953// cx.update(|cx| {
954// open_new(&app_state, cx, |workspace, cx| {
955// Editor::new_file(workspace, &Default::default(), cx)
956// })
957// })
958// .await;
959
960// let window = cx
961// .windows()
962// .first()
963// .unwrap()
964// .downcast::<Workspace>()
965// .unwrap();
966// let workspace = window.root(cx);
967
968// let editor = workspace.update(cx, |workspace, cx| {
969// workspace
970// .active_item(cx)
971// .unwrap()
972// .downcast::<editor::Editor>()
973// .unwrap()
974// });
975
976// editor.update(cx, |editor, cx| {
977// assert!(editor.text(cx).is_empty());
978// assert!(!editor.is_dirty(cx));
979// });
980
981// let save_task = workspace.update(cx, |workspace, cx| {
982// workspace.save_active_item(SaveIntent::Save, cx)
983// });
984// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
985// cx.foreground().run_until_parked();
986// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
987// save_task.await.unwrap();
988// editor.read_with(cx, |editor, cx| {
989// assert!(!editor.is_dirty(cx));
990// assert_eq!(editor.title(cx), "the-new-name");
991// });
992// }
993
994// #[gpui::test]
995// async fn test_open_entry(cx: &mut TestAppContext) {
996// let app_state = init_test(cx);
997// app_state
998// .fs
999// .as_fake()
1000// .insert_tree(
1001// "/root",
1002// json!({
1003// "a": {
1004// "file1": "contents 1",
1005// "file2": "contents 2",
1006// "file3": "contents 3",
1007// },
1008// }),
1009// )
1010// .await;
1011
1012// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1013// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1014// let workspace = window.root(cx);
1015
1016// let entries = cx.read(|cx| workspace.file_project_paths(cx));
1017// let file1 = entries[0].clone();
1018// let file2 = entries[1].clone();
1019// let file3 = entries[2].clone();
1020
1021// // Open the first entry
1022// let entry_1 = workspace
1023// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1024// .await
1025// .unwrap();
1026// cx.read(|cx| {
1027// let pane = workspace.read(cx).active_pane().read(cx);
1028// assert_eq!(
1029// pane.active_item().unwrap().project_path(cx),
1030// Some(file1.clone())
1031// );
1032// assert_eq!(pane.items_len(), 1);
1033// });
1034
1035// // Open the second entry
1036// workspace
1037// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1038// .await
1039// .unwrap();
1040// cx.read(|cx| {
1041// let pane = workspace.read(cx).active_pane().read(cx);
1042// assert_eq!(
1043// pane.active_item().unwrap().project_path(cx),
1044// Some(file2.clone())
1045// );
1046// assert_eq!(pane.items_len(), 2);
1047// });
1048
1049// // Open the first entry again. The existing pane item is activated.
1050// let entry_1b = workspace
1051// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1052// .await
1053// .unwrap();
1054// assert_eq!(entry_1.id(), entry_1b.id());
1055
1056// cx.read(|cx| {
1057// let pane = workspace.read(cx).active_pane().read(cx);
1058// assert_eq!(
1059// pane.active_item().unwrap().project_path(cx),
1060// Some(file1.clone())
1061// );
1062// assert_eq!(pane.items_len(), 2);
1063// });
1064
1065// // Split the pane with the first entry, then open the second entry again.
1066// workspace
1067// .update(cx, |w, cx| {
1068// w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
1069// w.open_path(file2.clone(), None, true, cx)
1070// })
1071// .await
1072// .unwrap();
1073
1074// workspace.read_with(cx, |w, cx| {
1075// assert_eq!(
1076// w.active_pane()
1077// .read(cx)
1078// .active_item()
1079// .unwrap()
1080// .project_path(cx),
1081// Some(file2.clone())
1082// );
1083// });
1084
1085// // Open the third entry twice concurrently. Only one pane item is added.
1086// let (t1, t2) = workspace.update(cx, |w, cx| {
1087// (
1088// w.open_path(file3.clone(), None, true, cx),
1089// w.open_path(file3.clone(), None, true, cx),
1090// )
1091// });
1092// t1.await.unwrap();
1093// t2.await.unwrap();
1094// cx.read(|cx| {
1095// let pane = workspace.read(cx).active_pane().read(cx);
1096// assert_eq!(
1097// pane.active_item().unwrap().project_path(cx),
1098// Some(file3.clone())
1099// );
1100// let pane_entries = pane
1101// .items()
1102// .map(|i| i.project_path(cx).unwrap())
1103// .collect::<Vec<_>>();
1104// assert_eq!(pane_entries, &[file1, file2, file3]);
1105// });
1106// }
1107
1108// #[gpui::test]
1109// async fn test_open_paths(cx: &mut TestAppContext) {
1110// let app_state = init_test(cx);
1111
1112// app_state
1113// .fs
1114// .as_fake()
1115// .insert_tree(
1116// "/",
1117// json!({
1118// "dir1": {
1119// "a.txt": ""
1120// },
1121// "dir2": {
1122// "b.txt": ""
1123// },
1124// "dir3": {
1125// "c.txt": ""
1126// },
1127// "d.txt": ""
1128// }),
1129// )
1130// .await;
1131
1132// cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
1133// .await
1134// .unwrap();
1135// assert_eq!(cx.windows().len(), 1);
1136// let workspace = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
1137
1138// #[track_caller]
1139// fn assert_project_panel_selection(
1140// workspace: &Workspace,
1141// expected_worktree_path: &Path,
1142// expected_entry_path: &Path,
1143// cx: &AppContext,
1144// ) {
1145// let project_panel = [
1146// workspace.left_dock().read(cx).panel::<ProjectPanel>(),
1147// workspace.right_dock().read(cx).panel::<ProjectPanel>(),
1148// workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
1149// ]
1150// .into_iter()
1151// .find_map(std::convert::identity)
1152// .expect("found no project panels")
1153// .read(cx);
1154// let (selected_worktree, selected_entry) = project_panel
1155// .selected_entry(cx)
1156// .expect("project panel should have a selected entry");
1157// assert_eq!(
1158// selected_worktree.abs_path().as_ref(),
1159// expected_worktree_path,
1160// "Unexpected project panel selected worktree path"
1161// );
1162// assert_eq!(
1163// selected_entry.path.as_ref(),
1164// expected_entry_path,
1165// "Unexpected project panel selected entry path"
1166// );
1167// }
1168
1169// // Open a file within an existing worktree.
1170// workspace
1171// .update(cx, |view, cx| {
1172// view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
1173// })
1174// .await;
1175// cx.read(|cx| {
1176// let workspace = workspace.read(cx);
1177// assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
1178// assert_eq!(
1179// workspace
1180// .active_pane()
1181// .read(cx)
1182// .active_item()
1183// .unwrap()
1184// .as_any()
1185// .downcast_ref::<Editor>()
1186// .unwrap()
1187// .read(cx)
1188// .title(cx),
1189// "a.txt"
1190// );
1191// });
1192
1193// // Open a file outside of any existing worktree.
1194// workspace
1195// .update(cx, |view, cx| {
1196// view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
1197// })
1198// .await;
1199// cx.read(|cx| {
1200// let workspace = workspace.read(cx);
1201// assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
1202// let worktree_roots = workspace
1203// .worktrees(cx)
1204// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1205// .collect::<HashSet<_>>();
1206// assert_eq!(
1207// worktree_roots,
1208// vec!["/dir1", "/dir2/b.txt"]
1209// .into_iter()
1210// .map(Path::new)
1211// .collect(),
1212// );
1213// assert_eq!(
1214// workspace
1215// .active_pane()
1216// .read(cx)
1217// .active_item()
1218// .unwrap()
1219// .as_any()
1220// .downcast_ref::<Editor>()
1221// .unwrap()
1222// .read(cx)
1223// .title(cx),
1224// "b.txt"
1225// );
1226// });
1227
1228// // Ensure opening a directory and one of its children only adds one worktree.
1229// workspace
1230// .update(cx, |view, cx| {
1231// view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
1232// })
1233// .await;
1234// cx.read(|cx| {
1235// let workspace = workspace.read(cx);
1236// assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
1237// let worktree_roots = workspace
1238// .worktrees(cx)
1239// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1240// .collect::<HashSet<_>>();
1241// assert_eq!(
1242// worktree_roots,
1243// vec!["/dir1", "/dir2/b.txt", "/dir3"]
1244// .into_iter()
1245// .map(Path::new)
1246// .collect(),
1247// );
1248// assert_eq!(
1249// workspace
1250// .active_pane()
1251// .read(cx)
1252// .active_item()
1253// .unwrap()
1254// .as_any()
1255// .downcast_ref::<Editor>()
1256// .unwrap()
1257// .read(cx)
1258// .title(cx),
1259// "c.txt"
1260// );
1261// });
1262
1263// // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
1264// workspace
1265// .update(cx, |view, cx| {
1266// view.open_paths(vec!["/d.txt".into()], false, cx)
1267// })
1268// .await;
1269// cx.read(|cx| {
1270// let workspace = workspace.read(cx);
1271// assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
1272// let worktree_roots = workspace
1273// .worktrees(cx)
1274// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1275// .collect::<HashSet<_>>();
1276// assert_eq!(
1277// worktree_roots,
1278// vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
1279// .into_iter()
1280// .map(Path::new)
1281// .collect(),
1282// );
1283
1284// let visible_worktree_roots = workspace
1285// .visible_worktrees(cx)
1286// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1287// .collect::<HashSet<_>>();
1288// assert_eq!(
1289// visible_worktree_roots,
1290// vec!["/dir1", "/dir2/b.txt", "/dir3"]
1291// .into_iter()
1292// .map(Path::new)
1293// .collect(),
1294// );
1295
1296// assert_eq!(
1297// workspace
1298// .active_pane()
1299// .read(cx)
1300// .active_item()
1301// .unwrap()
1302// .as_any()
1303// .downcast_ref::<Editor>()
1304// .unwrap()
1305// .read(cx)
1306// .title(cx),
1307// "d.txt"
1308// );
1309// });
1310// }
1311
1312// #[gpui::test]
1313// async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
1314// let app_state = init_test(cx);
1315// cx.update(|cx| {
1316// cx.update_global::<SettingsStore, _, _>(|store, cx| {
1317// store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
1318// project_settings.file_scan_exclusions =
1319// Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
1320// });
1321// });
1322// });
1323// app_state
1324// .fs
1325// .as_fake()
1326// .insert_tree(
1327// "/root",
1328// json!({
1329// ".gitignore": "ignored_dir\n",
1330// ".git": {
1331// "HEAD": "ref: refs/heads/main",
1332// },
1333// "regular_dir": {
1334// "file": "regular file contents",
1335// },
1336// "ignored_dir": {
1337// "ignored_subdir": {
1338// "file": "ignored subfile contents",
1339// },
1340// "file": "ignored file contents",
1341// },
1342// "excluded_dir": {
1343// "file": "excluded file contents",
1344// },
1345// }),
1346// )
1347// .await;
1348
1349// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1350// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1351// let workspace = window.root(cx);
1352
1353// let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
1354// let paths_to_open = [
1355// Path::new("/root/excluded_dir/file").to_path_buf(),
1356// Path::new("/root/.git/HEAD").to_path_buf(),
1357// Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
1358// ];
1359// let (opened_workspace, new_items) = cx
1360// .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
1361// .await
1362// .unwrap();
1363
1364// assert_eq!(
1365// opened_workspace.id(),
1366// workspace.id(),
1367// "Excluded files in subfolders of a workspace root should be opened in the workspace"
1368// );
1369// let mut opened_paths = cx.read(|cx| {
1370// assert_eq!(
1371// new_items.len(),
1372// paths_to_open.len(),
1373// "Expect to get the same number of opened items as submitted paths to open"
1374// );
1375// new_items
1376// .iter()
1377// .zip(paths_to_open.iter())
1378// .map(|(i, path)| {
1379// match i {
1380// Some(Ok(i)) => {
1381// Some(i.project_path(cx).map(|p| p.path.display().to_string()))
1382// }
1383// Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
1384// None => None,
1385// }
1386// .flatten()
1387// })
1388// .collect::<Vec<_>>()
1389// });
1390// opened_paths.sort();
1391// assert_eq!(
1392// opened_paths,
1393// vec![
1394// None,
1395// Some(".git/HEAD".to_string()),
1396// Some("excluded_dir/file".to_string()),
1397// ],
1398// "Excluded files should get opened, excluded dir should not get opened"
1399// );
1400
1401// let entries = cx.read(|cx| workspace.file_project_paths(cx));
1402// assert_eq!(
1403// initial_entries, entries,
1404// "Workspace entries should not change after opening excluded files and directories paths"
1405// );
1406
1407// cx.read(|cx| {
1408// let pane = workspace.read(cx).active_pane().read(cx);
1409// let mut opened_buffer_paths = pane
1410// .items()
1411// .map(|i| {
1412// i.project_path(cx)
1413// .expect("all excluded files that got open should have a path")
1414// .path
1415// .display()
1416// .to_string()
1417// })
1418// .collect::<Vec<_>>();
1419// opened_buffer_paths.sort();
1420// assert_eq!(
1421// opened_buffer_paths,
1422// vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
1423// "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
1424// );
1425// });
1426// }
1427
1428// #[gpui::test]
1429// async fn test_save_conflicting_item(cx: &mut TestAppContext) {
1430// let app_state = init_test(cx);
1431// app_state
1432// .fs
1433// .as_fake()
1434// .insert_tree("/root", json!({ "a.txt": "" }))
1435// .await;
1436
1437// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1438// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1439// let workspace = window.root(cx);
1440
1441// // Open a file within an existing worktree.
1442// workspace
1443// .update(cx, |view, cx| {
1444// view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
1445// })
1446// .await;
1447// let editor = cx.read(|cx| {
1448// let pane = workspace.read(cx).active_pane().read(cx);
1449// let item = pane.active_item().unwrap();
1450// item.downcast::<Editor>().unwrap()
1451// });
1452
1453// editor.update(cx, |editor, cx| editor.handle_input("x", cx));
1454// app_state
1455// .fs
1456// .as_fake()
1457// .insert_file("/root/a.txt", "changed".to_string())
1458// .await;
1459// editor
1460// .condition(cx, |editor, cx| editor.has_conflict(cx))
1461// .await;
1462// cx.read(|cx| assert!(editor.is_dirty(cx)));
1463
1464// let save_task = workspace.update(cx, |workspace, cx| {
1465// workspace.save_active_item(SaveIntent::Save, cx)
1466// });
1467// cx.foreground().run_until_parked();
1468// window.simulate_prompt_answer(0, cx);
1469// save_task.await.unwrap();
1470// editor.read_with(cx, |editor, cx| {
1471// assert!(!editor.is_dirty(cx));
1472// assert!(!editor.has_conflict(cx));
1473// });
1474// }
1475
1476// #[gpui::test]
1477// async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
1478// let app_state = init_test(cx);
1479// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1480
1481// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1482// project.update(cx, |project, _| project.languages().add(rust_lang()));
1483// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1484// let workspace = window.root(cx);
1485// let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
1486
1487// // Create a new untitled buffer
1488// cx.dispatch_action(window.into(), NewFile);
1489// let editor = workspace.read_with(cx, |workspace, cx| {
1490// workspace
1491// .active_item(cx)
1492// .unwrap()
1493// .downcast::<Editor>()
1494// .unwrap()
1495// });
1496
1497// editor.update(cx, |editor, cx| {
1498// assert!(!editor.is_dirty(cx));
1499// assert_eq!(editor.title(cx), "untitled");
1500// assert!(Arc::ptr_eq(
1501// &editor.language_at(0, cx).unwrap(),
1502// &languages::PLAIN_TEXT
1503// ));
1504// editor.handle_input("hi", cx);
1505// assert!(editor.is_dirty(cx));
1506// });
1507
1508// // Save the buffer. This prompts for a filename.
1509// let save_task = workspace.update(cx, |workspace, cx| {
1510// workspace.save_active_item(SaveIntent::Save, cx)
1511// });
1512// cx.foreground().run_until_parked();
1513// cx.simulate_new_path_selection(|parent_dir| {
1514// assert_eq!(parent_dir, Path::new("/root"));
1515// Some(parent_dir.join("the-new-name.rs"))
1516// });
1517// cx.read(|cx| {
1518// assert!(editor.is_dirty(cx));
1519// assert_eq!(editor.read(cx).title(cx), "untitled");
1520// });
1521
1522// // When the save completes, the buffer's title is updated and the language is assigned based
1523// // on the path.
1524// save_task.await.unwrap();
1525// editor.read_with(cx, |editor, cx| {
1526// assert!(!editor.is_dirty(cx));
1527// assert_eq!(editor.title(cx), "the-new-name.rs");
1528// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
1529// });
1530
1531// // Edit the file and save it again. This time, there is no filename prompt.
1532// editor.update(cx, |editor, cx| {
1533// editor.handle_input(" there", cx);
1534// assert!(editor.is_dirty(cx));
1535// });
1536// let save_task = workspace.update(cx, |workspace, cx| {
1537// workspace.save_active_item(SaveIntent::Save, cx)
1538// });
1539// save_task.await.unwrap();
1540// assert!(!cx.did_prompt_for_new_path());
1541// editor.read_with(cx, |editor, cx| {
1542// assert!(!editor.is_dirty(cx));
1543// assert_eq!(editor.title(cx), "the-new-name.rs")
1544// });
1545
1546// // Open the same newly-created file in another pane item. The new editor should reuse
1547// // the same buffer.
1548// cx.dispatch_action(window.into(), NewFile);
1549// workspace
1550// .update(cx, |workspace, cx| {
1551// workspace.split_and_clone(
1552// workspace.active_pane().clone(),
1553// SplitDirection::Right,
1554// cx,
1555// );
1556// workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
1557// })
1558// .await
1559// .unwrap();
1560// let editor2 = workspace.update(cx, |workspace, cx| {
1561// workspace
1562// .active_item(cx)
1563// .unwrap()
1564// .downcast::<Editor>()
1565// .unwrap()
1566// });
1567// cx.read(|cx| {
1568// assert_eq!(
1569// editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
1570// editor.read(cx).buffer().read(cx).as_singleton().unwrap()
1571// );
1572// })
1573// }
1574
1575// #[gpui::test]
1576// async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
1577// let app_state = init_test(cx);
1578// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1579
1580// let project = Project::test(app_state.fs.clone(), [], cx).await;
1581// project.update(cx, |project, _| project.languages().add(rust_lang()));
1582// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1583// let workspace = window.root(cx);
1584
1585// // Create a new untitled buffer
1586// cx.dispatch_action(window.into(), NewFile);
1587// let editor = workspace.read_with(cx, |workspace, cx| {
1588// workspace
1589// .active_item(cx)
1590// .unwrap()
1591// .downcast::<Editor>()
1592// .unwrap()
1593// });
1594
1595// editor.update(cx, |editor, cx| {
1596// assert!(Arc::ptr_eq(
1597// &editor.language_at(0, cx).unwrap(),
1598// &languages::PLAIN_TEXT
1599// ));
1600// editor.handle_input("hi", cx);
1601// assert!(editor.is_dirty(cx));
1602// });
1603
1604// // Save the buffer. This prompts for a filename.
1605// let save_task = workspace.update(cx, |workspace, cx| {
1606// workspace.save_active_item(SaveIntent::Save, cx)
1607// });
1608// cx.foreground().run_until_parked();
1609// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
1610// save_task.await.unwrap();
1611// // The buffer is not dirty anymore and the language is assigned based on the path.
1612// editor.read_with(cx, |editor, cx| {
1613// assert!(!editor.is_dirty(cx));
1614// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
1615// });
1616// }
1617
1618// #[gpui::test]
1619// async fn test_pane_actions(cx: &mut TestAppContext) {
1620// let app_state = init_test(cx);
1621// app_state
1622// .fs
1623// .as_fake()
1624// .insert_tree(
1625// "/root",
1626// json!({
1627// "a": {
1628// "file1": "contents 1",
1629// "file2": "contents 2",
1630// "file3": "contents 3",
1631// },
1632// }),
1633// )
1634// .await;
1635
1636// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1637// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
1638// let workspace = window.root(cx);
1639
1640// let entries = cx.read(|cx| workspace.file_project_paths(cx));
1641// let file1 = entries[0].clone();
1642
1643// let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1644
1645// workspace
1646// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1647// .await
1648// .unwrap();
1649
1650// let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
1651// let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
1652// assert_eq!(editor.project_path(cx), Some(file1.clone()));
1653// let buffer = editor.update(cx, |editor, cx| {
1654// editor.insert("dirt", cx);
1655// editor.buffer().downgrade()
1656// });
1657// (editor.downgrade(), buffer)
1658// });
1659
1660// cx.dispatch_action(window.into(), pane::SplitRight);
1661// let editor_2 = cx.update(|cx| {
1662// let pane_2 = workspace.read(cx).active_pane().clone();
1663// assert_ne!(pane_1, pane_2);
1664
1665// let pane2_item = pane_2.read(cx).active_item().unwrap();
1666// assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
1667
1668// pane2_item.downcast::<Editor>().unwrap().downgrade()
1669// });
1670// cx.dispatch_action(
1671// window.into(),
1672// workspace::CloseActiveItem { save_intent: None },
1673// );
1674
1675// cx.foreground().run_until_parked();
1676// workspace.read_with(cx, |workspace, _| {
1677// assert_eq!(workspace.panes().len(), 1);
1678// assert_eq!(workspace.active_pane(), &pane_1);
1679// });
1680
1681// cx.dispatch_action(
1682// window.into(),
1683// workspace::CloseActiveItem { save_intent: None },
1684// );
1685// cx.foreground().run_until_parked();
1686// window.simulate_prompt_answer(1, cx);
1687// cx.foreground().run_until_parked();
1688
1689// workspace.read_with(cx, |workspace, cx| {
1690// assert_eq!(workspace.panes().len(), 1);
1691// assert!(workspace.active_item(cx).is_none());
1692// });
1693
1694// cx.assert_dropped(editor_1);
1695// cx.assert_dropped(editor_2);
1696// cx.assert_dropped(buffer);
1697// }
1698
1699// #[gpui::test]
1700// async fn test_navigation(cx: &mut TestAppContext) {
1701// let app_state = init_test(cx);
1702// app_state
1703// .fs
1704// .as_fake()
1705// .insert_tree(
1706// "/root",
1707// json!({
1708// "a": {
1709// "file1": "contents 1\n".repeat(20),
1710// "file2": "contents 2\n".repeat(20),
1711// "file3": "contents 3\n".repeat(20),
1712// },
1713// }),
1714// )
1715// .await;
1716
1717// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1718// let workspace = cx
1719// .add_window(|cx| Workspace::test_new(project.clone(), cx))
1720// .root(cx);
1721// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1722
1723// let entries = cx.read(|cx| workspace.file_project_paths(cx));
1724// let file1 = entries[0].clone();
1725// let file2 = entries[1].clone();
1726// let file3 = entries[2].clone();
1727
1728// let editor1 = workspace
1729// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1730// .await
1731// .unwrap()
1732// .downcast::<Editor>()
1733// .unwrap();
1734// editor1.update(cx, |editor, cx| {
1735// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1736// s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
1737// });
1738// });
1739// let editor2 = workspace
1740// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1741// .await
1742// .unwrap()
1743// .downcast::<Editor>()
1744// .unwrap();
1745// let editor3 = workspace
1746// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1747// .await
1748// .unwrap()
1749// .downcast::<Editor>()
1750// .unwrap();
1751
1752// editor3
1753// .update(cx, |editor, cx| {
1754// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1755// s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
1756// });
1757// editor.newline(&Default::default(), cx);
1758// editor.newline(&Default::default(), cx);
1759// editor.move_down(&Default::default(), cx);
1760// editor.move_down(&Default::default(), cx);
1761// editor.save(project.clone(), cx)
1762// })
1763// .await
1764// .unwrap();
1765// editor3.update(cx, |editor, cx| {
1766// editor.set_scroll_position(vec2f(0., 12.5), cx)
1767// });
1768// assert_eq!(
1769// active_location(&workspace, cx),
1770// (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1771// );
1772
1773// workspace
1774// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1775// .await
1776// .unwrap();
1777// assert_eq!(
1778// active_location(&workspace, cx),
1779// (file3.clone(), DisplayPoint::new(0, 0), 0.)
1780// );
1781
1782// workspace
1783// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1784// .await
1785// .unwrap();
1786// assert_eq!(
1787// active_location(&workspace, cx),
1788// (file2.clone(), DisplayPoint::new(0, 0), 0.)
1789// );
1790
1791// workspace
1792// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1793// .await
1794// .unwrap();
1795// assert_eq!(
1796// active_location(&workspace, cx),
1797// (file1.clone(), DisplayPoint::new(10, 0), 0.)
1798// );
1799
1800// workspace
1801// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1802// .await
1803// .unwrap();
1804// assert_eq!(
1805// active_location(&workspace, cx),
1806// (file1.clone(), DisplayPoint::new(0, 0), 0.)
1807// );
1808
1809// // Go back one more time and ensure we don't navigate past the first item in the history.
1810// workspace
1811// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1812// .await
1813// .unwrap();
1814// assert_eq!(
1815// active_location(&workspace, cx),
1816// (file1.clone(), DisplayPoint::new(0, 0), 0.)
1817// );
1818
1819// workspace
1820// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1821// .await
1822// .unwrap();
1823// assert_eq!(
1824// active_location(&workspace, cx),
1825// (file1.clone(), DisplayPoint::new(10, 0), 0.)
1826// );
1827
1828// workspace
1829// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1830// .await
1831// .unwrap();
1832// assert_eq!(
1833// active_location(&workspace, cx),
1834// (file2.clone(), DisplayPoint::new(0, 0), 0.)
1835// );
1836
1837// // Go forward to an item that has been closed, ensuring it gets re-opened at the same
1838// // location.
1839// pane.update(cx, |pane, cx| {
1840// let editor3_id = editor3.id();
1841// drop(editor3);
1842// pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
1843// })
1844// .await
1845// .unwrap();
1846// workspace
1847// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1848// .await
1849// .unwrap();
1850// assert_eq!(
1851// active_location(&workspace, cx),
1852// (file3.clone(), DisplayPoint::new(0, 0), 0.)
1853// );
1854
1855// workspace
1856// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1857// .await
1858// .unwrap();
1859// assert_eq!(
1860// active_location(&workspace, cx),
1861// (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1862// );
1863
1864// workspace
1865// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1866// .await
1867// .unwrap();
1868// assert_eq!(
1869// active_location(&workspace, cx),
1870// (file3.clone(), DisplayPoint::new(0, 0), 0.)
1871// );
1872
1873// // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
1874// pane.update(cx, |pane, cx| {
1875// let editor2_id = editor2.id();
1876// drop(editor2);
1877// pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
1878// })
1879// .await
1880// .unwrap();
1881// app_state
1882// .fs
1883// .remove_file(Path::new("/root/a/file2"), Default::default())
1884// .await
1885// .unwrap();
1886// cx.foreground().run_until_parked();
1887
1888// workspace
1889// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1890// .await
1891// .unwrap();
1892// assert_eq!(
1893// active_location(&workspace, cx),
1894// (file1.clone(), DisplayPoint::new(10, 0), 0.)
1895// );
1896// workspace
1897// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
1898// .await
1899// .unwrap();
1900// assert_eq!(
1901// active_location(&workspace, cx),
1902// (file3.clone(), DisplayPoint::new(0, 0), 0.)
1903// );
1904
1905// // Modify file to collapse multiple nav history entries into the same location.
1906// // Ensure we don't visit the same location twice when navigating.
1907// editor1.update(cx, |editor, cx| {
1908// editor.change_selections(None, cx, |s| {
1909// s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
1910// })
1911// });
1912
1913// for _ in 0..5 {
1914// editor1.update(cx, |editor, cx| {
1915// editor.change_selections(None, cx, |s| {
1916// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
1917// });
1918// });
1919// editor1.update(cx, |editor, cx| {
1920// editor.change_selections(None, cx, |s| {
1921// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
1922// })
1923// });
1924// }
1925
1926// editor1.update(cx, |editor, cx| {
1927// editor.transact(cx, |editor, cx| {
1928// editor.change_selections(None, cx, |s| {
1929// s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
1930// });
1931// editor.insert("", cx);
1932// })
1933// });
1934
1935// editor1.update(cx, |editor, cx| {
1936// editor.change_selections(None, cx, |s| {
1937// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1938// })
1939// });
1940// workspace
1941// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1942// .await
1943// .unwrap();
1944// assert_eq!(
1945// active_location(&workspace, cx),
1946// (file1.clone(), DisplayPoint::new(2, 0), 0.)
1947// );
1948// workspace
1949// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
1950// .await
1951// .unwrap();
1952// assert_eq!(
1953// active_location(&workspace, cx),
1954// (file1.clone(), DisplayPoint::new(3, 0), 0.)
1955// );
1956
1957// fn active_location(
1958// workspace: &ViewHandle<Workspace>,
1959// cx: &mut TestAppContext,
1960// ) -> (ProjectPath, DisplayPoint, f32) {
1961// workspace.update(cx, |workspace, cx| {
1962// let item = workspace.active_item(cx).unwrap();
1963// let editor = item.downcast::<Editor>().unwrap();
1964// let (selections, scroll_position) = editor.update(cx, |editor, cx| {
1965// (
1966// editor.selections.display_ranges(cx),
1967// editor.scroll_position(cx),
1968// )
1969// });
1970// (
1971// item.project_path(cx).unwrap(),
1972// selections[0].start,
1973// scroll_position.y(),
1974// )
1975// })
1976// }
1977// }
1978
1979// #[gpui::test]
1980// async fn test_reopening_closed_items(cx: &mut TestAppContext) {
1981// let app_state = init_test(cx);
1982// app_state
1983// .fs
1984// .as_fake()
1985// .insert_tree(
1986// "/root",
1987// json!({
1988// "a": {
1989// "file1": "",
1990// "file2": "",
1991// "file3": "",
1992// "file4": "",
1993// },
1994// }),
1995// )
1996// .await;
1997
1998// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1999// let workspace = cx
2000// .add_window(|cx| Workspace::test_new(project, cx))
2001// .root(cx);
2002// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
2003
2004// let entries = cx.read(|cx| workspace.file_project_paths(cx));
2005// let file1 = entries[0].clone();
2006// let file2 = entries[1].clone();
2007// let file3 = entries[2].clone();
2008// let file4 = entries[3].clone();
2009
2010// let file1_item_id = workspace
2011// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
2012// .await
2013// .unwrap()
2014// .id();
2015// let file2_item_id = workspace
2016// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
2017// .await
2018// .unwrap()
2019// .id();
2020// let file3_item_id = workspace
2021// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
2022// .await
2023// .unwrap()
2024// .id();
2025// let file4_item_id = workspace
2026// .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
2027// .await
2028// .unwrap()
2029// .id();
2030// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2031
2032// // Close all the pane items in some arbitrary order.
2033// pane.update(cx, |pane, cx| {
2034// pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
2035// })
2036// .await
2037// .unwrap();
2038// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2039
2040// pane.update(cx, |pane, cx| {
2041// pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
2042// })
2043// .await
2044// .unwrap();
2045// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2046
2047// pane.update(cx, |pane, cx| {
2048// pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
2049// })
2050// .await
2051// .unwrap();
2052// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2053
2054// pane.update(cx, |pane, cx| {
2055// pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
2056// })
2057// .await
2058// .unwrap();
2059// assert_eq!(active_path(&workspace, cx), None);
2060
2061// // Reopen all the closed items, ensuring they are reopened in the same order
2062// // in which they were closed.
2063// workspace
2064// .update(cx, Workspace::reopen_closed_item)
2065// .await
2066// .unwrap();
2067// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2068
2069// workspace
2070// .update(cx, Workspace::reopen_closed_item)
2071// .await
2072// .unwrap();
2073// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2074
2075// workspace
2076// .update(cx, Workspace::reopen_closed_item)
2077// .await
2078// .unwrap();
2079// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2080
2081// workspace
2082// .update(cx, Workspace::reopen_closed_item)
2083// .await
2084// .unwrap();
2085// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2086
2087// // Reopening past the last closed item is a no-op.
2088// workspace
2089// .update(cx, Workspace::reopen_closed_item)
2090// .await
2091// .unwrap();
2092// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2093
2094// // Reopening closed items doesn't interfere with navigation history.
2095// workspace
2096// .update(cx, |workspace, cx| {
2097// workspace.go_back(workspace.active_pane().downgrade(), cx)
2098// })
2099// .await
2100// .unwrap();
2101// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2102
2103// workspace
2104// .update(cx, |workspace, cx| {
2105// workspace.go_back(workspace.active_pane().downgrade(), cx)
2106// })
2107// .await
2108// .unwrap();
2109// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2110
2111// workspace
2112// .update(cx, |workspace, cx| {
2113// workspace.go_back(workspace.active_pane().downgrade(), cx)
2114// })
2115// .await
2116// .unwrap();
2117// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2118
2119// workspace
2120// .update(cx, |workspace, cx| {
2121// workspace.go_back(workspace.active_pane().downgrade(), cx)
2122// })
2123// .await
2124// .unwrap();
2125// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
2126
2127// workspace
2128// .update(cx, |workspace, cx| {
2129// workspace.go_back(workspace.active_pane().downgrade(), cx)
2130// })
2131// .await
2132// .unwrap();
2133// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
2134
2135// workspace
2136// .update(cx, |workspace, cx| {
2137// workspace.go_back(workspace.active_pane().downgrade(), cx)
2138// })
2139// .await
2140// .unwrap();
2141// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
2142
2143// workspace
2144// .update(cx, |workspace, cx| {
2145// workspace.go_back(workspace.active_pane().downgrade(), cx)
2146// })
2147// .await
2148// .unwrap();
2149// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2150
2151// workspace
2152// .update(cx, |workspace, cx| {
2153// workspace.go_back(workspace.active_pane().downgrade(), cx)
2154// })
2155// .await
2156// .unwrap();
2157// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
2158
2159// fn active_path(
2160// workspace: &ViewHandle<Workspace>,
2161// cx: &TestAppContext,
2162// ) -> Option<ProjectPath> {
2163// workspace.read_with(cx, |workspace, cx| {
2164// let item = workspace.active_item(cx)?;
2165// item.project_path(cx)
2166// })
2167// }
2168// }
2169
2170// #[gpui::test]
2171// async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
2172// struct TestView;
2173
2174// impl Entity for TestView {
2175// type Event = ();
2176// }
2177
2178// impl View for TestView {
2179// fn ui_name() -> &'static str {
2180// "TestView"
2181// }
2182
2183// fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
2184// Empty::new().into_any()
2185// }
2186// }
2187
2188// let executor = cx.background();
2189// let fs = FakeFs::new(executor.clone());
2190
2191// actions!(test, [A, B]);
2192// // From the Atom keymap
2193// actions!(workspace, [ActivatePreviousPane]);
2194// // From the JetBrains keymap
2195// actions!(pane, [ActivatePrevItem]);
2196
2197// fs.save(
2198// "/settings.json".as_ref(),
2199// &r#"
2200// {
2201// "base_keymap": "Atom"
2202// }
2203// "#
2204// .into(),
2205// Default::default(),
2206// )
2207// .await
2208// .unwrap();
2209
2210// fs.save(
2211// "/keymap.json".as_ref(),
2212// &r#"
2213// [
2214// {
2215// "bindings": {
2216// "backspace": "test::A"
2217// }
2218// }
2219// ]
2220// "#
2221// .into(),
2222// Default::default(),
2223// )
2224// .await
2225// .unwrap();
2226
2227// cx.update(|cx| {
2228// cx.set_global(SettingsStore::test(cx));
2229// theme::init(Assets, cx);
2230// welcome::init(cx);
2231
2232// cx.add_global_action(|_: &A, _cx| {});
2233// cx.add_global_action(|_: &B, _cx| {});
2234// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
2235// cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
2236
2237// let settings_rx = watch_config_file(
2238// executor.clone(),
2239// fs.clone(),
2240// PathBuf::from("/settings.json"),
2241// );
2242// let keymap_rx =
2243// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
2244
2245// handle_keymap_file_changes(keymap_rx, cx);
2246// handle_settings_file_changes(settings_rx, cx);
2247// });
2248
2249// cx.foreground().run_until_parked();
2250
2251// let window = cx.add_window(|_| TestView);
2252
2253// // Test loading the keymap base at all
2254// assert_key_bindings_for(
2255// window.into(),
2256// cx,
2257// vec![("backspace", &A), ("k", &ActivatePreviousPane)],
2258// line!(),
2259// );
2260
2261// // Test modifying the users keymap, while retaining the base keymap
2262// fs.save(
2263// "/keymap.json".as_ref(),
2264// &r#"
2265// [
2266// {
2267// "bindings": {
2268// "backspace": "test::B"
2269// }
2270// }
2271// ]
2272// "#
2273// .into(),
2274// Default::default(),
2275// )
2276// .await
2277// .unwrap();
2278
2279// cx.foreground().run_until_parked();
2280
2281// assert_key_bindings_for(
2282// window.into(),
2283// cx,
2284// vec![("backspace", &B), ("k", &ActivatePreviousPane)],
2285// line!(),
2286// );
2287
2288// // Test modifying the base, while retaining the users keymap
2289// fs.save(
2290// "/settings.json".as_ref(),
2291// &r#"
2292// {
2293// "base_keymap": "JetBrains"
2294// }
2295// "#
2296// .into(),
2297// Default::default(),
2298// )
2299// .await
2300// .unwrap();
2301
2302// cx.foreground().run_until_parked();
2303
2304// assert_key_bindings_for(
2305// window.into(),
2306// cx,
2307// vec![("backspace", &B), ("[", &ActivatePrevItem)],
2308// line!(),
2309// );
2310
2311// #[track_caller]
2312// fn assert_key_bindings_for<'a>(
2313// window: AnyWindowHandle,
2314// cx: &TestAppContext,
2315// actions: Vec<(&'static str, &'a dyn Action)>,
2316// line: u32,
2317// ) {
2318// for (key, action) in actions {
2319// // assert that...
2320// assert!(
2321// cx.available_actions(window, 0)
2322// .into_iter()
2323// .any(|(_, bound_action, b)| {
2324// // action names match...
2325// bound_action.name() == action.name()
2326// && bound_action.namespace() == action.namespace()
2327// // and key strokes contain the given key
2328// && b.iter()
2329// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
2330// }),
2331// "On {} Failed to find {} with key binding {}",
2332// line,
2333// action.name(),
2334// key
2335// );
2336// }
2337// }
2338// }
2339
2340// #[gpui::test]
2341// async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
2342// struct TestView;
2343
2344// impl Entity for TestView {
2345// type Event = ();
2346// }
2347
2348// impl View for TestView {
2349// fn ui_name() -> &'static str {
2350// "TestView"
2351// }
2352
2353// fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
2354// Empty::new().into_any()
2355// }
2356// }
2357
2358// let executor = cx.background();
2359// let fs = FakeFs::new(executor.clone());
2360
2361// actions!(test, [A, B]);
2362// // From the Atom keymap
2363// actions!(workspace, [ActivatePreviousPane]);
2364// // From the JetBrains keymap
2365// actions!(pane, [ActivatePrevItem]);
2366
2367// fs.save(
2368// "/settings.json".as_ref(),
2369// &r#"
2370// {
2371// "base_keymap": "Atom"
2372// }
2373// "#
2374// .into(),
2375// Default::default(),
2376// )
2377// .await
2378// .unwrap();
2379
2380// fs.save(
2381// "/keymap.json".as_ref(),
2382// &r#"
2383// [
2384// {
2385// "bindings": {
2386// "backspace": "test::A"
2387// }
2388// }
2389// ]
2390// "#
2391// .into(),
2392// Default::default(),
2393// )
2394// .await
2395// .unwrap();
2396
2397// cx.update(|cx| {
2398// cx.set_global(SettingsStore::test(cx));
2399// theme::init(Assets, cx);
2400// welcome::init(cx);
2401
2402// cx.add_global_action(|_: &A, _cx| {});
2403// cx.add_global_action(|_: &B, _cx| {});
2404// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
2405// cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
2406
2407// let settings_rx = watch_config_file(
2408// executor.clone(),
2409// fs.clone(),
2410// PathBuf::from("/settings.json"),
2411// );
2412// let keymap_rx =
2413// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
2414
2415// handle_keymap_file_changes(keymap_rx, cx);
2416// handle_settings_file_changes(settings_rx, cx);
2417// });
2418
2419// cx.foreground().run_until_parked();
2420
2421// let window = cx.add_window(|_| TestView);
2422
2423// // Test loading the keymap base at all
2424// assert_key_bindings_for(
2425// window.into(),
2426// cx,
2427// vec![("backspace", &A), ("k", &ActivatePreviousPane)],
2428// line!(),
2429// );
2430
2431// // Test disabling the key binding for the base keymap
2432// fs.save(
2433// "/keymap.json".as_ref(),
2434// &r#"
2435// [
2436// {
2437// "bindings": {
2438// "backspace": null
2439// }
2440// }
2441// ]
2442// "#
2443// .into(),
2444// Default::default(),
2445// )
2446// .await
2447// .unwrap();
2448
2449// cx.foreground().run_until_parked();
2450
2451// assert_key_bindings_for(
2452// window.into(),
2453// cx,
2454// vec![("k", &ActivatePreviousPane)],
2455// line!(),
2456// );
2457
2458// // Test modifying the base, while retaining the users keymap
2459// fs.save(
2460// "/settings.json".as_ref(),
2461// &r#"
2462// {
2463// "base_keymap": "JetBrains"
2464// }
2465// "#
2466// .into(),
2467// Default::default(),
2468// )
2469// .await
2470// .unwrap();
2471
2472// cx.foreground().run_until_parked();
2473
2474// assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!());
2475
2476// #[track_caller]
2477// fn assert_key_bindings_for<'a>(
2478// window: AnyWindowHandle,
2479// cx: &TestAppContext,
2480// actions: Vec<(&'static str, &'a dyn Action)>,
2481// line: u32,
2482// ) {
2483// for (key, action) in actions {
2484// // assert that...
2485// assert!(
2486// cx.available_actions(window, 0)
2487// .into_iter()
2488// .any(|(_, bound_action, b)| {
2489// // action names match...
2490// bound_action.name() == action.name()
2491// && bound_action.namespace() == action.namespace()
2492// // and key strokes contain the given key
2493// && b.iter()
2494// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
2495// }),
2496// "On {} Failed to find {} with key binding {}",
2497// line,
2498// action.name(),
2499// key
2500// );
2501// }
2502// }
2503// }
2504
2505// #[gpui::test]
2506// fn test_bundled_settings_and_themes(cx: &mut AppContext) {
2507// cx.platform()
2508// .fonts()
2509// .add_fonts(&[
2510// Assets
2511// .load("fonts/zed-sans/zed-sans-extended.ttf")
2512// .unwrap()
2513// .to_vec()
2514// .into(),
2515// Assets
2516// .load("fonts/zed-mono/zed-mono-extended.ttf")
2517// .unwrap()
2518// .to_vec()
2519// .into(),
2520// Assets
2521// .load("fonts/plex/IBMPlexSans-Regular.ttf")
2522// .unwrap()
2523// .to_vec()
2524// .into(),
2525// ])
2526// .unwrap();
2527// let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
2528// let mut settings = SettingsStore::default();
2529// settings
2530// .set_default_settings(&settings::default_settings(), cx)
2531// .unwrap();
2532// cx.set_global(settings);
2533// theme::init(Assets, cx);
2534
2535// let mut has_default_theme = false;
2536// for theme_name in themes.list(false).map(|meta| meta.name) {
2537// let theme = themes.get(&theme_name).unwrap();
2538// assert_eq!(theme.meta.name, theme_name);
2539// if theme.meta.name == settings::get::<ThemeSettings>(cx).theme.meta.name {
2540// has_default_theme = true;
2541// }
2542// }
2543// assert!(has_default_theme);
2544// }
2545
2546// #[gpui::test]
2547// fn test_bundled_languages(cx: &mut AppContext) {
2548// cx.set_global(SettingsStore::test(cx));
2549// let mut languages = LanguageRegistry::test();
2550// languages.set_executor(cx.background().clone());
2551// let languages = Arc::new(languages);
2552// let node_runtime = node_runtime::FakeNodeRuntime::new();
2553// languages::init(languages.clone(), node_runtime, cx);
2554// for name in languages.language_names() {
2555// languages.language_for_name(&name);
2556// }
2557// cx.foreground().run_until_parked();
2558// }
2559
2560// fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
2561// cx.foreground().forbid_parking();
2562// cx.update(|cx| {
2563// let mut app_state = AppState::test(cx);
2564// let state = Arc::get_mut(&mut app_state).unwrap();
2565// state.initialize_workspace = initialize_workspace;
2566// state.build_window_options = build_window_options;
2567// theme::init((), cx);
2568// audio::init((), cx);
2569// channel::init(&app_state.client, app_state.user_store.clone(), cx);
2570// call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
2571// notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
2572// workspace::init(app_state.clone(), cx);
2573// Project::init_settings(cx);
2574// language::init(cx);
2575// editor::init(cx);
2576// project_panel::init_settings(cx);
2577// collab_ui::init(&app_state, cx);
2578// pane::init(cx);
2579// project_panel::init((), cx);
2580// terminal_view::init(cx);
2581// assistant::init(cx);
2582// app_state
2583// })
2584// }
2585
2586// fn rust_lang() -> Arc<language::Language> {
2587// Arc::new(language::Language::new(
2588// language::LanguageConfig {
2589// name: "Rust".into(),
2590// path_suffixes: vec!["rs".to_string()],
2591// ..Default::default()
2592// },
2593// Some(tree_sitter_rust::language()),
2594// ))
2595// }
2596// }