From 47297bc3eed596b8b741911795ac980bac831049 Mon Sep 17 00:00:00 2001 From: saberoueslati Date: Wed, 29 Apr 2026 01:22:54 +0100 Subject: [PATCH] agent_ui: Insert dropped file links at the cursor --- crates/agent_ui/src/message_editor.rs | 166 +++++++++++++++++++------- 1 file changed, 126 insertions(+), 40 deletions(-) diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 401c282201d84f..11e9abb50642fa 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -162,14 +162,8 @@ impl EventEmitter for MessageEditor {} const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0); -enum MentionInsertPosition { - AtCursor, - EndOfBuffer, -} - fn insert_mention_for_project_path( project_path: &ProjectPath, - position: MentionInsertPosition, editor: &Entity, mention_set: &Entity, project: &Entity, @@ -200,38 +194,20 @@ fn insert_mention_for_project_path( let mention_text = mention_uri.as_link().to_string(); let content_len = mention_text.len(); - let text_anchor = match position { - MentionInsertPosition::AtCursor => editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); - let buffer_snapshot = snapshot.as_singleton()?; - let text_anchor = snapshot - .anchor_to_buffer_anchor(editor.selections.newest_anchor().start)? - .0 - .bias_left(&buffer_snapshot); - - editor.insert(&mention_text, window, cx); - editor.insert(" ", window, cx); - - Some(text_anchor) - }), - MentionInsertPosition::EndOfBuffer => { - let multi_buffer = editor.read(cx).buffer().clone(); - let buffer = multi_buffer.read(cx).as_singleton()?; - let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); - let new_text = format!("{mention_text} "); - editor.update(cx, |editor, cx| { - editor.edit( - [( - multi_buffer::Anchor::Max..multi_buffer::Anchor::Max, - new_text, - )], - cx, - ); - }); - Some(anchor) - } - }?; + let text_anchor = editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let buffer_snapshot = snapshot.as_singleton()?; + let text_anchor = snapshot + .anchor_to_buffer_anchor(editor.selections.newest_anchor().start)? + .0 + .bias_left(&buffer_snapshot); + + editor.insert(&mention_text, window, cx); + editor.insert(" ", window, cx); + + Some(text_anchor) + })?; Some(mention_set.update(cx, |mention_set, cx| { mention_set.confirm_mention_completion( @@ -334,7 +310,6 @@ fn insert_project_path_as_context( let project = workspace.read(cx).project().clone(); insert_mention_for_project_path( &project_path, - MentionInsertPosition::AtCursor, &editor, &mention_set, &project, @@ -1276,7 +1251,6 @@ impl MessageEditor { for path in paths { if let Some(task) = insert_mention_for_project_path( &path, - MentionInsertPosition::EndOfBuffer, &self.editor, &self.mention_set, &project, @@ -4352,6 +4326,88 @@ mod tests { }); } + #[gpui::test] + async fn test_dragged_file_path_inserts_at_cursor(cx: &mut TestAppContext) { + init_test(cx); + let (message_editor, editor, mut cx) = + setup_paste_test_message_editor(json!({"file.txt": "content"}), cx).await; + + editor.update_in(&mut cx, |editor, window, cx| { + editor.set_text("Hello world", window, cx); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.select_ranges([MultiBufferOffset(6)..MultiBufferOffset(6)]); + }); + }); + + insert_dragged_project_paths(&message_editor, vec!["file.txt"], &mut cx); + + let expected_uri = MentionUri::File { + abs_path: path!("/project/file.txt").into(), + } + .to_uri() + .to_string(); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Hello [@file.txt]({expected_uri}) world") + ); + }); + + let contents = mention_contents(&message_editor, &mut cx).await; + + let [(uri, Mention::Text { content, .. })] = contents.as_slice() else { + panic!("Unexpected mentions"); + }; + assert_eq!(content, "content"); + assert_eq!( + uri, + &MentionUri::File { + abs_path: path!("/project/file.txt").into(), + } + ); + } + + #[gpui::test] + async fn test_dragged_file_paths_insert_in_order_at_cursor(cx: &mut TestAppContext) { + init_test(cx); + let (message_editor, editor, mut cx) = setup_paste_test_message_editor( + json!({ + "one.txt": "one", + "two.txt": "two", + }), + cx, + ) + .await; + + editor.update_in(&mut cx, |editor, window, cx| { + editor.set_text("Hello world", window, cx); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.select_ranges([MultiBufferOffset(6)..MultiBufferOffset(6)]); + }); + }); + + insert_dragged_project_paths(&message_editor, vec!["one.txt", "two.txt"], &mut cx); + + let first_uri = MentionUri::File { + abs_path: path!("/project/one.txt").into(), + } + .to_uri() + .to_string(); + let second_uri = MentionUri::File { + abs_path: path!("/project/two.txt").into(), + } + .to_uri() + .to_string(); + + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.text(cx), + format!("Hello [@one.txt]({first_uri}) [@two.txt]({second_uri}) world") + ); + }); + } + #[gpui::test] async fn test_paste_mixed_external_image_without_extension_and_file_path( cx: &mut TestAppContext, @@ -4501,6 +4557,36 @@ mod tests { cx.run_until_parked(); } + fn insert_dragged_project_paths( + message_editor: &Entity, + paths: Vec<&str>, + cx: &mut VisualTestContext, + ) { + message_editor.update_in(cx, |message_editor, window, cx| { + let workspace = message_editor + .workspace + .upgrade() + .expect("message editor should keep workspace alive"); + let project = workspace.read(cx).project().clone(); + let worktree_id = project.update(cx, |project, cx| { + let mut worktrees = project.worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1, "expected a single worktree"); + worktrees.pop().unwrap().read(cx).id() + }); + + let paths = paths + .into_iter() + .map(|path| ProjectPath { + worktree_id, + path: rel_path(path).into(), + }) + .collect(); + + message_editor.insert_dragged_files(paths, vec![], window, cx); + }); + cx.run_until_parked(); + } + async fn mention_contents( message_editor: &Entity, cx: &mut VisualTestContext,