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