use std::any::{Any, TypeId}; use anyhow::Result;use collections::HashSet;use diff::BufferDiff;use editor::{scroll::Autoscroll, Editor, EditorEvent};use feature_flags::FeatureFlagViewExt;use futures::StreamExt;use gpui::{ actions, AnyElement, AnyView, App, AppContext, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Render, Subscription, Task, WeakEntity,};use language::{Anchor, Buffer, Capability, OffsetRangeExt, Point};use multi_buffer::{MultiBuffer, PathKey};use project::{git::GitState, Project, ProjectPath};use theme::ActiveTheme;use ui::prelude::*;use util::ResultExt as _;use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, searchable::SearchableItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace,}; use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry}; actions!(git, [Diff]); pub(crate) struct ProjectDiff { multibuffer: Entity<MultiBuffer>, editor: Entity<Editor>, project: Entity<Project>, git_panel: Entity<GitPanel>, git_state: Entity<GitState>, workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, update_needed: postage::watch::Sender<()>, pending_scroll: Option<PathKey>, _task: Task<Result<()>>, _subscription: Subscription,} struct DiffBuffer { path_key: PathKey, buffer: Entity<Buffer>, diff: Entity<BufferDiff>,} const CONFLICT_NAMESPACE: &'static str = "0";const TRACKED_NAMESPACE: &'static str = "1";const NEW_NAMESPACE: &'static str = "2"; impl ProjectDiff { pub(crate) fn register( _: &mut Workspace, window: Option<&mut Window>, cx: &mut Context<Workspace>, ) { let Some(window) = window else { return }; cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| { workspace.register_action(Self::deploy); }); } fn deploy( workspace: &mut Workspace, _: &Diff, window: &mut Window, cx: &mut Context<Workspace>, ) { Self::deploy_at(workspace, None, window, cx) } pub fn deploy_at( workspace: &mut Workspace, entry: Option<GitStatusEntry>, window: &mut Window, cx: &mut Context<Workspace>, ) { let project_diff = if let Some(existing) = workspace.item_of_type::<Self>(cx) { workspace.activate_item(&existing, true, true, window, cx); existing } else { let workspace_handle = cx.entity(); let project_diff = cx.new(|cx| { Self::new( workspace.project().clone(), workspace_handle, workspace.panel::<GitPanel>(cx).unwrap(), window, cx, ) }); workspace.add_item_to_active_pane( Box::new(project_diff.clone()), None, true, window, cx, ); project_diff }; if let Some(entry) = entry { project_diff.update(cx, |project_diff, cx| { project_diff.scroll_to(entry, window, cx); }) } } fn new( project: Entity<Project>, workspace: Entity<Workspace>, git_panel: Entity<GitPanel>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); let editor = cx.new(|cx| { let mut diff_display_editor = Editor::for_multibuffer( multibuffer.clone(), Some(project.clone()), true, window, cx, ); diff_display_editor.set_expand_all_diff_hunks(cx); diff_display_editor.register_addon(GitPanelAddon { git_panel: git_panel.clone(), }); diff_display_editor }); cx.subscribe_in(&editor, window, Self::handle_editor_event) .detach(); let git_state = project.read(cx).git_state().clone(); let git_state_subscription = cx.subscribe_in( &git_state, window, move |this, _git_state, _event, _window, _cx| { *this.update_needed.borrow_mut() = (); }, ); let (mut send, recv) = postage::watch::channel::<()>(); let worker = window.spawn(cx, { let this = cx.weak_entity(); |cx| Self::handle_status_updates(this, recv, cx) }); // Kick of a refresh immediately *send.borrow_mut() = (); Self { project, git_state: git_state.clone(), git_panel: git_panel.clone(), workspace: workspace.downgrade(), focus_handle, editor, multibuffer, pending_scroll: None, update_needed: send, _task: worker, _subscription: git_state_subscription, } } pub fn scroll_to( &mut self, entry: GitStatusEntry, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.git_state.read(cx).active_repository() else { return; }; let repo = git_repo.read(cx); let Some(abs_path) = repo .repo_path_to_project_path(&entry.repo_path) .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx)) else { return; }; let namespace = if repo.has_conflict(&entry.repo_path) { CONFLICT_NAMESPACE } else if entry.status.is_created() { NEW_NAMESPACE } else { TRACKED_NAMESPACE }; let path_key = PathKey::namespaced(namespace, &abs_path); self.scroll_to_path(path_key, window, cx) } fn scroll_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context<Self>) { if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) { self.editor.update(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| { s.select_ranges([position..position]); }) }) } else { self.pending_scroll = Some(path_key); } } fn handle_editor_event( &mut self, editor: &Entity<Editor>, event: &EditorEvent, window: &mut Window, cx: &mut Context<Self>, ) { match event { EditorEvent::ScrollPositionChanged { .. } => editor.update(cx, |editor, cx| { let anchor = editor.scroll_manager.anchor().anchor; let Some((_, buffer, _)) = self.multibuffer.read(cx).excerpt_containing(anchor, cx) else { return; }; let Some(project_path) = buffer .read(cx) .file() .map(|file| (file.worktree_id(cx), file.path().clone())) else { return; }; self.workspace .update(cx, |workspace, cx| { if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { git_panel.update(cx, |git_panel, cx| { git_panel.set_focused_path(project_path.into(), window, cx) }) } }) .ok(); }), _ => {} } } fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> { let Some(repo) = self.git_state.read(cx).active_repository() else { self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.clear(cx); }); return vec![]; }; let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>(); let mut result = vec![]; repo.update(cx, |repo, cx| { for entry in repo.status() { if !entry.status.has_changes() { continue; } let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else { continue; }; let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { continue; }; let namespace = if repo.has_conflict(&entry.repo_path) { CONFLICT_NAMESPACE } else if entry.status.is_created() { NEW_NAMESPACE } else { TRACKED_NAMESPACE }; let path_key = PathKey::namespaced(namespace, &abs_path); previous_paths.remove(&path_key); let load_buffer = self .project .update(cx, |project, cx| project.open_buffer(project_path, cx)); let project = self.project.clone(); result.push(cx.spawn(|_, mut cx| async move { let buffer = load_buffer.await?; let changes = project .update(&mut cx, |project, cx| { project.open_uncommitted_diff(buffer.clone(), cx) })? .await?; Ok(DiffBuffer { path_key, buffer, diff: changes, }) })); } }); self.multibuffer.update(cx, |multibuffer, cx| { for path in previous_paths { multibuffer.remove_excerpts_for_path(path, cx); } }); result } fn register_buffer( &mut self, diff_buffer: DiffBuffer, window: &mut Window, cx: &mut Context<Self>, ) { let path_key = diff_buffer.path_key; let buffer = diff_buffer.buffer; let diff = diff_buffer.diff; let snapshot = buffer.read(cx).snapshot(); let diff = diff.read(cx); let diff_hunk_ranges = if diff.snapshot.base_text.is_none() { vec![Point::zero()..snapshot.max_point()] } else { diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot) .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) .collect::<Vec<_>>() }; self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.set_excerpts_for_path( path_key.clone(), buffer, diff_hunk_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ); }); if self.pending_scroll.as_ref() == Some(&path_key) { self.scroll_to_path(path_key, window, cx); } } pub async fn handle_status_updates( this: WeakEntity<Self>, mut recv: postage::watch::Receiver<()>, mut cx: AsyncWindowContext, ) -> Result<()> { while let Some(_) = recv.next().await { let buffers_to_load = this.update(&mut cx, |this, cx| this.load_buffers(cx))?; for buffer_to_load in buffers_to_load { if let Some(buffer) = buffer_to_load.await.log_err() { cx.update(|window, cx| { this.update(cx, |this, cx| this.register_buffer(buffer, window, cx)) .ok(); })?; } } this.update(&mut cx, |this, _| this.pending_scroll.take())?; } Ok(()) }} impl EventEmitter<EditorEvent> for ProjectDiff {} impl Focusable for ProjectDiff { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() }} impl Item for ProjectDiff { type Event = EditorEvent; fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { Editor::to_item_events(event, f) } fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor .update(cx, |editor, cx| editor.deactivated(window, cx)); } fn navigate( &mut self, data: Box<dyn Any>, window: &mut Window, cx: &mut Context<Self>, ) -> bool { self.editor .update(cx, |editor, cx| editor.navigate(data, window, cx)) } fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> { Some("Project Diff".into()) } fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement { Label::new("Uncommitted Changes") .color(if params.selected { Color::Default } else { Color::Muted }) .into_any_element() } fn telemetry_event_text(&self) -> Option<&'static str> { Some("project diagnostics") } fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> { Some(Box::new(self.editor.clone())) } fn for_each_project_item( &self, cx: &App, f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), ) { self.editor.for_each_project_item(cx, f) } fn is_singleton(&self, _: &App) -> bool { false } fn set_nav_history( &mut self, nav_history: ItemNavHistory, _: &mut Window, cx: &mut Context<Self>, ) { self.editor.update(cx, |editor, _| { editor.set_nav_history(Some(nav_history)); }); } fn clone_on_split( &self, _workspace_id: Option<workspace::WorkspaceId>, window: &mut Window, cx: &mut Context<Self>, ) -> Option<Entity<Self>> where Self: Sized, { let workspace = self.workspace.upgrade()?; Some(cx.new(|cx| { ProjectDiff::new( self.project.clone(), workspace, self.git_panel.clone(), window, cx, ) })) } fn is_dirty(&self, cx: &App) -> bool { self.multibuffer.read(cx).is_dirty(cx) } fn has_conflict(&self, cx: &App) -> bool { self.multibuffer.read(cx).has_conflict(cx) } fn can_save(&self, _: &App) -> bool { true } fn save( &mut self, format: bool, project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>, ) -> Task<Result<()>> { self.editor.save(format, project, window, cx) } fn save_as( &mut self, _: Entity<Project>, _: ProjectPath, _window: &mut Window, _: &mut Context<Self>, ) -> Task<Result<()>> { unreachable!() } fn reload( &mut self, project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>, ) -> Task<Result<()>> { self.editor.reload(project, window, cx) } Lines 1–500 of 557. Load more
use std::any::{Any, TypeId}; use anyhow::Result; use collections::HashSet; use diff::BufferDiff; use editor::{scroll::Autoscroll, Editor, EditorEvent}; use feature_flags::FeatureFlagViewExt; use futures::StreamExt; use gpui::{ actions, AnyElement, AnyView, App, AppContext, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, }; use language::{Anchor, Buffer, Capability, OffsetRangeExt, Point}; use multi_buffer::{MultiBuffer, PathKey}; use project::{git::GitState, Project, ProjectPath}; use theme::ActiveTheme; use ui::prelude::*; use util::ResultExt as _; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, searchable::SearchableItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace, }; use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry}; actions!(git, [Diff]); pub(crate) struct ProjectDiff { multibuffer: Entity<MultiBuffer>, editor: Entity<Editor>, project: Entity<Project>, git_panel: Entity<GitPanel>, git_state: Entity<GitState>, workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, update_needed: postage::watch::Sender<()>, pending_scroll: Option<PathKey>, _task: Task<Result<()>>, _subscription: Subscription, } struct DiffBuffer { path_key: PathKey, buffer: Entity<Buffer>, diff: Entity<BufferDiff>, } const CONFLICT_NAMESPACE: &'static str = "0"; const TRACKED_NAMESPACE: &'static str = "1"; const NEW_NAMESPACE: &'static str = "2"; impl ProjectDiff { pub(crate) fn register( _: &mut Workspace, window: Option<&mut Window>, cx: &mut Context<Workspace>, ) { let Some(window) = window else { return }; cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| { workspace.register_action(Self::deploy); }); } fn deploy( workspace: &mut Workspace, _: &Diff, window: &mut Window, cx: &mut Context<Workspace>, ) { Self::deploy_at(workspace, None, window, cx) } pub fn deploy_at( workspace: &mut Workspace, entry: Option<GitStatusEntry>, window: &mut Window, cx: &mut Context<Workspace>, ) { let project_diff = if let Some(existing) = workspace.item_of_type::<Self>(cx) { workspace.activate_item(&existing, true, true, window, cx); existing } else { let workspace_handle = cx.entity(); let project_diff = cx.new(|cx| { Self::new( workspace.project().clone(), workspace_handle, workspace.panel::<GitPanel>(cx).unwrap(), window, cx, ) }); workspace.add_item_to_active_pane( Box::new(project_diff.clone()), None, true, window, cx, ); project_diff }; if let Some(entry) = entry { project_diff.update(cx, |project_diff, cx| { project_diff.scroll_to(entry, window, cx); }) } } fn new( project: Entity<Project>, workspace: Entity<Workspace>, git_panel: Entity<GitPanel>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); let editor = cx.new(|cx| { let mut diff_display_editor = Editor::for_multibuffer( multibuffer.clone(), Some(project.clone()), true, window, cx, ); diff_display_editor.set_expand_all_diff_hunks(cx); diff_display_editor.register_addon(GitPanelAddon { git_panel: git_panel.clone(), }); diff_display_editor }); cx.subscribe_in(&editor, window, Self::handle_editor_event) .detach(); let git_state = project.read(cx).git_state().clone(); let git_state_subscription = cx.subscribe_in( &git_state, window, move |this, _git_state, _event, _window, _cx| { *this.update_needed.borrow_mut() = (); }, ); let (mut send, recv) = postage::watch::channel::<()>(); let worker = window.spawn(cx, { let this = cx.weak_entity(); |cx| Self::handle_status_updates(this, recv, cx) }); // Kick of a refresh immediately *send.borrow_mut() = (); Self { project, git_state: git_state.clone(), git_panel: git_panel.clone(), workspace: workspace.downgrade(), focus_handle, editor, multibuffer, pending_scroll: None, update_needed: send, _task: worker, _subscription: git_state_subscription, } } pub fn scroll_to( &mut self, entry: GitStatusEntry, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.git_state.read(cx).active_repository() else { return; }; let repo = git_repo.read(cx); let Some(abs_path) = repo .repo_path_to_project_path(&entry.repo_path) .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx)) else { return; }; let namespace = if repo.has_conflict(&entry.repo_path) { CONFLICT_NAMESPACE } else if entry.status.is_created() { NEW_NAMESPACE } else { TRACKED_NAMESPACE }; let path_key = PathKey::namespaced(namespace, &abs_path); self.scroll_to_path(path_key, window, cx) } fn scroll_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context<Self>) { if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) { self.editor.update(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| { s.select_ranges([position..position]); }) }) } else { self.pending_scroll = Some(path_key); } } fn handle_editor_event( &mut self, editor: &Entity<Editor>, event: &EditorEvent, window: &mut Window, cx: &mut Context<Self>, ) { match event { EditorEvent::ScrollPositionChanged { .. } => editor.update(cx, |editor, cx| { let anchor = editor.scroll_manager.anchor().anchor; let Some((_, buffer, _)) = self.multibuffer.read(cx).excerpt_containing(anchor, cx) else { return; }; let Some(project_path) = buffer .read(cx) .file() .map(|file| (file.worktree_id(cx), file.path().clone())) else { return; }; self.workspace .update(cx, |workspace, cx| { if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { git_panel.update(cx, |git_panel, cx| { git_panel.set_focused_path(project_path.into(), window, cx) }) } }) .ok(); }), _ => {} } } fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> { let Some(repo) = self.git_state.read(cx).active_repository() else { self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.clear(cx); }); return vec![]; }; let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>(); let mut result = vec![]; repo.update(cx, |repo, cx| { for entry in repo.status() { if !entry.status.has_changes() { continue; } let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else { continue; }; let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { continue; }; let namespace = if repo.has_conflict(&entry.repo_path) { CONFLICT_NAMESPACE } else if entry.status.is_created() { NEW_NAMESPACE } else { TRACKED_NAMESPACE }; let path_key = PathKey::namespaced(namespace, &abs_path); previous_paths.remove(&path_key); let load_buffer = self .project .update(cx, |project, cx| project.open_buffer(project_path, cx)); let project = self.project.clone(); result.push(cx.spawn(|_, mut cx| async move { let buffer = load_buffer.await?; let changes = project .update(&mut cx, |project, cx| { project.open_uncommitted_diff(buffer.clone(), cx) })? .await?; Ok(DiffBuffer { path_key, buffer, diff: changes, }) })); } }); self.multibuffer.update(cx, |multibuffer, cx| { for path in previous_paths { multibuffer.remove_excerpts_for_path(path, cx); } }); result } fn register_buffer( &mut self, diff_buffer: DiffBuffer, window: &mut Window, cx: &mut Context<Self>, ) { let path_key = diff_buffer.path_key; let buffer = diff_buffer.buffer; let diff = diff_buffer.diff; let snapshot = buffer.read(cx).snapshot(); let diff = diff.read(cx); let diff_hunk_ranges = if diff.snapshot.base_text.is_none() { vec![Point::zero()..snapshot.max_point()] } else { diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot) .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) .collect::<Vec<_>>() }; self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.set_excerpts_for_path( path_key.clone(), buffer, diff_hunk_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ); }); if self.pending_scroll.as_ref() == Some(&path_key) { self.scroll_to_path(path_key, window, cx); } } pub async fn handle_status_updates( this: WeakEntity<Self>, mut recv: postage::watch::Receiver<()>, mut cx: AsyncWindowContext, ) -> Result<()> { while let Some(_) = recv.next().await { let buffers_to_load = this.update(&mut cx, |this, cx| this.load_buffers(cx))?; for buffer_to_load in buffers_to_load { if let Some(buffer) = buffer_to_load.await.log_err() { cx.update(|window, cx| { this.update(cx, |this, cx| this.register_buffer(buffer, window, cx)) .ok(); })?; } } this.update(&mut cx, |this, _| this.pending_scroll.take())?; } Ok(()) } } impl EventEmitter<EditorEvent> for ProjectDiff {} impl Focusable for ProjectDiff { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } impl Item for ProjectDiff { type Event = EditorEvent; fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { Editor::to_item_events(event, f) } fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor .update(cx, |editor, cx| editor.deactivated(window, cx)); } fn navigate( &mut self, data: Box<dyn Any>, window: &mut Window, cx: &mut Context<Self>, ) -> bool { self.editor .update(cx, |editor, cx| editor.navigate(data, window, cx)) } fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> { Some("Project Diff".into()) } fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement { Label::new("Uncommitted Changes") .color(if params.selected { Color::Default } else { Color::Muted }) .into_any_element() } fn telemetry_event_text(&self) -> Option<&'static str> { Some("project diagnostics") } fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> { Some(Box::new(self.editor.clone())) } fn for_each_project_item( &self, cx: &App, f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), ) { self.editor.for_each_project_item(cx, f) } fn is_singleton(&self, _: &App) -> bool { false } fn set_nav_history( &mut self, nav_history: ItemNavHistory, _: &mut Window, cx: &mut Context<Self>, ) { self.editor.update(cx, |editor, _| { editor.set_nav_history(Some(nav_history)); }); } fn clone_on_split( &self, _workspace_id: Option<workspace::WorkspaceId>, window: &mut Window, cx: &mut Context<Self>, ) -> Option<Entity<Self>> where Self: Sized, { let workspace = self.workspace.upgrade()?; Some(cx.new(|cx| { ProjectDiff::new( self.project.clone(), workspace, self.git_panel.clone(), window, cx, ) })) } fn is_dirty(&self, cx: &App) -> bool { self.multibuffer.read(cx).is_dirty(cx) } fn has_conflict(&self, cx: &App) -> bool { self.multibuffer.read(cx).has_conflict(cx) } fn can_save(&self, _: &App) -> bool { true } fn save( &mut self, format: bool, project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>, ) -> Task<Result<()>> { self.editor.save(format, project, window, cx) } fn save_as( &mut self, _: Entity<Project>, _: ProjectPath, _window: &mut Window, _: &mut Context<Self>, ) -> Task<Result<()>> { unreachable!() } fn reload( &mut self, project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>, ) -> Task<Result<()>> { self.editor.reload(project, window, cx) }