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
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -18092,7 +18092,6 @@ dependencies = [
|
|||
"component",
|
||||
"dap",
|
||||
"db",
|
||||
"derive_more",
|
||||
"env_logger 0.11.8",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
|
|
1
assets/icons/crosshair.svg
Normal file
1
assets/icons/crosshair.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-crosshair-icon lucide-crosshair"><circle cx="12" cy="12" r="10"/><line x1="22" x2="18" y1="12" y2="12"/><line x1="6" x2="2" y1="12" y2="12"/><line x1="12" x2="12" y1="6" y2="2"/><line x1="12" x2="12" y1="22" y2="18"/></svg>
|
After Width: | Height: | Size: 426 B |
|
@ -77,7 +77,8 @@ actions!(
|
|||
Keep,
|
||||
Reject,
|
||||
RejectAll,
|
||||
KeepAll
|
||||
KeepAll,
|
||||
Follow
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -104,10 +104,9 @@ impl Render for AssistantModelSelector {
|
|||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let model = self.selector.read(cx).active_model(cx);
|
||||
let (model_name, model_icon) = match model {
|
||||
Some(model) => (model.model.name().0, Some(model.provider.icon())),
|
||||
_ => (SharedString::from("No model selected"), None),
|
||||
};
|
||||
let model_name = model
|
||||
.map(|model| model.model.name().0)
|
||||
.unwrap_or_else(|| SharedString::from("No model selected"));
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
|
@ -116,11 +115,6 @@ impl Render for AssistantModelSelector {
|
|||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.children(
|
||||
model_icon.map(|icon| {
|
||||
Icon::new(icon).color(Color::Muted).size(IconSize::Small)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.size(LabelSize::Small)
|
||||
|
|
|
@ -37,8 +37,8 @@ use ui::{
|
|||
Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::OpenConfiguration;
|
||||
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
|
||||
|
||||
|
@ -52,7 +52,7 @@ use crate::thread_history::{PastContext, PastThread, ThreadHistory};
|
|||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::UsageBanner;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, ExpandMessageEditor,
|
||||
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow,
|
||||
InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
|
||||
OpenHistory, ThreadEvent, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
|
||||
};
|
||||
|
@ -107,6 +107,9 @@ pub fn init(cx: &mut App) {
|
|||
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &Follow, window, cx| {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
})
|
||||
.register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
|
|
|
@ -11,7 +11,7 @@ use gpui::{
|
|||
use itertools::Itertools;
|
||||
use language::Buffer;
|
||||
use project::ProjectItem;
|
||||
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use ui::{PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::{AgentContextHandle, ContextKind};
|
||||
|
@ -357,7 +357,7 @@ impl Focusable for ContextStrip {
|
|||
}
|
||||
|
||||
impl Render for ContextStrip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let context_picker = self.context_picker.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
|
@ -434,30 +434,6 @@ impl Render for ContextStrip {
|
|||
})
|
||||
.with_handle(self.context_picker_menu_handle.clone()),
|
||||
)
|
||||
.when(no_added_context && suggested_context.is_none(), {
|
||||
|parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.ml_1p5()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new("Add Context")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.opacity(0.5)
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&ToggleContextPicker,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
.children(
|
||||
added_contexts
|
||||
.into_iter()
|
||||
|
|
|
@ -32,7 +32,7 @@ use std::time::Duration;
|
|||
use theme::ThemeSettings;
|
||||
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_llm_client::CompletionMode;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
|
@ -42,7 +42,7 @@ use crate::profile_selector::ProfileSelector;
|
|||
use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{
|
||||
ActiveThread, AgentDiffPane, Chat, ExpandMessageEditor, NewThread, OpenAgentDiff,
|
||||
ActiveThread, AgentDiffPane, Chat, ExpandMessageEditor, Follow, NewThread, OpenAgentDiff,
|
||||
RemoveAllContext, ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
|
||||
};
|
||||
|
||||
|
@ -97,7 +97,7 @@ pub(crate) fn create_editor(
|
|||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
|
||||
editor.set_placeholder_text("Message the agent – @ to include context", cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_soft_wrap();
|
||||
editor.set_context_menu_options(ContextMenuOptions {
|
||||
|
@ -200,8 +200,7 @@ impl MessageEditor {
|
|||
model_selector,
|
||||
edits_expanded: false,
|
||||
editor_is_expanded: false,
|
||||
profile_selector: cx
|
||||
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
|
||||
profile_selector: cx.new(|cx| ProfileSelector::new(fs, thread_store, cx)),
|
||||
last_estimated_token_count: None,
|
||||
update_token_count_task: None,
|
||||
_subscriptions: subscriptions,
|
||||
|
@ -457,6 +456,44 @@ impl MessageEditor {
|
|||
)
|
||||
}
|
||||
|
||||
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let following = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.is_being_followed(CollaboratorId::Agent)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
IconButton::new("follow-agent", IconName::Crosshair)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.toggle_state(following)
|
||||
.selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
|
||||
.tooltip(move |window, cx| {
|
||||
if following {
|
||||
Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
|
||||
} else {
|
||||
Tooltip::with_meta(
|
||||
"Follow Agent",
|
||||
Some(&Follow),
|
||||
"Track the agent's location as it reads and edits files.",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if following {
|
||||
workspace.unfollow(CollaboratorId::Agent, window, cx);
|
||||
} else {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_editor(
|
||||
&self,
|
||||
font_size: Rems,
|
||||
|
@ -522,6 +559,9 @@ impl MessageEditor {
|
|||
.items_start()
|
||||
.justify_between()
|
||||
.child(self.context_strip.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(focus_handle.is_focused(window), |this| {
|
||||
this.child(
|
||||
IconButton::new("toggle-height", expand_icon)
|
||||
|
@ -546,10 +586,12 @@ impl MessageEditor {
|
|||
}
|
||||
})
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
|
||||
window
|
||||
.dispatch_action(Box::new(ExpandMessageEditor), cx);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
|
@ -592,7 +634,12 @@ impl MessageEditor {
|
|||
h_flex()
|
||||
.flex_none()
|
||||
.justify_between()
|
||||
.child(h_flex().gap_2().child(self.profile_selector.clone()))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(self.render_follow_toggle(cx))
|
||||
.child(self.profile_selector.clone()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
|
|
|
@ -4,22 +4,20 @@ use assistant_settings::{
|
|||
AgentProfile, AgentProfileId, AssistantSettings, GroupedAgentProfiles, builtin_profiles,
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
|
||||
use gpui::{Action, Entity, Subscription, WeakEntity, prelude::*};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use settings::{Settings as _, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
prelude::*,
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
|
||||
use crate::{ManageProfiles, ThreadStore};
|
||||
|
||||
pub struct ProfileSelector {
|
||||
profiles: GroupedAgentProfiles,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
focus_handle: FocusHandle,
|
||||
menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
@ -28,7 +26,6 @@ impl ProfileSelector {
|
|||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
focus_handle: FocusHandle,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
|
@ -39,7 +36,6 @@ impl ProfileSelector {
|
|||
profiles: GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx)),
|
||||
fs,
|
||||
thread_store,
|
||||
focus_handle,
|
||||
menu_handle: PopoverMenuHandle::default(),
|
||||
_subscriptions: vec![settings_subscription],
|
||||
}
|
||||
|
@ -132,7 +128,7 @@ impl ProfileSelector {
|
|||
}
|
||||
|
||||
impl Render for ProfileSelector {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let profile_id = &settings.default_profile;
|
||||
let profile = settings.profiles.get(profile_id);
|
||||
|
@ -146,15 +142,7 @@ impl Render for ProfileSelector {
|
|||
.default_model()
|
||||
.map_or(false, |default| default.model.supports_tools());
|
||||
|
||||
let icon = match profile_id.as_str() {
|
||||
builtin_profiles::WRITE => IconName::Pencil,
|
||||
builtin_profiles::ASK => IconName::MessageBubbles,
|
||||
builtin_profiles::MANUAL => IconName::MessageBubbleDashed,
|
||||
_ => IconName::UserRoundPen,
|
||||
};
|
||||
|
||||
let this = cx.entity().clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
PopoverMenu::new("profile-selector")
|
||||
.menu(move |window, cx| {
|
||||
|
@ -164,7 +152,6 @@ impl Render for ProfileSelector {
|
|||
ButtonLike::new("profile-selector-button").child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(selected_profile)
|
||||
.size(LabelSize::Small)
|
||||
|
@ -174,17 +161,7 @@ impl Render for ProfileSelector {
|
|||
Icon::new(IconName::ChevronDown)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(div().opacity(0.5).children({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ButtonLike::new("tools-not-supported-button")
|
||||
|
|
|
@ -1582,10 +1582,17 @@ impl Thread {
|
|||
let tool_uses = thread.use_pending_tools(window, cx, model.clone());
|
||||
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
|
||||
}
|
||||
StopReason::EndTurn => {}
|
||||
StopReason::MaxTokens => {}
|
||||
StopReason::EndTurn | StopReason::MaxTokens => {
|
||||
thread.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(None, cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
thread.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(None, cx);
|
||||
});
|
||||
|
||||
if error.is::<PaymentRequiredError>() {
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
|
||||
} else if error.is::<MaxMonthlySpendReachedError>() {
|
||||
|
|
|
@ -62,7 +62,10 @@ use ui::{
|
|||
prelude::*,
|
||||
};
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::searchable::{Direction, SearchableItemHandle};
|
||||
use workspace::{
|
||||
CollaboratorId,
|
||||
searchable::{Direction, SearchableItemHandle},
|
||||
};
|
||||
use workspace::{
|
||||
Save, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
item::{self, FollowableItem, Item, ItemHandle},
|
||||
|
@ -3417,15 +3420,14 @@ impl FollowableItem for ContextEditor {
|
|||
true
|
||||
}
|
||||
|
||||
fn set_leader_peer_id(
|
||||
fn set_leader_id(
|
||||
&mut self,
|
||||
leader_peer_id: Option<proto::PeerId>,
|
||||
leader_id: Option<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_leader_peer_id(leader_peer_id, window, cx)
|
||||
})
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.set_leader_id(leader_id, window, cx))
|
||||
}
|
||||
|
||||
fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<item::Dedup> {
|
||||
|
|
|
@ -29,6 +29,10 @@ impl ActionLog {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn project(&self) -> &Entity<Project> {
|
||||
&self.project
|
||||
}
|
||||
|
||||
/// Notifies a diagnostics check
|
||||
pub fn checked_project_diagnostics(&mut self) {
|
||||
self.edited_since_project_diagnostics_check = false;
|
||||
|
|
|
@ -19,6 +19,7 @@ use language_model::{
|
|||
LanguageModel, LanguageModelCompletionError, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
MessageContent, Role,
|
||||
};
|
||||
use project::{AgentLocation, Project};
|
||||
use serde::Serialize;
|
||||
use std::{cmp, iter, mem, ops::Range, path::PathBuf, sync::Arc, task::Poll};
|
||||
use streaming_diff::{CharOperation, StreamingDiff};
|
||||
|
@ -59,17 +60,20 @@ pub struct EditAgentOutput {
|
|||
pub struct EditAgent {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
action_log: Entity<ActionLog>,
|
||||
project: Entity<Project>,
|
||||
templates: Arc<Templates>,
|
||||
}
|
||||
|
||||
impl EditAgent {
|
||||
pub fn new(
|
||||
model: Arc<dyn LanguageModel>,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
templates: Arc<Templates>,
|
||||
) -> Self {
|
||||
EditAgent {
|
||||
model,
|
||||
project,
|
||||
action_log,
|
||||
templates,
|
||||
}
|
||||
|
@ -118,14 +122,42 @@ impl EditAgent {
|
|||
let (output_events_tx, output_events_rx) = mpsc::unbounded();
|
||||
let this = self.clone();
|
||||
let task = cx.spawn(async move |cx| {
|
||||
// Ensure the buffer is tracked by the action log.
|
||||
this.action_log
|
||||
.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx))?;
|
||||
let output = this
|
||||
.replace_text_with_chunks_internal(buffer, edit_chunks, output_events_tx, cx)
|
||||
.await;
|
||||
this.project
|
||||
.update(cx, |project, cx| project.set_agent_location(None, cx))?;
|
||||
output
|
||||
});
|
||||
(task, output_events_rx)
|
||||
}
|
||||
|
||||
async fn replace_text_with_chunks_internal(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
edit_chunks: impl 'static + Send + Stream<Item = Result<String, LanguageModelCompletionError>>,
|
||||
output_events_tx: mpsc::UnboundedSender<EditAgentOutputEvent>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<EditAgentOutput> {
|
||||
cx.update(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
|
||||
this.action_log
|
||||
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
self.action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), cx);
|
||||
});
|
||||
self.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: language::Anchor::MAX,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
output_events_tx
|
||||
.unbounded_send(EditAgentOutputEvent::Edited)
|
||||
.ok();
|
||||
})?;
|
||||
|
||||
let mut raw_edits = String::new();
|
||||
|
@ -135,8 +167,17 @@ impl EditAgent {
|
|||
raw_edits.push_str(&chunk);
|
||||
cx.update(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.append(chunk, cx));
|
||||
this.action_log
|
||||
self.action_log
|
||||
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
self.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: language::Anchor::MAX,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
})?;
|
||||
output_events_tx
|
||||
.unbounded_send(EditAgentOutputEvent::Edited)
|
||||
|
@ -147,8 +188,6 @@ impl EditAgent {
|
|||
_raw_edits: raw_edits,
|
||||
_parser_metrics: EditParserMetrics::default(),
|
||||
})
|
||||
});
|
||||
(task, output_events_rx)
|
||||
}
|
||||
|
||||
pub fn edit(
|
||||
|
@ -161,6 +200,18 @@ impl EditAgent {
|
|||
Task<Result<EditAgentOutput>>,
|
||||
mpsc::UnboundedReceiver<EditAgentOutputEvent>,
|
||||
) {
|
||||
self.project
|
||||
.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: language::Anchor::MIN,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
let this = self.clone();
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
let output = cx.spawn(async move |cx| {
|
||||
|
@ -194,8 +245,14 @@ impl EditAgent {
|
|||
let (output_events_tx, output_events_rx) = mpsc::unbounded();
|
||||
let this = self.clone();
|
||||
let task = cx.spawn(async move |mut cx| {
|
||||
this.apply_edits_internal(buffer, edit_chunks, output_events_tx, &mut cx)
|
||||
.await
|
||||
this.action_log
|
||||
.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx))?;
|
||||
let output = this
|
||||
.apply_edits_internal(buffer, edit_chunks, output_events_tx, &mut cx)
|
||||
.await;
|
||||
this.project
|
||||
.update(cx, |project, cx| project.set_agent_location(None, cx))?;
|
||||
output
|
||||
});
|
||||
(task, output_events_rx)
|
||||
}
|
||||
|
@ -207,10 +264,6 @@ impl EditAgent {
|
|||
output_events: mpsc::UnboundedSender<EditAgentOutputEvent>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<EditAgentOutput> {
|
||||
// Ensure the buffer is tracked by the action log.
|
||||
self.action_log
|
||||
.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx))?;
|
||||
|
||||
let (output, mut edit_events) = Self::parse_edit_chunks(edit_chunks, cx);
|
||||
while let Some(edit_event) = edit_events.next().await {
|
||||
let EditParserEvent::OldText(old_text_query) = edit_event? else {
|
||||
|
@ -275,14 +328,15 @@ impl EditAgent {
|
|||
match op {
|
||||
CharOperation::Insert { text } => {
|
||||
let edit_start = snapshot.anchor_after(edit_start);
|
||||
edits_tx.unbounded_send((edit_start..edit_start, text))?;
|
||||
edits_tx
|
||||
.unbounded_send((edit_start..edit_start, Arc::from(text)))?;
|
||||
}
|
||||
CharOperation::Delete { bytes } => {
|
||||
let edit_end = edit_start + bytes;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
edits_tx.unbounded_send((edit_range, String::new()))?;
|
||||
edits_tx.unbounded_send((edit_range, Arc::from("")))?;
|
||||
}
|
||||
CharOperation::Keep { bytes } => edit_start += bytes,
|
||||
}
|
||||
|
@ -296,13 +350,35 @@ impl EditAgent {
|
|||
// TODO: group all edits into one transaction
|
||||
let mut edits_rx = edits_rx.ready_chunks(32);
|
||||
while let Some(edits) = edits_rx.next().await {
|
||||
if edits.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Edit the buffer and report edits to the action log as part of the
|
||||
// same effect cycle, otherwise the edit will be reported as if the
|
||||
// user made it.
|
||||
cx.update(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
|
||||
let max_edit_end = buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits.iter().cloned(), None, cx);
|
||||
let max_edit_end = buffer
|
||||
.summaries_for_anchors::<Point, _>(
|
||||
edits.iter().map(|(range, _)| &range.end),
|
||||
)
|
||||
.max()
|
||||
.unwrap();
|
||||
buffer.anchor_before(max_edit_end)
|
||||
});
|
||||
self.action_log
|
||||
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx))
|
||||
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
self.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: max_edit_end,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
})?;
|
||||
output_events
|
||||
.unbounded_send(EditAgentOutputEvent::Edited)
|
||||
|
@ -657,7 +733,7 @@ mod tests {
|
|||
use gpui::{App, AppContext, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use project::Project;
|
||||
use project::{AgentLocation, Project};
|
||||
use rand::prelude::*;
|
||||
use rand::rngs::StdRng;
|
||||
use std::cmp;
|
||||
|
@ -775,8 +851,11 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_events(cx: &mut TestAppContext) {
|
||||
async fn test_edit_events(cx: &mut TestAppContext) {
|
||||
let agent = init_test(cx).await;
|
||||
let project = agent
|
||||
.action_log
|
||||
.read_with(cx, |log, _| log.project().clone());
|
||||
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx));
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
let (apply, mut events) = agent.apply_edit_chunks(
|
||||
|
@ -792,6 +871,10 @@ mod tests {
|
|||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"abc\ndef\nghi"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
None
|
||||
);
|
||||
|
||||
chunks_tx.unbounded_send("bc</old_text>").unwrap();
|
||||
cx.run_until_parked();
|
||||
|
@ -800,6 +883,10 @@ mod tests {
|
|||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"abc\ndef\nghi"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
None
|
||||
);
|
||||
|
||||
chunks_tx.unbounded_send("<new_text>abX").unwrap();
|
||||
cx.run_until_parked();
|
||||
|
@ -808,6 +895,13 @@ mod tests {
|
|||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"abXc\ndef\nghi"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 3)))
|
||||
})
|
||||
);
|
||||
|
||||
chunks_tx.unbounded_send("cY").unwrap();
|
||||
cx.run_until_parked();
|
||||
|
@ -816,6 +910,13 @@ mod tests {
|
|||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"abXcY\ndef\nghi"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
|
||||
})
|
||||
);
|
||||
|
||||
chunks_tx.unbounded_send("</new_text>").unwrap();
|
||||
chunks_tx.unbounded_send("<old_text>hall").unwrap();
|
||||
|
@ -825,6 +926,13 @@ mod tests {
|
|||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"abXcY\ndef\nghi"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
|
||||
})
|
||||
);
|
||||
|
||||
chunks_tx.unbounded_send("ucinated old</old_text>").unwrap();
|
||||
chunks_tx.unbounded_send("<new_text>").unwrap();
|
||||
|
@ -839,6 +947,13 @@ mod tests {
|
|||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"abXcY\ndef\nghi"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
|
||||
})
|
||||
);
|
||||
|
||||
chunks_tx.unbounded_send("hallucinated new</new_").unwrap();
|
||||
chunks_tx.unbounded_send("text>").unwrap();
|
||||
|
@ -848,6 +963,13 @@ mod tests {
|
|||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"abXcY\ndef\nghi"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
|
||||
})
|
||||
);
|
||||
|
||||
chunks_tx.unbounded_send("<old_text>gh").unwrap();
|
||||
chunks_tx.unbounded_send("i</old_text>").unwrap();
|
||||
|
@ -858,6 +980,13 @@ mod tests {
|
|||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"abXcY\ndef\nghi"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5)))
|
||||
})
|
||||
);
|
||||
|
||||
chunks_tx.unbounded_send("GHI</new_text>").unwrap();
|
||||
cx.run_until_parked();
|
||||
|
@ -869,6 +998,13 @@ mod tests {
|
|||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"abXcY\ndef\nGHI"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(2, 3)))
|
||||
})
|
||||
);
|
||||
|
||||
drop(chunks_tx);
|
||||
apply.await.unwrap();
|
||||
|
@ -877,16 +1013,108 @@ mod tests {
|
|||
"abXcY\ndef\nGHI"
|
||||
);
|
||||
assert_eq!(drain_events(&mut events), vec![]);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
fn drain_events(
|
||||
stream: &mut UnboundedReceiver<EditAgentOutputEvent>,
|
||||
) -> Vec<EditAgentOutputEvent> {
|
||||
let mut events = Vec::new();
|
||||
while let Ok(Some(event)) = stream.try_next() {
|
||||
events.push(event);
|
||||
}
|
||||
events
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_overwrite_events(cx: &mut TestAppContext) {
|
||||
let agent = init_test(cx).await;
|
||||
let project = agent
|
||||
.action_log
|
||||
.read_with(cx, |log, _| log.project().clone());
|
||||
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx));
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
let (apply, mut events) = agent.replace_text_with_chunks(
|
||||
buffer.clone(),
|
||||
chunks_rx.map(|chunk: &str| Ok(chunk.to_string())),
|
||||
&mut cx.to_async(),
|
||||
);
|
||||
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain_events(&mut events),
|
||||
vec![EditAgentOutputEvent::Edited]
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
""
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: language::Anchor::MAX
|
||||
})
|
||||
);
|
||||
|
||||
chunks_tx.unbounded_send("jkl\n").unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain_events(&mut events),
|
||||
vec![EditAgentOutputEvent::Edited]
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"jkl\n"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: language::Anchor::MAX
|
||||
})
|
||||
);
|
||||
|
||||
chunks_tx.unbounded_send("mno\n").unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain_events(&mut events),
|
||||
vec![EditAgentOutputEvent::Edited]
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"jkl\nmno\n"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: language::Anchor::MAX
|
||||
})
|
||||
);
|
||||
|
||||
chunks_tx.unbounded_send("pqr").unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain_events(&mut events),
|
||||
vec![EditAgentOutputEvent::Edited]
|
||||
);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"jkl\nmno\npqr"
|
||||
);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: language::Anchor::MAX
|
||||
})
|
||||
);
|
||||
|
||||
drop(chunks_tx);
|
||||
apply.await.unwrap();
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.snapshot().text()),
|
||||
"jkl\nmno\npqr"
|
||||
);
|
||||
assert_eq!(drain_events(&mut events), vec![]);
|
||||
assert_eq!(
|
||||
project.read_with(cx, |project, _| project.agent_location()),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -1173,7 +1401,17 @@ mod tests {
|
|||
cx.update(Project::init_settings);
|
||||
let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let action_log = cx.new(|_| ActionLog::new(project));
|
||||
EditAgent::new(model, action_log, Templates::new())
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
EditAgent::new(model, project, action_log, Templates::new())
|
||||
}
|
||||
|
||||
fn drain_events(
|
||||
stream: &mut UnboundedReceiver<EditAgentOutputEvent>,
|
||||
) -> Vec<EditAgentOutputEvent> {
|
||||
let mut events = Vec::new();
|
||||
while let Ok(Some(event)) = stream.try_next() {
|
||||
events.push(event);
|
||||
}
|
||||
events
|
||||
}
|
||||
}
|
||||
|
|
|
@ -517,7 +517,7 @@ fn eval_from_pixels_constructor() {
|
|||
input_path: input_file_path.into(),
|
||||
input_content: Some(input_file_content.into()),
|
||||
edit_description: edit_description.into(),
|
||||
assertion: EvalAssertion::assert_eq(indoc! {"
|
||||
assertion: EvalAssertion::judge_diff(indoc! {"
|
||||
- The diff contains a new `from_pixels` constructor
|
||||
- The diff contains new tests for the `from_pixels` constructor
|
||||
"}),
|
||||
|
@ -957,7 +957,7 @@ impl EditAgentTest {
|
|||
|
||||
cx.spawn(async move |cx| {
|
||||
let agent_model =
|
||||
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
|
||||
Self::load_model("google", "gemini-2.5-pro-preview-03-25", cx).await;
|
||||
let judge_model =
|
||||
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
|
||||
(agent_model.unwrap(), judge_model.unwrap())
|
||||
|
@ -967,7 +967,7 @@ impl EditAgentTest {
|
|||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
Self {
|
||||
agent: EditAgent::new(agent_model, action_log, Templates::new()),
|
||||
agent: EditAgent::new(agent_model, project.clone(), action_log, Templates::new()),
|
||||
project,
|
||||
judge_model,
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ use language::{
|
|||
language_settings::SoftWrap,
|
||||
};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use project::{AgentLocation, Project};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
|
@ -164,6 +164,19 @@ impl Tool for EditFileTool {
|
|||
})?
|
||||
.await?;
|
||||
|
||||
// Set the agent's location to the top of the file
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: language::Anchor::MIN,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
if input.old_string.is_empty() {
|
||||
|
@ -226,6 +239,7 @@ impl Tool for EditFileTool {
|
|||
let snapshot = cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
|
||||
|
||||
let base_version = diff.base_version.clone();
|
||||
let snapshot = buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.apply_diff(diff, cx);
|
||||
|
@ -233,6 +247,21 @@ impl Tool for EditFileTool {
|
|||
buffer.snapshot()
|
||||
});
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
|
||||
// Set the agent's location to the position of the first edit
|
||||
if let Some(first_edit) = snapshot.edits_since::<usize>(&base_version).next() {
|
||||
let position = snapshot.anchor_before(first_edit.new.start);
|
||||
project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
snapshot
|
||||
})?;
|
||||
|
||||
|
|
|
@ -6,8 +6,9 @@ use gpui::{AnyWindowHandle, App, Entity, Task};
|
|||
|
||||
use indoc::formatdoc;
|
||||
use itertools::Itertools;
|
||||
use language::{Anchor, Point};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use project::{AgentLocation, Project};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
@ -35,11 +36,11 @@ pub struct ReadFileToolInput {
|
|||
|
||||
/// Optional line number to start reading on (1-based index)
|
||||
#[serde(default)]
|
||||
pub start_line: Option<usize>,
|
||||
pub start_line: Option<u32>,
|
||||
|
||||
/// Optional line number to end reading on (1-based index, inclusive)
|
||||
#[serde(default)]
|
||||
pub end_line: Option<usize>,
|
||||
pub end_line: Option<u32>,
|
||||
}
|
||||
|
||||
pub struct ReadFileTool;
|
||||
|
@ -109,7 +110,7 @@ impl Tool for ReadFileTool {
|
|||
let file_path = input.path.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
if !exists.await? {
|
||||
return Err(anyhow!("{} not found", file_path))
|
||||
return Err(anyhow!("{} not found", file_path));
|
||||
}
|
||||
|
||||
let buffer = cx
|
||||
|
@ -118,25 +119,54 @@ impl Tool for ReadFileTool {
|
|||
})?
|
||||
.await?;
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: Anchor::MIN,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
// Check if specific line ranges are provided
|
||||
if input.start_line.is_some() || input.end_line.is_some() {
|
||||
let mut anchor = None;
|
||||
let result = buffer.read_with(cx, |buffer, _cx| {
|
||||
let text = buffer.text();
|
||||
// .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0.
|
||||
let start = input.start_line.unwrap_or(1).max(1);
|
||||
let lines = text.split('\n').skip(start - 1);
|
||||
let start_row = start - 1;
|
||||
if start_row <= buffer.max_point().row {
|
||||
let column = buffer.line_indent_for_row(start_row).raw_len();
|
||||
anchor = Some(buffer.anchor_before(Point::new(start_row, column)));
|
||||
}
|
||||
|
||||
let lines = text.split('\n').skip(start_row as usize);
|
||||
if let Some(end) = input.end_line {
|
||||
let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
|
||||
Itertools::intersperse(lines.take(count), "\n").collect()
|
||||
Itertools::intersperse(lines.take(count as usize), "\n").collect()
|
||||
} else {
|
||||
Itertools::intersperse(lines, "\n").collect()
|
||||
}
|
||||
})?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.track_buffer(buffer, cx);
|
||||
log.track_buffer(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
if let Some(anchor) = anchor {
|
||||
project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: anchor,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
} else {
|
||||
// No line ranges specified, so check file size to see if it's too big.
|
||||
|
@ -165,7 +195,8 @@ impl Tool for ReadFileTool {
|
|||
})
|
||||
}
|
||||
}
|
||||
}).into()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -170,7 +170,7 @@ impl Tool for StreamingEditFileTool {
|
|||
.update(|cx| LanguageModelRegistry::read_global(cx).default_model())?
|
||||
.context("default model not set")?
|
||||
.model;
|
||||
let edit_agent = EditAgent::new(model, action_log, Templates::new());
|
||||
let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
|
|
|
@ -10,6 +10,7 @@ use std::{
|
|||
pub use system_clock::*;
|
||||
|
||||
pub const LOCAL_BRANCH_REPLICA_ID: u16 = u16::MAX;
|
||||
pub const AGENT_REPLICA_ID: u16 = u16::MAX - 1;
|
||||
|
||||
/// A unique identifier for each distributed node.
|
||||
pub type ReplicaId = u16;
|
||||
|
|
|
@ -13,6 +13,7 @@ use gpui::{BackgroundExecutor, Context, Entity, TestAppContext, Window};
|
|||
use rpc::{RECEIVE_TIMEOUT, proto::PeerId};
|
||||
use serde_json::json;
|
||||
use std::ops::Range;
|
||||
use workspace::CollaboratorId;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_core_channel_buffers(
|
||||
|
@ -300,13 +301,20 @@ fn assert_remote_selections(
|
|||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let hub = editor.collaboration_hub().unwrap();
|
||||
let collaborators = hub.collaborators(cx);
|
||||
let range = Anchor::min()..Anchor::max();
|
||||
let remote_selections = snapshot
|
||||
.remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
|
||||
.remote_selections_in_range(&range, hub, cx)
|
||||
.map(|s| {
|
||||
let CollaboratorId::PeerId(peer_id) = s.collaborator_id else {
|
||||
panic!("unexpected collaborator id");
|
||||
};
|
||||
let start = s.selection.start.to_offset(&snapshot.buffer_snapshot);
|
||||
let end = s.selection.end.to_offset(&snapshot.buffer_snapshot);
|
||||
(s.participant_index, start..end)
|
||||
let user_id = collaborators.get(&peer_id).unwrap().user_id;
|
||||
let participant_index = hub.user_participant_indices(cx).get(&user_id).copied();
|
||||
(participant_index, start..end)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
|
|
|
@ -18,7 +18,7 @@ use serde_json::json;
|
|||
use settings::SettingsStore;
|
||||
use text::{Point, ToPoint};
|
||||
use util::{path, test::sample_text};
|
||||
use workspace::{SplitDirection, Workspace, item::ItemHandle as _};
|
||||
use workspace::{CollaboratorId, SplitDirection, Workspace, item::ItemHandle as _};
|
||||
|
||||
use super::TestClient;
|
||||
|
||||
|
@ -425,7 +425,7 @@ async fn test_basic_following(
|
|||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
Some(peer_id_b)
|
||||
Some(peer_id_b.into())
|
||||
);
|
||||
assert_eq!(
|
||||
workspace_a.update_in(cx_a, |workspace, _, cx| workspace
|
||||
|
@ -1267,7 +1267,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
|||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
Some(leader_id)
|
||||
Some(leader_id.into())
|
||||
);
|
||||
let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
|
@ -1292,7 +1292,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
|||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
Some(leader_id)
|
||||
Some(leader_id.into())
|
||||
);
|
||||
|
||||
// When client B edits, it automatically stops following client A.
|
||||
|
@ -1308,7 +1308,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
|||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
Some(leader_id)
|
||||
Some(leader_id.into())
|
||||
);
|
||||
|
||||
// When client B scrolls, it automatically stops following client A.
|
||||
|
@ -1326,7 +1326,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
|||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
Some(leader_id)
|
||||
Some(leader_id.into())
|
||||
);
|
||||
|
||||
// When client B activates a different pane, it continues following client A in the original pane.
|
||||
|
@ -1335,7 +1335,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
|||
});
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
Some(leader_id)
|
||||
Some(leader_id.into())
|
||||
);
|
||||
|
||||
workspace_b.update_in(cx_b, |workspace, window, cx| {
|
||||
|
@ -1343,7 +1343,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
|||
});
|
||||
assert_eq!(
|
||||
workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
Some(leader_id)
|
||||
Some(leader_id.into())
|
||||
);
|
||||
|
||||
// When client B activates a different item in the original pane, it automatically stops following client A.
|
||||
|
@ -1406,13 +1406,13 @@ async fn test_peers_simultaneously_following_each_other(
|
|||
workspace_a.update(cx_a, |workspace, _| {
|
||||
assert_eq!(
|
||||
workspace.leader_for_pane(workspace.active_pane()),
|
||||
Some(client_b_id)
|
||||
Some(client_b_id.into())
|
||||
);
|
||||
});
|
||||
workspace_b.update(cx_b, |workspace, _| {
|
||||
assert_eq!(
|
||||
workspace.leader_for_pane(workspace.active_pane()),
|
||||
Some(client_a_id)
|
||||
Some(client_a_id.into())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -1513,7 +1513,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
|||
workspace_b_project_a.update(&mut cx_b2, |workspace, cx| {
|
||||
assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
|
||||
assert_eq!(
|
||||
client_a.peer_id(),
|
||||
client_a.peer_id().map(Into::into),
|
||||
workspace.leader_for_pane(workspace.active_pane())
|
||||
);
|
||||
let item = workspace.active_item(cx).unwrap();
|
||||
|
@ -1554,7 +1554,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
|||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
|
||||
assert_eq!(
|
||||
client_b.peer_id(),
|
||||
client_b.peer_id().map(Into::into),
|
||||
workspace.leader_for_pane(workspace.active_pane())
|
||||
);
|
||||
let item = workspace.active_pane().read(cx).active_item().unwrap();
|
||||
|
@ -1615,7 +1615,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
|
|||
assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
|
||||
assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
|
||||
assert_eq!(
|
||||
client_b.peer_id(),
|
||||
client_b.peer_id().map(Into::into),
|
||||
workspace.leader_for_pane(workspace.active_pane())
|
||||
);
|
||||
let item = workspace.active_item(cx).unwrap();
|
||||
|
@ -1866,7 +1866,11 @@ fn pane_summaries(workspace: &Entity<Workspace>, cx: &mut VisualTestContext) ->
|
|||
.panes()
|
||||
.iter()
|
||||
.map(|pane| {
|
||||
let leader = workspace.leader_for_pane(pane);
|
||||
let leader = match workspace.leader_for_pane(pane) {
|
||||
Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
|
||||
Some(CollaboratorId::Agent) => unimplemented!(),
|
||||
None => None,
|
||||
};
|
||||
let active = pane == active_pane;
|
||||
let pane = pane.read(cx);
|
||||
let active_ix = pane.active_item_index();
|
||||
|
@ -1985,7 +1989,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
|||
let channel_notes_1_b = workspace_b.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.leader_for_pane(workspace.active_pane()),
|
||||
Some(client_a.peer_id().unwrap())
|
||||
Some(client_a.peer_id().unwrap().into())
|
||||
);
|
||||
workspace
|
||||
.active_item(cx)
|
||||
|
@ -2015,7 +2019,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
|
|||
let channel_notes_2_b = workspace_b.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.leader_for_pane(workspace.active_pane()),
|
||||
Some(client_a.peer_id().unwrap())
|
||||
Some(client_a.peer_id().unwrap().into())
|
||||
);
|
||||
workspace
|
||||
.active_item(cx)
|
||||
|
|
|
@ -22,7 +22,7 @@ use std::{
|
|||
};
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::item::TabContentParams;
|
||||
use workspace::{CollaboratorId, item::TabContentParams};
|
||||
use workspace::{
|
||||
ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
|
@ -654,15 +654,14 @@ impl FollowableItem for ChannelView {
|
|||
})
|
||||
}
|
||||
|
||||
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.editor.update(cx, |editor, cx| {
|
||||
editor.set_leader_peer_id(leader_peer_id, window, cx)
|
||||
})
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.set_leader_id(leader_id, window, cx))
|
||||
}
|
||||
|
||||
fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
|
||||
|
|
|
@ -7,11 +7,11 @@ use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task
|
|||
use project::Project;
|
||||
use project::debugger::session::Session;
|
||||
use project::worktree_store::WorktreeStore;
|
||||
use rpc::proto::{self, PeerId};
|
||||
use rpc::proto;
|
||||
use running::RunningState;
|
||||
use ui::{Indicator, prelude::*};
|
||||
use workspace::{
|
||||
FollowableItem, ViewId, Workspace,
|
||||
CollaboratorId, FollowableItem, ViewId, Workspace,
|
||||
item::{self, Item},
|
||||
};
|
||||
|
||||
|
@ -189,9 +189,9 @@ impl FollowableItem for DebugSession {
|
|||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
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>,
|
||||
) {
|
||||
|
|
|
@ -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,6 +19928,17 @@ impl EditorSnapshot {
|
|||
self.buffer_snapshot
|
||||
.selections_in_range(range, false)
|
||||
.filter_map(move |(replica_id, line_mode, cursor_shape, selection)| {
|
||||
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();
|
||||
|
@ -19936,10 +19947,15 @@ impl EditorSnapshot {
|
|||
selection,
|
||||
cursor_shape,
|
||||
line_mode,
|
||||
participant_index,
|
||||
peer_id: collaborator.peer_id,
|
||||
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,14 +1149,18 @@ 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(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() {
|
||||
if let Some((local_selection_style, _)) = selections.first_mut()
|
||||
{
|
||||
*local_selection_style = cx
|
||||
.theme()
|
||||
.players()
|
||||
|
@ -1166,6 +1169,13 @@ impl EditorElement {
|
|||
}
|
||||
}
|
||||
}
|
||||
CollaboratorId::Agent => {
|
||||
if let Some((local_selection_style, _)) = selections.first_mut() {
|
||||
*local_selection_style = cx.theme().players().agent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut remote_selections = HashMap::default();
|
||||
for selection in snapshot.remote_selections_in_range(
|
||||
|
@ -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())?;
|
||||
|
|
|
@ -72,6 +72,7 @@ pub enum IconName {
|
|||
CopilotInit,
|
||||
Copy,
|
||||
CountdownTimer,
|
||||
Crosshair,
|
||||
CursorIBeam,
|
||||
Dash,
|
||||
DatabaseZap,
|
||||
|
|
|
@ -375,7 +375,7 @@ impl ProjectItem for ImageView {
|
|||
|
||||
fn for_project_item(
|
||||
project: Entity<Project>,
|
||||
_: &Pane,
|
||||
_: Option<&Pane>,
|
||||
item: Entity<Self::Item>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
|
|
@ -19,8 +19,8 @@ pub use crate::{
|
|||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use async_watch as watch;
|
||||
use clock::Lamport;
|
||||
pub use clock::ReplicaId;
|
||||
use clock::{AGENT_REPLICA_ID, Lamport};
|
||||
use collections::HashMap;
|
||||
use fs::MTime;
|
||||
use futures::channel::oneshot;
|
||||
|
@ -2132,6 +2132,31 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_agent_selections(
|
||||
&mut self,
|
||||
selections: Arc<[Selection<Anchor>]>,
|
||||
line_mode: bool,
|
||||
cursor_shape: CursorShape,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let lamport_timestamp = self.text.lamport_clock.tick();
|
||||
self.remote_selections.insert(
|
||||
AGENT_REPLICA_ID,
|
||||
SelectionSet {
|
||||
selections: selections.clone(),
|
||||
lamport_timestamp,
|
||||
line_mode,
|
||||
cursor_shape,
|
||||
},
|
||||
);
|
||||
self.non_text_state_update_count += 1;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn remove_agent_selections(&mut self, cx: &mut Context<Self>) {
|
||||
self.set_agent_selections(Arc::default(), false, Default::default(), cx);
|
||||
}
|
||||
|
||||
/// Replaces the buffer's entire text.
|
||||
pub fn set_text<T>(&mut self, text: T, cx: &mut Context<Self>) -> Option<clock::Lamport>
|
||||
where
|
||||
|
|
|
@ -68,9 +68,9 @@ use gpui::{
|
|||
};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
Buffer, BufferEvent, Capability, CodeLabel, Language, LanguageName, LanguageRegistry,
|
||||
PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction, Unclipped,
|
||||
language_settings::InlayHintKind, proto::split_operations,
|
||||
Buffer, BufferEvent, Capability, CodeLabel, CursorShape, Language, LanguageName,
|
||||
LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction,
|
||||
Unclipped, language_settings::InlayHintKind, proto::split_operations,
|
||||
};
|
||||
use lsp::{
|
||||
CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode,
|
||||
|
@ -138,7 +138,7 @@ const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
|
|||
const MAX_SEARCH_RESULT_FILES: usize = 5_000;
|
||||
const MAX_SEARCH_RESULT_RANGES: usize = 10_000;
|
||||
|
||||
pub trait ProjectItem {
|
||||
pub trait ProjectItem: 'static {
|
||||
fn try_open(
|
||||
project: &Entity<Project>,
|
||||
path: &ProjectPath,
|
||||
|
@ -197,6 +197,13 @@ pub struct Project {
|
|||
environment: Entity<ProjectEnvironment>,
|
||||
settings_observer: Entity<SettingsObserver>,
|
||||
toolchain_store: Option<Entity<ToolchainStore>>,
|
||||
agent_location: Option<AgentLocation>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AgentLocation {
|
||||
pub buffer: WeakEntity<Buffer>,
|
||||
pub position: Anchor,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -304,8 +311,11 @@ pub enum Event {
|
|||
RevealInProjectPanel(ProjectEntryId),
|
||||
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
|
||||
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
|
||||
AgentLocationChanged,
|
||||
}
|
||||
|
||||
pub struct AgentLocationChanged;
|
||||
|
||||
pub enum DebugAdapterClientState {
|
||||
Starting(Task<Option<Arc<DebugAdapterClient>>>),
|
||||
Running(Arc<DebugAdapterClient>),
|
||||
|
@ -986,6 +996,8 @@ impl Project {
|
|||
search_excluded_history: Self::new_search_history(),
|
||||
|
||||
toolchain_store: Some(toolchain_store),
|
||||
|
||||
agent_location: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1142,6 +1154,7 @@ impl Project {
|
|||
search_excluded_history: Self::new_search_history(),
|
||||
|
||||
toolchain_store: Some(toolchain_store),
|
||||
agent_location: None,
|
||||
};
|
||||
|
||||
// ssh -> local machine handlers
|
||||
|
@ -1381,6 +1394,7 @@ impl Project {
|
|||
environment,
|
||||
remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())),
|
||||
toolchain_store: None,
|
||||
agent_location: None,
|
||||
};
|
||||
this.set_role(role, cx);
|
||||
for worktree in worktrees {
|
||||
|
@ -4875,6 +4889,46 @@ impl Project {
|
|||
pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
|
||||
self.git_store.read(cx).status_for_buffer_id(buffer_id, cx)
|
||||
}
|
||||
|
||||
pub fn set_agent_location(
|
||||
&mut self,
|
||||
new_location: Option<AgentLocation>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(old_location) = self.agent_location.as_ref() {
|
||||
old_location
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| buffer.remove_agent_selections(cx))
|
||||
.ok();
|
||||
}
|
||||
|
||||
if let Some(location) = new_location.as_ref() {
|
||||
location
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_agent_selections(
|
||||
Arc::from([language::Selection {
|
||||
id: 0,
|
||||
start: location.position,
|
||||
end: location.position,
|
||||
reversed: false,
|
||||
goal: language::SelectionGoal::None,
|
||||
}]),
|
||||
false,
|
||||
CursorShape::Hollow,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
self.agent_location = new_location;
|
||||
cx.emit(Event::AgentLocationChanged);
|
||||
}
|
||||
|
||||
pub fn agent_location(&self) -> Option<AgentLocation> {
|
||||
self.agent_location.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PathMatchCandidateSet {
|
||||
|
|
|
@ -5291,7 +5291,7 @@ impl ProjectItem for TestProjectItemView {
|
|||
|
||||
fn for_project_item(
|
||||
_: Entity<Project>,
|
||||
_: &Pane,
|
||||
_: Option<&Pane>,
|
||||
project_item: Entity<Self::Item>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
|
|
@ -829,7 +829,7 @@ impl ProjectItem for NotebookEditor {
|
|||
|
||||
fn for_project_item(
|
||||
project: Entity<Project>,
|
||||
_: &Pane,
|
||||
_: Option<&Pane>,
|
||||
item: Entity<Self::Item>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
|
|
@ -129,6 +129,10 @@ impl PlayerColors {
|
|||
*self.0.first().unwrap()
|
||||
}
|
||||
|
||||
pub fn agent(&self) -> PlayerColor {
|
||||
*self.0.last().unwrap()
|
||||
}
|
||||
|
||||
pub fn absent(&self) -> PlayerColor {
|
||||
*self.0.last().unwrap()
|
||||
}
|
||||
|
|
|
@ -1143,7 +1143,7 @@ impl Vim {
|
|||
&& !newest_selection_empty
|
||||
&& self.mode == Mode::Normal
|
||||
// When following someone, don't switch vim mode.
|
||||
&& editor.leader_peer_id().is_none()
|
||||
&& editor.leader_id().is_none()
|
||||
{
|
||||
if preserve_selection {
|
||||
self.switch_mode(Mode::Visual, true, window, cx);
|
||||
|
@ -1468,7 +1468,7 @@ impl Vim {
|
|||
fn local_selections_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(editor) = self.editor() else { return };
|
||||
|
||||
if editor.read(cx).leader_peer_id().is_some() {
|
||||
if editor.read(cx).leader_id().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ clock.workspace = true
|
|||
collections.workspace = true
|
||||
component.workspace = true
|
||||
db.workspace = true
|
||||
derive_more.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
use crate::{
|
||||
DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry,
|
||||
ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory,
|
||||
SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
pane::{self, Pane},
|
||||
persistence::model::ItemId,
|
||||
searchable::SearchableItemHandle,
|
||||
workspace_settings::{AutosaveSetting, WorkspaceSettings},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::{
|
||||
Client,
|
||||
proto::{self, PeerId},
|
||||
};
|
||||
use client::{Client, proto};
|
||||
use futures::{StreamExt, channel::mpsc};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, Context, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
|
@ -770,7 +767,7 @@ impl<T: Item> ItemHandle for Entity<T> {
|
|||
proto::UpdateView {
|
||||
id: item
|
||||
.remote_id(workspace.client(), window, cx)
|
||||
.map(|id| id.to_proto()),
|
||||
.and_then(|id| id.to_proto()),
|
||||
variant: pending_update.borrow_mut().take(),
|
||||
leader_id,
|
||||
},
|
||||
|
@ -810,13 +807,27 @@ impl<T: Item> ItemHandle for Entity<T> {
|
|||
}
|
||||
|
||||
if item.item_focus_handle(cx).contains_focused(window, cx) {
|
||||
match leader_id {
|
||||
Some(CollaboratorId::Agent) => {}
|
||||
Some(CollaboratorId::PeerId(leader_peer_id)) => {
|
||||
item.add_event_to_update_proto(
|
||||
event,
|
||||
&mut pending_update.borrow_mut(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pending_update_tx.unbounded_send(leader_id).ok();
|
||||
pending_update_tx.unbounded_send(Some(leader_peer_id)).ok();
|
||||
}
|
||||
None => {
|
||||
item.add_event_to_update_proto(
|
||||
event,
|
||||
&mut pending_update.borrow_mut(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pending_update_tx.unbounded_send(None).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1081,7 +1092,7 @@ pub trait ProjectItem: Item {
|
|||
|
||||
fn for_project_item(
|
||||
project: Entity<Project>,
|
||||
pane: &Pane,
|
||||
pane: Option<&Pane>,
|
||||
item: Entity<Self::Item>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -1126,19 +1137,31 @@ pub trait FollowableItem: Item {
|
|||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn is_project_item(&self, window: &Window, cx: &App) -> bool;
|
||||
fn set_leader_peer_id(
|
||||
fn set_leader_id(
|
||||
&mut self,
|
||||
leader_peer_id: Option<PeerId>,
|
||||
leader_peer_id: Option<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
);
|
||||
fn dedup(&self, existing: &Self, window: &Window, cx: &App) -> Option<Dedup>;
|
||||
fn update_agent_location(
|
||||
&mut self,
|
||||
_location: language::Anchor,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FollowableItemHandle: ItemHandle {
|
||||
fn remote_id(&self, client: &Arc<Client>, window: &mut Window, cx: &mut App) -> Option<ViewId>;
|
||||
fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle>;
|
||||
fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, window: &mut Window, cx: &mut App);
|
||||
fn set_leader_id(
|
||||
&self,
|
||||
leader_peer_id: Option<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant>;
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
|
@ -1162,13 +1185,14 @@ pub trait FollowableItemHandle: ItemHandle {
|
|||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Dedup>;
|
||||
fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App);
|
||||
}
|
||||
|
||||
impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
|
||||
fn remote_id(&self, client: &Arc<Client>, _: &mut Window, cx: &mut App) -> Option<ViewId> {
|
||||
self.read(cx).remote_id().or_else(|| {
|
||||
client.peer_id().map(|creator| ViewId {
|
||||
creator,
|
||||
creator: CollaboratorId::PeerId(creator),
|
||||
id: self.item_id().as_u64(),
|
||||
})
|
||||
})
|
||||
|
@ -1178,15 +1202,8 @@ impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
|
|||
Box::new(self.downgrade())
|
||||
}
|
||||
|
||||
fn set_leader_peer_id(
|
||||
&self,
|
||||
leader_peer_id: Option<PeerId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.set_leader_peer_id(leader_peer_id, window, cx)
|
||||
})
|
||||
fn set_leader_id(&self, leader_id: Option<CollaboratorId>, window: &mut Window, cx: &mut App) {
|
||||
self.update(cx, |this, cx| this.set_leader_id(leader_id, window, cx))
|
||||
}
|
||||
|
||||
fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
|
||||
|
@ -1237,6 +1254,12 @@ impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
|
|||
let existing = existing.to_any().downcast::<T>().ok()?;
|
||||
self.read(cx).dedup(existing.read(cx), window, cx)
|
||||
}
|
||||
|
||||
fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.update_agent_location(location, window, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WeakFollowableItemHandle: Send + Sync {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
use crate::{
|
||||
AppState, FollowerState, Pane, Workspace, WorkspaceSettings,
|
||||
AppState, CollaboratorId, FollowerState, Pane, Workspace, WorkspaceSettings,
|
||||
pane_group::element::pane_axis,
|
||||
workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical},
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use call::{ActiveCall, ParticipantLocation};
|
||||
use client::proto::PeerId;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels,
|
||||
|
@ -188,7 +187,7 @@ pub enum Member {
|
|||
#[derive(Clone, Copy)]
|
||||
pub struct PaneRenderContext<'a> {
|
||||
pub project: &'a Entity<Project>,
|
||||
pub follower_states: &'a HashMap<PeerId, FollowerState>,
|
||||
pub follower_states: &'a HashMap<CollaboratorId, FollowerState>,
|
||||
pub active_call: Option<&'a Entity<ActiveCall>>,
|
||||
pub active_pane: &'a Entity<Pane>,
|
||||
pub app_state: &'a Arc<AppState>,
|
||||
|
@ -243,21 +242,26 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
|
|||
None
|
||||
}
|
||||
});
|
||||
let leader = follower_state.as_ref().and_then(|(leader_id, _)| {
|
||||
let room = self.active_call?.read(cx).room()?.read(cx);
|
||||
room.remote_participant_for_peer_id(*leader_id)
|
||||
});
|
||||
let Some(leader) = leader else {
|
||||
let Some((leader_id, follower_state)) = follower_state else {
|
||||
return LeaderDecoration::default();
|
||||
};
|
||||
let is_in_unshared_view = follower_state.as_ref().map_or(false, |(_, state)| {
|
||||
state
|
||||
.active_view_id
|
||||
.is_some_and(|view_id| !state.items_by_leader_view_id.contains_key(&view_id))
|
||||
|
||||
let mut leader_color;
|
||||
let status_box;
|
||||
match leader_id {
|
||||
CollaboratorId::PeerId(peer_id) => {
|
||||
let Some(leader) = self.active_call.as_ref().and_then(|call| {
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
room.remote_participant_for_peer_id(peer_id)
|
||||
}) else {
|
||||
return LeaderDecoration::default();
|
||||
};
|
||||
|
||||
let is_in_unshared_view = follower_state.active_view_id.is_some_and(|view_id| {
|
||||
!follower_state
|
||||
.items_by_leader_view_id
|
||||
.contains_key(&view_id)
|
||||
});
|
||||
let is_in_panel = follower_state
|
||||
.as_ref()
|
||||
.map_or(false, |(_, state)| state.dock_pane.is_some());
|
||||
|
||||
let mut leader_join_data = None;
|
||||
let leader_status_box = match leader.location {
|
||||
|
@ -288,17 +292,7 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
|
|||
leader.user.github_login
|
||||
))),
|
||||
};
|
||||
let mut leader_color = cx
|
||||
.theme()
|
||||
.players()
|
||||
.color_for_participant(leader.participant_index.0)
|
||||
.cursor;
|
||||
if is_in_panel {
|
||||
leader_color.fade_out(0.75);
|
||||
} else {
|
||||
leader_color.fade_out(0.3);
|
||||
}
|
||||
let status_box = leader_status_box.map(|status| {
|
||||
status_box = leader_status_box.map(|status| {
|
||||
div()
|
||||
.absolute()
|
||||
.w_96()
|
||||
|
@ -311,8 +305,9 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
|
|||
leader_join_data,
|
||||
|this, (leader_project_id, leader_user_id)| {
|
||||
let app_state = self.app_state.clone();
|
||||
this.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, move |_, _, cx| {
|
||||
this.cursor_pointer().on_mouse_down(
|
||||
MouseButton::Left,
|
||||
move |_, _, cx| {
|
||||
crate::join_in_room_project(
|
||||
leader_project_id,
|
||||
leader_user_id,
|
||||
|
@ -320,11 +315,31 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
|
|||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
});
|
||||
leader_color = cx
|
||||
.theme()
|
||||
.players()
|
||||
.color_for_participant(leader.participant_index.0)
|
||||
.cursor;
|
||||
}
|
||||
CollaboratorId::Agent => {
|
||||
status_box = None;
|
||||
leader_color = cx.theme().players().agent().cursor;
|
||||
}
|
||||
}
|
||||
|
||||
let is_in_panel = follower_state.dock_pane.is_some();
|
||||
if is_in_panel {
|
||||
leader_color.fade_out(0.75);
|
||||
} else {
|
||||
leader_color.fade_out(0.3);
|
||||
}
|
||||
|
||||
LeaderDecoration {
|
||||
status_box,
|
||||
border: Some(leader_color),
|
||||
|
@ -339,6 +354,7 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
|
|||
self.workspace
|
||||
}
|
||||
}
|
||||
|
||||
impl Member {
|
||||
fn new_axis(old_pane: Entity<Pane>, new_pane: Entity<Pane>, direction: SplitDirection) -> Self {
|
||||
use Axis::*;
|
||||
|
|
|
@ -24,7 +24,6 @@ use client::{
|
|||
proto::{self, ErrorCode, PanelId, PeerId},
|
||||
};
|
||||
use collections::{HashMap, HashSet, hash_map};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub use dock::Panel;
|
||||
use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
|
||||
use futures::{
|
||||
|
@ -36,12 +35,12 @@ use futures::{
|
|||
future::try_join_all,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context, CursorStyle,
|
||||
Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global,
|
||||
Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions, Point, PromptLevel,
|
||||
Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity, WindowBounds,
|
||||
WindowHandle, WindowId, WindowOptions, action_as, actions, canvas, impl_action_as,
|
||||
impl_actions, point, relative, size, transparent_black,
|
||||
Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
|
||||
CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
Focusable, Global, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions,
|
||||
Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity,
|
||||
WindowBounds, WindowHandle, WindowId, WindowOptions, action_as, actions, canvas,
|
||||
impl_action_as, impl_actions, point, relative, size, transparent_black,
|
||||
};
|
||||
pub use history_manager::*;
|
||||
pub use item::{
|
||||
|
@ -451,29 +450,37 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
|||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deref, DerefMut)]
|
||||
struct ProjectItemOpeners(Vec<ProjectItemOpener>);
|
||||
type BuildProjectItemFn =
|
||||
fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
|
||||
|
||||
type ProjectItemOpener = fn(
|
||||
type BuildProjectItemForPathFn =
|
||||
fn(
|
||||
&Entity<Project>,
|
||||
&ProjectPath,
|
||||
&mut Window,
|
||||
&mut App,
|
||||
)
|
||||
-> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
|
||||
) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
|
||||
|
||||
type WorkspaceItemBuilder =
|
||||
Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
|
||||
#[derive(Clone, Default)]
|
||||
struct ProjectItemRegistry {
|
||||
build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
|
||||
build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
|
||||
}
|
||||
|
||||
impl Global for ProjectItemOpeners {}
|
||||
|
||||
/// Registers a [ProjectItem] for the app. When opening a file, all the registered
|
||||
/// items will get a chance to open the file, starting from the project item that
|
||||
/// was added last.
|
||||
pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
|
||||
let builders = cx.default_global::<ProjectItemOpeners>();
|
||||
builders.push(|project, project_path, window, cx| {
|
||||
let project_item = <I::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
|
||||
impl ProjectItemRegistry {
|
||||
fn register<T: ProjectItem>(&mut self) {
|
||||
self.build_project_item_fns_by_type.insert(
|
||||
TypeId::of::<T::Item>(),
|
||||
|item, project, pane, window, cx| {
|
||||
let item = item.downcast().unwrap();
|
||||
Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
|
||||
as Box<dyn ItemHandle>
|
||||
},
|
||||
);
|
||||
self.build_project_item_for_path_fns
|
||||
.push(|project, project_path, window, cx| {
|
||||
let project_item =
|
||||
<T::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
|
||||
let project = project.clone();
|
||||
Some(window.spawn(cx, async move |cx| {
|
||||
let project_item = project_item.await?;
|
||||
|
@ -481,9 +488,9 @@ pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
|
|||
project_item.read_with(cx, project::ProjectItem::entry_id)?;
|
||||
let build_workspace_item = Box::new(
|
||||
|pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
|
||||
Box::new(
|
||||
cx.new(|cx| I::for_project_item(project, pane, project_item, window, cx)),
|
||||
) as Box<dyn ItemHandle>
|
||||
Box::new(cx.new(|cx| {
|
||||
T::for_project_item(project, Some(pane), project_item, window, cx)
|
||||
})) as Box<dyn ItemHandle>
|
||||
},
|
||||
) as Box<_>;
|
||||
Ok((project_entry_id, build_workspace_item))
|
||||
|
@ -491,6 +498,51 @@ pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
|
|||
});
|
||||
}
|
||||
|
||||
fn open_path(
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
path: &ProjectPath,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
|
||||
let Some(open_project_item) = self
|
||||
.build_project_item_for_path_fns
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|open_project_item| open_project_item(&project, &path, window, cx))
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
|
||||
};
|
||||
open_project_item
|
||||
}
|
||||
|
||||
fn build_item<T: project::ProjectItem>(
|
||||
&self,
|
||||
item: Entity<T>,
|
||||
project: Entity<Project>,
|
||||
pane: Option<&Pane>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
let build = self
|
||||
.build_project_item_fns_by_type
|
||||
.get(&TypeId::of::<T>())?;
|
||||
Some(build(item.into_any(), project, pane, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
type WorkspaceItemBuilder =
|
||||
Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
|
||||
|
||||
impl Global for ProjectItemRegistry {}
|
||||
|
||||
/// Registers a [ProjectItem] for the app. When opening a file, all the registered
|
||||
/// items will get a chance to open the file, starting from the project item that
|
||||
/// was added last.
|
||||
pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
|
||||
cx.default_global::<ProjectItemRegistry>().register::<I>();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
|
||||
|
||||
|
@ -666,6 +718,24 @@ pub struct WorkspaceStore {
|
|||
_subscriptions: Vec<client::Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
|
||||
pub enum CollaboratorId {
|
||||
PeerId(PeerId),
|
||||
Agent,
|
||||
}
|
||||
|
||||
impl From<PeerId> for CollaboratorId {
|
||||
fn from(peer_id: PeerId) -> Self {
|
||||
CollaboratorId::PeerId(peer_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PeerId> for CollaboratorId {
|
||||
fn from(peer_id: &PeerId) -> Self {
|
||||
CollaboratorId::PeerId(*peer_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
struct Follower {
|
||||
project_id: Option<u64>,
|
||||
|
@ -852,8 +922,8 @@ pub struct Workspace {
|
|||
titlebar_item: Option<AnyView>,
|
||||
notifications: Notifications,
|
||||
project: Entity<Project>,
|
||||
follower_states: HashMap<PeerId, FollowerState>,
|
||||
last_leaders_by_pane: HashMap<WeakEntity<Pane>, PeerId>,
|
||||
follower_states: HashMap<CollaboratorId, FollowerState>,
|
||||
last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
|
||||
window_edited: bool,
|
||||
dirty_items: HashMap<EntityId, Subscription>,
|
||||
active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
|
||||
|
@ -883,7 +953,7 @@ impl EventEmitter<Event> for Workspace {}
|
|||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ViewId {
|
||||
pub creator: PeerId,
|
||||
pub creator: CollaboratorId,
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
|
@ -984,6 +1054,10 @@ impl Workspace {
|
|||
);
|
||||
}
|
||||
|
||||
project::Event::AgentLocationChanged => {
|
||||
this.handle_agent_location_changed(window, cx)
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
cx.notify()
|
||||
|
@ -3083,15 +3157,8 @@ impl Workspace {
|
|||
cx: &mut App,
|
||||
) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
|
||||
let project = self.project().clone();
|
||||
let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
|
||||
let Some(open_project_item) = project_item_builders
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|open_project_item| open_project_item(&project, &path, window, cx))
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
|
||||
};
|
||||
open_project_item
|
||||
let registry = cx.default_global::<ProjectItemRegistry>().clone();
|
||||
registry.open_path(&project, &path, window, cx)
|
||||
}
|
||||
|
||||
pub fn find_project_item<T>(
|
||||
|
@ -3152,7 +3219,9 @@ impl Workspace {
|
|||
}
|
||||
|
||||
let item = pane.update(cx, |pane, cx| {
|
||||
cx.new(|cx| T::for_project_item(self.project().clone(), pane, project_item, window, cx))
|
||||
cx.new(|cx| {
|
||||
T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
|
||||
})
|
||||
});
|
||||
let item_id = item.item_id();
|
||||
let mut destination_index = None;
|
||||
|
@ -3605,7 +3674,7 @@ impl Workspace {
|
|||
pane: &Entity<Pane>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Option<PeerId> {
|
||||
) -> Option<CollaboratorId> {
|
||||
let leader_id = self.leader_for_pane(pane)?;
|
||||
self.unfollow(leader_id, window, cx);
|
||||
Some(leader_id)
|
||||
|
@ -3785,9 +3854,9 @@ impl Workspace {
|
|||
|
||||
fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.follower_states.retain(|leader_id, state| {
|
||||
if *leader_id == peer_id {
|
||||
if *leader_id == CollaboratorId::PeerId(peer_id) {
|
||||
for item in state.items_by_leader_view_id.values() {
|
||||
item.view.set_leader_peer_id(None, window, cx);
|
||||
item.view.set_leader_id(None, window, cx);
|
||||
}
|
||||
false
|
||||
} else {
|
||||
|
@ -3799,10 +3868,11 @@ impl Workspace {
|
|||
|
||||
pub fn start_following(
|
||||
&mut self,
|
||||
leader_id: PeerId,
|
||||
leader_id: impl Into<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let leader_id = leader_id.into();
|
||||
let pane = self.active_pane().clone();
|
||||
|
||||
self.last_leaders_by_pane
|
||||
|
@ -3820,12 +3890,14 @@ impl Workspace {
|
|||
);
|
||||
cx.notify();
|
||||
|
||||
match leader_id {
|
||||
CollaboratorId::PeerId(leader_peer_id) => {
|
||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let request = self.app_state.client.request(proto::Follow {
|
||||
room_id,
|
||||
project_id,
|
||||
leader_id: Some(leader_id),
|
||||
leader_id: Some(leader_peer_id),
|
||||
});
|
||||
|
||||
Some(cx.spawn_in(window, async move |this, cx| {
|
||||
|
@ -3842,7 +3914,7 @@ impl Workspace {
|
|||
Ok::<_, anyhow::Error>(())
|
||||
})??;
|
||||
if let Some(view) = response.active_view {
|
||||
Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?;
|
||||
Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
|
||||
}
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.leader_updated(leader_id, window, cx)
|
||||
|
@ -3850,6 +3922,12 @@ impl Workspace {
|
|||
Ok(())
|
||||
}))
|
||||
}
|
||||
CollaboratorId::Agent => {
|
||||
self.leader_updated(leader_id, window, cx)?;
|
||||
Some(Task::ready(Ok(())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn follow_next_collaborator(
|
||||
&mut self,
|
||||
|
@ -3861,26 +3939,34 @@ impl Workspace {
|
|||
let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
|
||||
let mut collaborators = collaborators.keys().copied();
|
||||
for peer_id in collaborators.by_ref() {
|
||||
if peer_id == leader_id {
|
||||
if CollaboratorId::PeerId(peer_id) == leader_id {
|
||||
break;
|
||||
}
|
||||
}
|
||||
collaborators.next()
|
||||
collaborators.next().map(CollaboratorId::PeerId)
|
||||
} else if let Some(last_leader_id) =
|
||||
self.last_leaders_by_pane.get(&self.active_pane.downgrade())
|
||||
{
|
||||
if collaborators.contains_key(last_leader_id) {
|
||||
match last_leader_id {
|
||||
CollaboratorId::PeerId(peer_id) => {
|
||||
if collaborators.contains_key(peer_id) {
|
||||
Some(*last_leader_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
CollaboratorId::Agent => Some(CollaboratorId::Agent),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let pane = self.active_pane.clone();
|
||||
let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
|
||||
else {
|
||||
let Some(leader_id) = next_leader_id.or_else(|| {
|
||||
Some(CollaboratorId::PeerId(
|
||||
collaborators.keys().copied().next()?,
|
||||
))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
|
||||
|
@ -3891,12 +3977,20 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn follow(&mut self, leader_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
pub fn follow(
|
||||
&mut self,
|
||||
leader_id: impl Into<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let leader_id = leader_id.into();
|
||||
|
||||
if let CollaboratorId::PeerId(peer_id) = leader_id {
|
||||
let Some(room) = ActiveCall::global(cx).read(cx).room() else {
|
||||
return;
|
||||
};
|
||||
let room = room.read(cx);
|
||||
let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
|
||||
let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
@ -3920,6 +4014,7 @@ impl Workspace {
|
|||
crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
// if you're already following, find the right pane and focus it.
|
||||
if let Some(follower_state) = self.follower_states.get(&leader_id) {
|
||||
|
@ -3936,16 +4031,19 @@ impl Workspace {
|
|||
|
||||
pub fn unfollow(
|
||||
&mut self,
|
||||
leader_id: PeerId,
|
||||
leader_id: impl Into<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
cx.notify();
|
||||
|
||||
let leader_id = leader_id.into();
|
||||
let state = self.follower_states.remove(&leader_id)?;
|
||||
for (_, item) in state.items_by_leader_view_id {
|
||||
item.view.set_leader_peer_id(None, window, cx);
|
||||
item.view.set_leader_id(None, window, cx);
|
||||
}
|
||||
|
||||
if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||
self.app_state
|
||||
|
@ -3953,15 +4051,16 @@ impl Workspace {
|
|||
.send(proto::Unfollow {
|
||||
room_id,
|
||||
project_id,
|
||||
leader_id: Some(leader_id),
|
||||
leader_id: Some(leader_peer_id),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
|
||||
self.follower_states.contains_key(&peer_id)
|
||||
pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
|
||||
self.follower_states.contains_key(&id.into())
|
||||
}
|
||||
|
||||
fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
@ -4096,6 +4195,10 @@ impl Workspace {
|
|||
let leader_id = self
|
||||
.pane_for(&*item)
|
||||
.and_then(|pane| self.leader_for_pane(&pane));
|
||||
let leader_peer_id = match leader_id {
|
||||
Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
|
||||
Some(CollaboratorId::Agent) | None => None,
|
||||
};
|
||||
|
||||
let item_handle = item.to_followable_item_handle(cx)?;
|
||||
let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
|
||||
|
@ -4109,8 +4212,8 @@ impl Workspace {
|
|||
}
|
||||
|
||||
Some(proto::View {
|
||||
id: Some(id.to_proto()),
|
||||
leader_id,
|
||||
id: id.to_proto(),
|
||||
leader_id: leader_peer_id,
|
||||
variant: Some(variant),
|
||||
panel_id: panel_id.map(|id| id as i32),
|
||||
})
|
||||
|
@ -4155,7 +4258,7 @@ impl Workspace {
|
|||
proto::update_followers::Variant::CreateView(view) => {
|
||||
let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
|
||||
let should_add_view = this.update(cx, |this, _| {
|
||||
if let Some(state) = this.follower_states.get_mut(&leader_id) {
|
||||
if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
|
||||
anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
|
||||
} else {
|
||||
anyhow::Ok(false)
|
||||
|
@ -4168,7 +4271,7 @@ impl Workspace {
|
|||
}
|
||||
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
|
||||
let should_add_view = this.update(cx, |this, _| {
|
||||
if let Some(state) = this.follower_states.get_mut(&leader_id) {
|
||||
if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
|
||||
state.active_view_id = update_active_view
|
||||
.view
|
||||
.as_ref()
|
||||
|
@ -4202,7 +4305,7 @@ impl Workspace {
|
|||
let mut tasks = Vec::new();
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let project = this.project.clone();
|
||||
if let Some(state) = this.follower_states.get(&leader_id) {
|
||||
if let Some(state) = this.follower_states.get(&leader_id.into()) {
|
||||
let view_id = ViewId::from_proto(id.clone())?;
|
||||
if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
|
||||
tasks.push(item.view.apply_update_proto(
|
||||
|
@ -4241,7 +4344,7 @@ impl Workspace {
|
|||
let pane = this.update(cx, |this, _cx| {
|
||||
let state = this
|
||||
.follower_states
|
||||
.get(&leader_id)
|
||||
.get(&leader_id.into())
|
||||
.context("stopped following")?;
|
||||
anyhow::Ok(state.pane().clone())
|
||||
})??;
|
||||
|
@ -4304,8 +4407,8 @@ impl Workspace {
|
|||
};
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let state = this.follower_states.get_mut(&leader_id)?;
|
||||
item.set_leader_peer_id(Some(leader_id), window, cx);
|
||||
let state = this.follower_states.get_mut(&leader_id.into())?;
|
||||
item.set_leader_id(Some(leader_id.into()), window, cx);
|
||||
state.items_by_leader_view_id.insert(
|
||||
id,
|
||||
FollowerView {
|
||||
|
@ -4320,6 +4423,71 @@ impl Workspace {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(agent_location) = self.project.read(cx).agent_location() {
|
||||
let buffer_entity_id = agent_location.buffer.entity_id();
|
||||
let view_id = ViewId {
|
||||
creator: CollaboratorId::Agent,
|
||||
id: buffer_entity_id.as_u64(),
|
||||
};
|
||||
follower_state.active_view_id = Some(view_id);
|
||||
|
||||
let item = match follower_state.items_by_leader_view_id.entry(view_id) {
|
||||
hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let existing_view =
|
||||
follower_state
|
||||
.center_pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| {
|
||||
let item = item.to_followable_item_handle(cx)?;
|
||||
if item.is_singleton(cx)
|
||||
&& item.project_item_model_ids(cx).as_slice()
|
||||
== [buffer_entity_id]
|
||||
{
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let view = existing_view.or_else(|| {
|
||||
agent_location.buffer.upgrade().and_then(|buffer| {
|
||||
cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
|
||||
registry.build_item(buffer, self.project.clone(), None, window, cx)
|
||||
})?
|
||||
.to_followable_item_handle(cx)
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(view) = view {
|
||||
Some(entry.insert(FollowerView {
|
||||
view,
|
||||
location: None,
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(item) = item {
|
||||
item.view
|
||||
.set_leader_id(Some(CollaboratorId::Agent), window, cx);
|
||||
item.view
|
||||
.update_agent_location(agent_location.position, window, cx);
|
||||
}
|
||||
} else {
|
||||
follower_state.active_view_id = None;
|
||||
}
|
||||
|
||||
self.leader_updated(CollaboratorId::Agent, window, cx);
|
||||
}
|
||||
|
||||
pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
|
||||
let mut is_project_item = true;
|
||||
let mut update = proto::UpdateActiveView::default();
|
||||
|
@ -4331,6 +4499,10 @@ impl Workspace {
|
|||
let leader_id = self
|
||||
.pane_for(&*item)
|
||||
.and_then(|pane| self.leader_for_pane(&pane));
|
||||
let leader_peer_id = match leader_id {
|
||||
Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
|
||||
Some(CollaboratorId::Agent) | None => None,
|
||||
};
|
||||
|
||||
if let Some(item) = item.to_followable_item_handle(cx) {
|
||||
let id = item
|
||||
|
@ -4340,8 +4512,8 @@ impl Workspace {
|
|||
if let Some(id) = id.clone() {
|
||||
if let Some(variant) = item.to_state_proto(window, cx) {
|
||||
let view = Some(proto::View {
|
||||
id: Some(id.clone()),
|
||||
leader_id,
|
||||
id: id.clone(),
|
||||
leader_id: leader_peer_id,
|
||||
variant: Some(variant),
|
||||
panel_id: panel_id.map(|id| id as i32),
|
||||
});
|
||||
|
@ -4350,8 +4522,8 @@ impl Workspace {
|
|||
update = proto::UpdateActiveView {
|
||||
view,
|
||||
// TODO: Remove after version 0.145.x stabilizes.
|
||||
id: Some(id.clone()),
|
||||
leader_id,
|
||||
id: id.clone(),
|
||||
leader_id: leader_peer_id,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -4420,7 +4592,7 @@ impl Workspace {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<PeerId> {
|
||||
pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
|
||||
self.follower_states.iter().find_map(|(leader_id, state)| {
|
||||
if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
|
||||
Some(*leader_id)
|
||||
|
@ -4432,49 +4604,19 @@ impl Workspace {
|
|||
|
||||
fn leader_updated(
|
||||
&mut self,
|
||||
leader_id: PeerId,
|
||||
leader_id: impl Into<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
cx.notify();
|
||||
|
||||
let call = self.active_call()?;
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
let participant = room.remote_participant_for_peer_id(leader_id)?;
|
||||
|
||||
let leader_in_this_app;
|
||||
let leader_in_this_project;
|
||||
match participant.location {
|
||||
call::ParticipantLocation::SharedProject { project_id } => {
|
||||
leader_in_this_app = true;
|
||||
leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
|
||||
}
|
||||
call::ParticipantLocation::UnsharedProject => {
|
||||
leader_in_this_app = true;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
call::ParticipantLocation::External => {
|
||||
leader_in_this_app = false;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
let leader_id = leader_id.into();
|
||||
let (panel_id, item) = match leader_id {
|
||||
CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
|
||||
CollaboratorId::Agent => (None, self.active_item_for_agent()?),
|
||||
};
|
||||
|
||||
let state = self.follower_states.get(&leader_id)?;
|
||||
let mut item_to_activate = None;
|
||||
if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
|
||||
if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
|
||||
if leader_in_this_project || !item.view.is_project_item(window, cx) {
|
||||
item_to_activate = Some((item.location, item.view.boxed_clone()));
|
||||
}
|
||||
}
|
||||
} else if let Some(shared_screen) =
|
||||
self.shared_screen_for_peer(leader_id, &state.center_pane, window, cx)
|
||||
{
|
||||
item_to_activate = Some((None, Box::new(shared_screen)));
|
||||
}
|
||||
|
||||
let (panel_id, item) = item_to_activate?;
|
||||
|
||||
let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
|
||||
let pane;
|
||||
if let Some(panel_id) = panel_id {
|
||||
|
@ -4504,7 +4646,60 @@ impl Workspace {
|
|||
}
|
||||
});
|
||||
|
||||
None
|
||||
Some(item)
|
||||
}
|
||||
|
||||
fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
|
||||
let state = self.follower_states.get(&CollaboratorId::Agent)?;
|
||||
let active_view_id = state.active_view_id?;
|
||||
Some(
|
||||
state
|
||||
.items_by_leader_view_id
|
||||
.get(&active_view_id)?
|
||||
.view
|
||||
.boxed_clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn active_item_for_peer(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
|
||||
let call = self.active_call()?;
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
let participant = room.remote_participant_for_peer_id(peer_id)?;
|
||||
let leader_in_this_app;
|
||||
let leader_in_this_project;
|
||||
match participant.location {
|
||||
call::ParticipantLocation::SharedProject { project_id } => {
|
||||
leader_in_this_app = true;
|
||||
leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
|
||||
}
|
||||
call::ParticipantLocation::UnsharedProject => {
|
||||
leader_in_this_app = true;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
call::ParticipantLocation::External => {
|
||||
leader_in_this_app = false;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
};
|
||||
let state = self.follower_states.get(&peer_id.into())?;
|
||||
let mut item_to_activate = None;
|
||||
if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
|
||||
if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
|
||||
if leader_in_this_project || !item.view.is_project_item(window, cx) {
|
||||
item_to_activate = Some((item.location, item.view.boxed_clone()));
|
||||
}
|
||||
}
|
||||
} else if let Some(shared_screen) =
|
||||
self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
|
||||
{
|
||||
item_to_activate = Some((None, Box::new(shared_screen)));
|
||||
}
|
||||
item_to_activate
|
||||
}
|
||||
|
||||
fn shared_screen_for_peer(
|
||||
|
@ -4571,7 +4766,7 @@ impl Workspace {
|
|||
match event {
|
||||
call::room::Event::ParticipantLocationChanged { participant_id }
|
||||
| call::room::Event::RemoteVideoTracksChanged { participant_id } => {
|
||||
self.leader_updated(*participant_id, window, cx);
|
||||
self.leader_updated(participant_id, window, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -5285,7 +5480,7 @@ impl Workspace {
|
|||
}
|
||||
|
||||
fn leader_border_for_pane(
|
||||
follower_states: &HashMap<PeerId, FollowerState>,
|
||||
follower_states: &HashMap<CollaboratorId, FollowerState>,
|
||||
pane: &Entity<Pane>,
|
||||
_: &Window,
|
||||
cx: &App,
|
||||
|
@ -5298,14 +5493,18 @@ fn leader_border_for_pane(
|
|||
}
|
||||
})?;
|
||||
|
||||
let mut leader_color = match leader_id {
|
||||
CollaboratorId::PeerId(leader_peer_id) => {
|
||||
let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
|
||||
let leader = room.remote_participant_for_peer_id(leader_id)?;
|
||||
let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
|
||||
|
||||
let mut leader_color = cx
|
||||
.theme()
|
||||
cx.theme()
|
||||
.players()
|
||||
.color_for_participant(leader.participant_index.0)
|
||||
.cursor;
|
||||
.cursor
|
||||
}
|
||||
CollaboratorId::Agent => cx.theme().players().agent().cursor,
|
||||
};
|
||||
leader_color.fade_out(0.3);
|
||||
Some(
|
||||
div()
|
||||
|
@ -6029,15 +6228,20 @@ impl ViewId {
|
|||
Ok(Self {
|
||||
creator: message
|
||||
.creator
|
||||
.map(CollaboratorId::PeerId)
|
||||
.ok_or_else(|| anyhow!("creator is missing"))?,
|
||||
id: message.id,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn to_proto(self) -> proto::ViewId {
|
||||
proto::ViewId {
|
||||
creator: Some(self.creator),
|
||||
pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
|
||||
if let CollaboratorId::PeerId(peer_id) = self.creator {
|
||||
Some(proto::ViewId {
|
||||
creator: Some(peer_id),
|
||||
id: self.id,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9069,7 +9273,7 @@ mod tests {
|
|||
|
||||
fn for_project_item(
|
||||
_project: Entity<Project>,
|
||||
_pane: &Pane,
|
||||
_pane: Option<&Pane>,
|
||||
_item: Entity<Self::Item>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -9144,7 +9348,7 @@ mod tests {
|
|||
|
||||
fn for_project_item(
|
||||
_project: Entity<Project>,
|
||||
_pane: &Pane,
|
||||
_pane: Option<&Pane>,
|
||||
_item: Entity<Self::Item>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -9191,7 +9395,7 @@ mod tests {
|
|||
|
||||
fn for_project_item(
|
||||
_project: Entity<Project>,
|
||||
_pane: &Pane,
|
||||
_pane: Option<&Pane>,
|
||||
_item: Entity<Self::Item>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue