Fix inline assist panic (#43364)

Fixes a panic that was introduced in #42633. Repro steps:
1. Open the inline assistant and mention a file in the prompt
2. Run the inline assistant
3. Remove the mention and insert a different one
4. 💥

This would happen because the mention set still had a reference to the
old editor, because we create a new one in `PromptEditor::unlink`.

Also removes the unused
`crates/agent_ui/src/context_picker/completion_provider.rs` file, which
was not removed by mistake in the previous PR.

Release Notes:

- N/A
This commit is contained in:
Bennet Bo Fenner
2025-11-23 18:26:07 +01:00
committed by GitHub
parent 1f03fc62db
commit da143c5527
5 changed files with 138 additions and 1803 deletions
+17 -12
View File
@@ -15,8 +15,9 @@ use anyhow::{Result, anyhow};
use collections::HashSet;
use editor::{
Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
EditorEvent, EditorMode, EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, ToOffset,
actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll,
EditorEvent, EditorMode, EditorStyle, Inlay, MultiBuffer, MultiBufferOffset,
MultiBufferSnapshot, ToOffset, actions::Paste, code_context_menus::CodeContextMenu,
scroll::Autoscroll,
};
use futures::{FutureExt as _, future::join_all};
use gpui::{
@@ -133,18 +134,16 @@ impl MessageEditor {
editor.register_addon(MessageEditorAddon::new());
editor
});
let mention_set = cx.new(|cx| {
let mention_set = cx.new(|_cx| {
MentionSet::new(
editor.clone(),
project.downgrade(),
history_store.clone(),
prompt_store.clone(),
window,
cx,
)
});
let completion_provider = Rc::new(PromptCompletionProvider::new(
cx.entity(),
editor.downgrade(),
mention_set.clone(),
history_store.clone(),
prompt_store.clone(),
@@ -166,14 +165,18 @@ impl MessageEditor {
let mut has_hint = false;
let mut subscriptions = Vec::new();
subscriptions.push(cx.subscribe(&editor, {
move |this, editor, event, cx| {
subscriptions.push(cx.subscribe_in(&editor, window, {
move |this, editor, event, window, cx| {
if let EditorEvent::Edited { .. } = event
&& !editor.read(cx).read_only(cx)
{
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
this.mention_set
.update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
let new_hints = this
.command_hint(editor.buffer(), cx)
.command_hint(snapshot.buffer())
.into_iter()
.collect::<Vec<_>>();
let has_new_hint = !new_hints.is_empty();
@@ -206,13 +209,12 @@ impl MessageEditor {
}
}
fn command_hint(&self, buffer: &Entity<MultiBuffer>, cx: &App) -> Option<Inlay> {
fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> {
let available_commands = self.available_commands.borrow();
if available_commands.is_empty() {
return None;
}
let snapshot = buffer.read(cx).snapshot(cx);
let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
if parsed_command.argument.is_some() {
return None;
@@ -286,6 +288,7 @@ impl MessageEditor {
content_len,
uri,
supports_images,
self.editor.clone(),
&workspace,
window,
cx,
@@ -480,7 +483,7 @@ impl MessageEditor {
editor.remove_creases(
self.mention_set.update(cx, |mention_set, _cx| {
mention_set
.remove_all()
.clear()
.map(|(crease_id, _)| crease_id)
.collect::<Vec<_>>()
}),
@@ -628,6 +631,7 @@ impl MessageEditor {
content_len,
uri,
supports_images,
self.editor.clone(),
&workspace,
window,
cx,
@@ -659,6 +663,7 @@ impl MessageEditor {
PromptCompletionProvider::<Entity<MessageEditor>>::completion_for_action(
PromptContextAction::AddSelections,
anchor..anchor,
self.editor.downgrade(),
self.mention_set.downgrade(),
&workspace,
cx,
+67 -30
View File
@@ -181,6 +181,7 @@ pub trait PromptCompletionProviderDelegate: Send + Sync + 'static {
pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> {
source: Arc<T>,
editor: WeakEntity<Editor>,
mention_set: Entity<MentionSet>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
@@ -190,6 +191,7 @@ pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> {
impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
pub fn new(
source: T,
editor: WeakEntity<Editor>,
mention_set: Entity<MentionSet>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
@@ -197,6 +199,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
) -> Self {
Self {
source: Arc::new(source),
editor,
mention_set,
workspace,
history_store,
@@ -207,6 +210,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
fn completion_for_entry(
entry: PromptContextEntry,
source_range: Range<Anchor>,
editor: WeakEntity<Editor>,
mention_set: WeakEntity<MentionSet>,
workspace: &Entity<Workspace>,
cx: &mut App,
@@ -227,9 +231,14 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
// inserted
confirm: Some(Arc::new(|_, _, _| true)),
}),
PromptContextEntry::Action(action) => {
Self::completion_for_action(action, source_range, mention_set, workspace, cx)
}
PromptContextEntry::Action(action) => Self::completion_for_action(
action,
source_range,
editor,
mention_set,
workspace,
cx,
),
}
}
@@ -238,6 +247,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
source_range: Range<Anchor>,
recent: bool,
source: Arc<T>,
editor: WeakEntity<Editor>,
mention_set: WeakEntity<MentionSet>,
workspace: Entity<Workspace>,
cx: &mut App,
@@ -269,6 +279,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
new_text_len - 1,
uri,
source,
editor,
mention_set,
workspace,
)),
@@ -279,6 +290,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
rule: RulesContextEntry,
source_range: Range<Anchor>,
source: Arc<T>,
editor: WeakEntity<Editor>,
mention_set: WeakEntity<MentionSet>,
workspace: Entity<Workspace>,
cx: &mut App,
@@ -306,6 +318,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
new_text_len - 1,
uri,
source,
editor,
mention_set,
workspace,
)),
@@ -319,6 +332,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
is_directory: bool,
source_range: Range<Anchor>,
source: Arc<T>,
editor: WeakEntity<Editor>,
mention_set: WeakEntity<MentionSet>,
workspace: Entity<Workspace>,
project: Entity<Project>,
@@ -364,6 +378,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
new_text_len - 1,
uri,
source,
editor,
mention_set,
workspace,
)),
@@ -374,6 +389,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
symbol: Symbol,
source_range: Range<Anchor>,
source: Arc<T>,
editor: WeakEntity<Editor>,
mention_set: WeakEntity<MentionSet>,
workspace: Entity<Workspace>,
cx: &mut App,
@@ -425,6 +441,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
new_text_len - 1,
uri,
source,
editor,
mention_set,
workspace,
)),
@@ -435,6 +452,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
source_range: Range<Anchor>,
url_to_fetch: SharedString,
source: Arc<T>,
editor: WeakEntity<Editor>,
mention_set: WeakEntity<MentionSet>,
workspace: Entity<Workspace>,
cx: &mut App,
@@ -463,6 +481,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
new_text.len() - 1,
mention_uri,
source,
editor,
mention_set,
workspace,
)),
@@ -472,6 +491,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
pub(crate) fn completion_for_action(
action: PromptContextAction,
source_range: Range<Anchor>,
editor: WeakEntity<Editor>,
mention_set: WeakEntity<MentionSet>,
workspace: &Entity<Workspace>,
cx: &mut App,
@@ -496,20 +516,24 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
let callback = Arc::new({
let source_range = source_range.clone();
move |_, window: &mut Window, cx: &mut App| {
let editor = editor.clone();
let selections = selections.clone();
let mention_set = mention_set.clone();
let source_range = source_range.clone();
window.defer(cx, move |window, cx| {
mention_set
.update(cx, |store, cx| {
store.confirm_mention_for_selection(
source_range,
selections,
window,
cx,
)
})
.ok();
if let Some(editor) = editor.upgrade() {
mention_set
.update(cx, |store, cx| {
store.confirm_mention_for_selection(
source_range,
selections,
editor,
window,
cx,
)
})
.ok();
}
});
false
}
@@ -853,6 +877,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
..snapshot.anchor_after(state.source_range().end);
let source = self.source.clone();
let editor = self.editor.clone();
let mention_set = self.mention_set.downgrade();
match state {
ContextCompletion::SlashCommand(SlashCommandCompletion {
@@ -955,6 +980,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
mat.is_dir,
source_range.clone(),
source.clone(),
editor.clone(),
mention_set.clone(),
workspace.clone(),
project.clone(),
@@ -967,6 +993,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
symbol,
source_range.clone(),
source.clone(),
editor.clone(),
mention_set.clone(),
workspace.clone(),
cx,
@@ -978,6 +1005,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
source_range.clone(),
false,
source.clone(),
editor.clone(),
mention_set.clone(),
workspace.clone(),
cx,
@@ -988,6 +1016,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
source_range.clone(),
true,
source.clone(),
editor.clone(),
mention_set.clone(),
workspace.clone(),
cx,
@@ -997,6 +1026,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
user_rules,
source_range.clone(),
source.clone(),
editor.clone(),
mention_set.clone(),
workspace.clone(),
cx,
@@ -1006,6 +1036,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
source_range.clone(),
url,
source.clone(),
editor.clone(),
mention_set.clone(),
workspace.clone(),
cx,
@@ -1015,6 +1046,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
Self::completion_for_entry(
entry,
source_range.clone(),
editor.clone(),
mention_set.clone(),
&workspace,
cx,
@@ -1091,33 +1123,38 @@ fn confirm_completion_callback<T: PromptCompletionProviderDelegate>(
content_len: usize,
mention_uri: MentionUri,
source: Arc<T>,
editor: WeakEntity<Editor>,
mention_set: WeakEntity<MentionSet>,
workspace: Entity<Workspace>,
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
Arc::new(move |_, window, cx| {
let source = source.clone();
let editor = editor.clone();
let mention_set = mention_set.clone();
let crease_text = crease_text.clone();
let mention_uri = mention_uri.clone();
let workspace = workspace.clone();
window.defer(cx, move |window, cx| {
mention_set
.clone()
.update(cx, |mention_set, cx| {
mention_set
.confirm_mention_completion(
crease_text,
start,
content_len,
mention_uri,
source.supports_images(cx),
&workspace,
window,
cx,
)
.detach();
})
.ok();
if let Some(editor) = editor.upgrade() {
mention_set
.clone()
.update(cx, |mention_set, cx| {
mention_set
.confirm_mention_completion(
crease_text,
start,
content_len,
mention_uri,
source.supports_images(cx),
editor,
&workspace,
window,
cx,
)
.detach();
})
.ok();
}
});
false
})
File diff suppressed because it is too large Load Diff
+36 -28
View File
@@ -1,8 +1,8 @@
use agent::HistoryStore;
use collections::VecDeque;
use collections::{HashMap, VecDeque};
use editor::actions::Paste;
use editor::code_context_menus::CodeContextMenu;
use editor::display_map::EditorMargins;
use editor::display_map::{CreaseId, EditorMargins};
use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
@@ -226,9 +226,10 @@ impl<T: 'static> PromptEditor<T> {
}
fn assign_completion_provider(&mut self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, _cx| {
self.editor.update(cx, |editor, cx| {
editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new(
PromptEditorCompletionProviderDelegate,
cx.weak_entity(),
self.mention_set.clone(),
self.history_store.clone(),
self.prompt_store.clone(),
@@ -253,18 +254,35 @@ impl<T: 'static> PromptEditor<T> {
extract_message_creases(editor, &self.mention_set, window, cx)
});
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
let mut creases = vec![];
self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text("Add a prompt…", window, cx);
editor.set_text(prompt, window, cx);
insert_message_creases(&mut editor, &existing_creases, window, cx);
creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
if focus {
window.focus(&editor.focus_handle(cx));
}
editor
});
self.mention_set.update(cx, |mention_set, _cx| {
debug_assert_eq!(
creases.len(),
mention_set.creases().len(),
"Missing creases"
);
let mentions = mention_set
.clear()
.zip(creases)
.map(|((_, value), id)| (id, value))
.collect::<HashMap<_, _>>();
mention_set.set_mentions(mentions);
});
self.assign_completion_provider(cx);
self.subscribe_to_editor(window, cx);
}
@@ -304,13 +322,18 @@ impl<T: 'static> PromptEditor<T> {
fn handle_prompt_editor_events(
&mut self,
_: &Entity<Editor>,
editor: &Entity<Editor>,
event: &EditorEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
EditorEvent::Edited { .. } => {
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
self.mention_set
.update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
@@ -321,7 +344,7 @@ impl<T: 'static> PromptEditor<T> {
.log_edit_event("inline assist", is_via_ssh);
});
}
let prompt = self.editor.read(cx).text(cx);
let prompt = snapshot.text();
if self
.prompt_history_ix
.is_none_or(|ix| self.prompt_history[ix] != prompt)
@@ -848,16 +871,8 @@ impl PromptEditor<BufferCodegen> {
editor
});
let mention_set = cx.new(|cx| {
MentionSet::new(
prompt_editor.clone(),
project,
history_store.clone(),
prompt_store.clone(),
window,
cx,
)
});
let mention_set =
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
let model_selector_menu_handle = PopoverMenuHandle::default();
@@ -999,16 +1014,8 @@ impl PromptEditor<TerminalCodegen> {
editor
});
let mention_set = cx.new(|cx| {
MentionSet::new(
prompt_editor.clone(),
project,
history_store.clone(),
prompt_store.clone(),
window,
cx,
)
});
let mention_set =
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
let model_selector_menu_handle = PopoverMenuHandle::default();
@@ -1203,7 +1210,7 @@ fn insert_message_creases(
message_creases: &[MessageCrease],
window: &mut Window,
cx: &mut Context<'_, Editor>,
) {
) -> Vec<CreaseId> {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let creases = message_creases
.iter()
@@ -1218,6 +1225,7 @@ fn insert_message_creases(
)
})
.collect::<Vec<_>>();
editor.insert_creases(creases.clone(), cx);
let ids = editor.insert_creases(creases.clone(), cx);
editor.fold_creases(creases, false, window, cx);
ids
}
+18 -32
View File
@@ -6,14 +6,14 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_slash_commands::codeblock_fence_for_path;
use collections::{HashMap, HashSet};
use editor::{
Anchor, Editor, EditorEvent, EditorSnapshot, ExcerptId, FoldPlaceholder, ToOffset,
Anchor, Editor, EditorSnapshot, ExcerptId, FoldPlaceholder, ToOffset,
display_map::{Crease, CreaseId, CreaseMetadata, FoldId},
scroll::Autoscroll,
};
use futures::{AsyncReadExt as _, FutureExt as _, future::Shared};
use gpui::{
Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Empty, Entity, EntityId,
Image, ImageFormat, Img, SharedString, Subscription, Task, WeakEntity, pulsating_between,
Image, ImageFormat, Img, SharedString, Task, WeakEntity, pulsating_between,
};
use http_client::{AsyncBody, HttpClientWithUrl};
use itertools::Either;
@@ -58,40 +58,23 @@ pub struct MentionImage {
}
pub struct MentionSet {
editor: Entity<Editor>,
project: WeakEntity<Project>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
_editor_subscription: Subscription,
}
impl MentionSet {
pub fn new(
editor: Entity<Editor>,
project: WeakEntity<Project>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let editor_subscription =
cx.subscribe_in(&editor, window, move |this, editor, event, window, cx| {
if let EditorEvent::Edited { .. } = event
&& !editor.read(cx).read_only(cx)
{
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
this.remove_invalid(snapshot);
}
});
Self {
editor,
project,
history_store,
prompt_store,
mentions: HashMap::default(),
_editor_subscription: editor_subscription,
}
}
@@ -122,9 +105,9 @@ impl MentionSet {
})
}
fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
pub fn remove_invalid(&mut self, snapshot: &EditorSnapshot) {
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
if !crease.range().start.is_valid(&snapshot.buffer_snapshot()) {
if !crease.range().start.is_valid(snapshot.buffer_snapshot()) {
self.mentions.remove(&crease_id);
}
}
@@ -146,7 +129,11 @@ impl MentionSet {
self.mentions.values().map(|(uri, _)| uri.clone()).collect()
}
pub fn remove_all(&mut self) -> impl Iterator<Item = (CreaseId, (MentionUri, MentionTask))> {
pub fn set_mentions(&mut self, mentions: HashMap<CreaseId, (MentionUri, MentionTask)>) {
self.mentions = mentions;
}
pub fn clear(&mut self) -> impl Iterator<Item = (CreaseId, (MentionUri, MentionTask))> {
self.mentions.drain()
}
@@ -157,6 +144,7 @@ impl MentionSet {
content_len: usize,
mention_uri: MentionUri,
supports_images: bool,
editor: Entity<Editor>,
workspace: &Entity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -165,9 +153,7 @@ impl MentionSet {
return Task::ready(());
};
let snapshot = self
.editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
let Some(start_anchor) = snapshot.buffer_snapshot().as_singleton_anchor(start) else {
return Task::ready(());
};
@@ -206,7 +192,7 @@ impl MentionSet {
mention_uri.name().into(),
IconName::Image.path().into(),
Some(image),
self.editor.clone(),
editor.clone(),
window,
cx,
)
@@ -218,7 +204,7 @@ impl MentionSet {
crease_text,
mention_uri.icon_path(cx),
None,
self.editor.clone(),
editor.clone(),
window,
cx,
)
@@ -265,7 +251,7 @@ impl MentionSet {
drop(tx);
if result.is_none() {
this.update(cx, |this, cx| {
this.editor.update(cx, |editor, cx| {
editor.update(cx, |editor, cx| {
// Remove mention
editor.edit([(start_anchor..end_anchor, "")], cx);
});
@@ -405,6 +391,7 @@ impl MentionSet {
&mut self,
source_range: Range<text::Anchor>,
selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
editor: Entity<Editor>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -412,7 +399,7 @@ impl MentionSet {
return;
};
let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
let Some(start) = snapshot.as_singleton_anchor(source_range.start) else {
return;
};
@@ -443,10 +430,10 @@ impl MentionSet {
selection_name(abs_path.as_deref(), &line_range).into(),
uri.icon_path(cx),
range,
self.editor.downgrade(),
editor.downgrade(),
);
let crease_id = self.editor.update(cx, |editor, cx| {
let crease_id = editor.update(cx, |editor, cx| {
let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
editor.fold_creases(vec![crease], false, window, cx);
crease_ids.first().copied().unwrap()
@@ -471,7 +458,6 @@ impl MentionSet {
// expected. We're leveraging `cx.on_next_frame` to wait 2 frames and
// ensure that the layout has been recalculated so that the autoscroll
// request actually shows the cursor's new position.
let editor = self.editor.clone();
cx.on_next_frame(window, move |_, window, cx| {
cx.on_next_frame(window, move |_, _, cx| {
editor.update(cx, |editor, cx| {