Add the ability to follow the agent as it makes edits (#29839)
Nathan here: I also tacked on a bunch of UI refinement. Release Notes: - Introduced the ability to follow the agent around as it reads and edits files. --------- Co-authored-by: Nathan Sobo <nathan@zed.dev> Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
425f32e068
commit
545ae27079
37 changed files with 1255 additions and 567 deletions
|
@ -56,7 +56,7 @@ use anyhow::{Context as _, Result, anyhow};
|
|||
use blink_manager::BlinkManager;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use client::{Collaborator, ParticipantIndex};
|
||||
use clock::ReplicaId;
|
||||
use clock::{AGENT_REPLICA_ID, ReplicaId};
|
||||
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use convert_case::{Case, Casing};
|
||||
use display_map::*;
|
||||
|
@ -201,7 +201,7 @@ use ui::{
|
|||
};
|
||||
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
|
||||
use workspace::{
|
||||
Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,
|
||||
CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,
|
||||
RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast,
|
||||
ViewId, Workspace, WorkspaceId, WorkspaceSettings,
|
||||
item::{ItemHandle, PreviewTabsSettings},
|
||||
|
@ -914,7 +914,7 @@ pub struct Editor {
|
|||
input_enabled: bool,
|
||||
use_modal_editing: bool,
|
||||
read_only: bool,
|
||||
leader_peer_id: Option<PeerId>,
|
||||
leader_id: Option<CollaboratorId>,
|
||||
remote_id: Option<ViewId>,
|
||||
pub hover_state: HoverState,
|
||||
pending_mouse_down: Option<Rc<RefCell<Option<MouseDownEvent>>>>,
|
||||
|
@ -1059,10 +1059,10 @@ pub struct RemoteSelection {
|
|||
pub replica_id: ReplicaId,
|
||||
pub selection: Selection<Anchor>,
|
||||
pub cursor_shape: CursorShape,
|
||||
pub peer_id: PeerId,
|
||||
pub collaborator_id: CollaboratorId,
|
||||
pub line_mode: bool,
|
||||
pub participant_index: Option<ParticipantIndex>,
|
||||
pub user_name: Option<SharedString>,
|
||||
pub color: PlayerColor,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -1723,7 +1723,7 @@ impl Editor {
|
|||
use_auto_surround: true,
|
||||
auto_replace_emoji_shortcode: false,
|
||||
jsx_tag_auto_close_enabled_in_any_buffer: false,
|
||||
leader_peer_id: None,
|
||||
leader_id: None,
|
||||
remote_id: None,
|
||||
hover_state: Default::default(),
|
||||
pending_mouse_down: None,
|
||||
|
@ -2175,8 +2175,8 @@ impl Editor {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn leader_peer_id(&self) -> Option<PeerId> {
|
||||
self.leader_peer_id
|
||||
pub fn leader_id(&self) -> Option<CollaboratorId> {
|
||||
self.leader_id
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> &Entity<MultiBuffer> {
|
||||
|
@ -2517,7 +2517,7 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
if self.focus_handle.is_focused(window) && self.leader_peer_id.is_none() {
|
||||
if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_active_selections(
|
||||
&self.selections.disjoint_anchors(),
|
||||
|
@ -18490,7 +18490,7 @@ impl Editor {
|
|||
self.show_cursor_names(window, cx);
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx);
|
||||
if self.leader_peer_id.is_none() {
|
||||
if self.leader_id.is_none() {
|
||||
buffer.set_active_selections(
|
||||
&self.selections.disjoint_anchors(),
|
||||
self.selections.line_mode,
|
||||
|
@ -19928,18 +19928,34 @@ impl EditorSnapshot {
|
|||
self.buffer_snapshot
|
||||
.selections_in_range(range, false)
|
||||
.filter_map(move |(replica_id, line_mode, cursor_shape, selection)| {
|
||||
let collaborator = collaborators_by_replica_id.get(&replica_id)?;
|
||||
let participant_index = participant_indices.get(&collaborator.user_id).copied();
|
||||
let user_name = participant_names.get(&collaborator.user_id).cloned();
|
||||
Some(RemoteSelection {
|
||||
replica_id,
|
||||
selection,
|
||||
cursor_shape,
|
||||
line_mode,
|
||||
participant_index,
|
||||
peer_id: collaborator.peer_id,
|
||||
user_name,
|
||||
})
|
||||
if replica_id == AGENT_REPLICA_ID {
|
||||
Some(RemoteSelection {
|
||||
replica_id,
|
||||
selection,
|
||||
cursor_shape,
|
||||
line_mode,
|
||||
collaborator_id: CollaboratorId::Agent,
|
||||
user_name: Some("Agent".into()),
|
||||
color: cx.theme().players().agent(),
|
||||
})
|
||||
} else {
|
||||
let collaborator = collaborators_by_replica_id.get(&replica_id)?;
|
||||
let participant_index = participant_indices.get(&collaborator.user_id).copied();
|
||||
let user_name = participant_names.get(&collaborator.user_id).cloned();
|
||||
Some(RemoteSelection {
|
||||
replica_id,
|
||||
selection,
|
||||
cursor_shape,
|
||||
line_mode,
|
||||
collaborator_id: CollaboratorId::PeerId(collaborator.peer_id),
|
||||
user_name,
|
||||
color: if let Some(index) = participant_index {
|
||||
cx.theme().players().color_for_participant(index.0)
|
||||
} else {
|
||||
cx.theme().players().absent()
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -12650,7 +12650,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
|
|||
Editor::from_state_proto(
|
||||
workspace_entity,
|
||||
ViewId {
|
||||
creator: Default::default(),
|
||||
creator: CollaboratorId::PeerId(PeerId::default()),
|
||||
id: 0,
|
||||
},
|
||||
&mut state_message,
|
||||
|
@ -12737,7 +12737,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
|
|||
Editor::from_state_proto(
|
||||
workspace_entity,
|
||||
ViewId {
|
||||
creator: Default::default(),
|
||||
creator: CollaboratorId::PeerId(PeerId::default()),
|
||||
id: 0,
|
||||
},
|
||||
&mut state_message,
|
||||
|
|
|
@ -28,7 +28,6 @@ use crate::{
|
|||
scroll::scroll_amount::ScrollAmount,
|
||||
};
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use client::ParticipantIndex;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt};
|
||||
use file_icons::FileIcons;
|
||||
|
@ -82,7 +81,7 @@ use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
|
|||
use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use util::{RangeExt, ResultExt, debug_panic};
|
||||
use workspace::{Workspace, item::Item, notifications::NotifyTaskExt};
|
||||
use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
|
||||
|
||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
|
||||
|
||||
|
@ -1126,7 +1125,7 @@ impl EditorElement {
|
|||
editor.cursor_shape,
|
||||
&snapshot.display_snapshot,
|
||||
is_newest,
|
||||
editor.leader_peer_id.is_none(),
|
||||
editor.leader_id.is_none(),
|
||||
None,
|
||||
);
|
||||
if is_newest {
|
||||
|
@ -1150,18 +1149,29 @@ impl EditorElement {
|
|||
|
||||
if let Some(collaboration_hub) = &editor.collaboration_hub {
|
||||
// When following someone, render the local selections in their color.
|
||||
if let Some(leader_id) = editor.leader_peer_id {
|
||||
if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id)
|
||||
{
|
||||
if let Some(participant_index) = collaboration_hub
|
||||
.user_participant_indices(cx)
|
||||
.get(&collaborator.user_id)
|
||||
{
|
||||
if let Some(leader_id) = editor.leader_id {
|
||||
match leader_id {
|
||||
CollaboratorId::PeerId(peer_id) => {
|
||||
if let Some(collaborator) =
|
||||
collaboration_hub.collaborators(cx).get(&peer_id)
|
||||
{
|
||||
if let Some(participant_index) = collaboration_hub
|
||||
.user_participant_indices(cx)
|
||||
.get(&collaborator.user_id)
|
||||
{
|
||||
if let Some((local_selection_style, _)) = selections.first_mut()
|
||||
{
|
||||
*local_selection_style = cx
|
||||
.theme()
|
||||
.players()
|
||||
.color_for_participant(participant_index.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CollaboratorId::Agent => {
|
||||
if let Some((local_selection_style, _)) = selections.first_mut() {
|
||||
*local_selection_style = cx
|
||||
.theme()
|
||||
.players()
|
||||
.color_for_participant(participant_index.0);
|
||||
*local_selection_style = cx.theme().players().agent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1173,12 +1183,9 @@ impl EditorElement {
|
|||
collaboration_hub.as_ref(),
|
||||
cx,
|
||||
) {
|
||||
let selection_style =
|
||||
Self::get_participant_color(selection.participant_index, cx);
|
||||
|
||||
// Don't re-render the leader's selections, since the local selections
|
||||
// match theirs.
|
||||
if Some(selection.peer_id) == editor.leader_peer_id {
|
||||
if Some(selection.collaborator_id) == editor.leader_id {
|
||||
continue;
|
||||
}
|
||||
let key = HoveredCursor {
|
||||
|
@ -1191,7 +1198,7 @@ impl EditorElement {
|
|||
|
||||
remote_selections
|
||||
.entry(selection.replica_id)
|
||||
.or_insert((selection_style, Vec::new()))
|
||||
.or_insert((selection.color, Vec::new()))
|
||||
.1
|
||||
.push(SelectionLayout::new(
|
||||
selection.selection,
|
||||
|
@ -1246,9 +1253,11 @@ impl EditorElement {
|
|||
collaboration_hub.deref(),
|
||||
cx,
|
||||
) {
|
||||
let color = Self::get_participant_color(remote_selection.participant_index, cx);
|
||||
add_cursor(remote_selection.selection.head(), color.cursor);
|
||||
if Some(remote_selection.peer_id) == editor.leader_peer_id {
|
||||
add_cursor(
|
||||
remote_selection.selection.head(),
|
||||
remote_selection.color.cursor,
|
||||
);
|
||||
if Some(remote_selection.collaborator_id) == editor.leader_id {
|
||||
skip_local = true;
|
||||
}
|
||||
}
|
||||
|
@ -2446,14 +2455,6 @@ impl EditorElement {
|
|||
Some(button)
|
||||
}
|
||||
|
||||
fn get_participant_color(participant_index: Option<ParticipantIndex>, cx: &App) -> PlayerColor {
|
||||
if let Some(index) = participant_index {
|
||||
cx.theme().players().color_for_participant(index.0)
|
||||
} else {
|
||||
cx.theme().players().absent()
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_relative_line_numbers(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
|
|
|
@ -23,7 +23,7 @@ use project::{
|
|||
Project, ProjectItem as _, ProjectPath, lsp_store::FormatTrigger,
|
||||
project_settings::ProjectSettings, search::SearchQuery,
|
||||
};
|
||||
use rpc::proto::{self, PeerId, update_view};
|
||||
use rpc::proto::{self, update_view};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
|
@ -39,7 +39,7 @@ use theme::{Theme, ThemeSettings};
|
|||
use ui::{IconDecorationKind, prelude::*};
|
||||
use util::{ResultExt, TryFutureExt, paths::PathExt};
|
||||
use workspace::{
|
||||
ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
item::{FollowableItem, Item, ItemEvent, ProjectItem},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
};
|
||||
|
@ -170,14 +170,14 @@ impl FollowableItem for Editor {
|
|||
}))
|
||||
}
|
||||
|
||||
fn set_leader_peer_id(
|
||||
fn set_leader_id(
|
||||
&mut self,
|
||||
leader_peer_id: Option<PeerId>,
|
||||
leader_id: Option<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.leader_peer_id = leader_peer_id;
|
||||
if self.leader_peer_id.is_some() {
|
||||
self.leader_id = leader_id;
|
||||
if self.leader_id.is_some() {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.remove_active_selections(cx);
|
||||
});
|
||||
|
@ -350,6 +350,30 @@ impl FollowableItem for Editor {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update_agent_location(
|
||||
&mut self,
|
||||
location: language::Anchor,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let buffer = buffer.read(cx);
|
||||
let Some((excerpt_id, _, _)) = buffer.as_singleton() else {
|
||||
return;
|
||||
};
|
||||
let position = buffer.anchor_in_excerpt(*excerpt_id, location).unwrap();
|
||||
let selection = Selection {
|
||||
id: 0,
|
||||
reversed: false,
|
||||
start: position,
|
||||
end: position,
|
||||
goal: SelectionGoal::None,
|
||||
};
|
||||
drop(buffer);
|
||||
self.set_selections_from_remote(vec![selection], None, window, cx);
|
||||
self.request_autoscroll_remotely(Autoscroll::center(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_editor_from_message(
|
||||
|
@ -1293,7 +1317,7 @@ impl ProjectItem for Editor {
|
|||
|
||||
fn for_project_item(
|
||||
project: Entity<Project>,
|
||||
pane: &Pane,
|
||||
pane: Option<&Pane>,
|
||||
buffer: Entity<Buffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -1304,7 +1328,7 @@ impl ProjectItem for Editor {
|
|||
{
|
||||
if WorkspaceSettings::get(None, cx).restore_on_file_reopen {
|
||||
if let Some(restoration_data) = Self::project_item_kind()
|
||||
.and_then(|kind| pane.project_item_restoration_data.get(&kind))
|
||||
.and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind))
|
||||
.and_then(|data| data.downcast_ref::<EditorRestorationData>())
|
||||
.and_then(|data| {
|
||||
let file = project::File::from_dyn(buffer.read(cx).file())?;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue