@@ -16,6 +16,7 @@ use editor::{
MultiBuffer, ToOffset,
actions::Paste,
display_map::{Crease, CreaseId, FoldId},
+ scroll::Autoscroll,
};
use futures::{
FutureExt as _,
@@ -591,6 +592,21 @@ impl MessageEditor {
),
);
}
+
+ // Take this explanation with a grain of salt but, with creases being
+ // inserted, GPUI's recomputes the editor layout in the next frames, so
+ // directly calling `editor.request_autoscroll` wouldn't work as
+ // 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| {
+ editor.request_autoscroll(Autoscroll::fit(), cx)
+ });
+ });
+ });
}
fn confirm_mention_for_thread(
@@ -1030,6 +1046,7 @@ impl MessageEditor {
self.editor.update(cx, |message_editor, cx| {
message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
+ message_editor.request_autoscroll(Autoscroll::fit(), cx);
});
if let Some(confirm) = completion.confirm {
confirm(CompletionIntent::Complete, window, cx);
@@ -2745,6 +2762,7 @@ mod tests {
_ => panic!("Expected Text mention for small file"),
}
}
+
#[gpui::test]
async fn test_insert_thread_summary(cx: &mut TestAppContext) {
init_test(cx);
@@ -2878,4 +2896,161 @@ mod tests {
})]
);
}
+
+ #[gpui::test]
+ async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let app_state = cx.update(AppState::test);
+
+ cx.update(|cx| {
+ language::init(cx);
+ editor::init(cx);
+ workspace::init(app_state.clone(), cx);
+ Project::init_settings(cx);
+ });
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ path!("/dir"),
+ json!({
+ "test.txt": "line1\nline2\nline3\nline4\nline5\n",
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
+ let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let workspace = window.root(cx).unwrap();
+
+ let worktree = project.update(cx, |project, cx| {
+ let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ worktrees.pop().unwrap()
+ });
+ let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
+
+ let mut cx = VisualTestContext::from_window(*window, cx);
+
+ // Open a regular editor with the created file, and select a portion of
+ // the text that will be used for the selections that are meant to be
+ // inserted in the agent panel.
+ let editor = workspace
+ .update_in(&mut cx, |workspace, window, cx| {
+ workspace.open_path(
+ ProjectPath {
+ worktree_id,
+ path: rel_path("test.txt").into(),
+ },
+ None,
+ false,
+ window,
+ cx,
+ )
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
+ selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]);
+ });
+ });
+
+ let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
+ let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
+
+ // Create a new `MessageEditor`. The `EditorMode::full()` has to be used
+ // to ensure we have a fixed viewport, so we can eventually actually
+ // place the cursor outside of the visible area.
+ let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
+ let workspace_handle = cx.weak_entity();
+ let message_editor = cx.new(|cx| {
+ MessageEditor::new(
+ workspace_handle,
+ project.clone(),
+ history_store.clone(),
+ None,
+ Default::default(),
+ Default::default(),
+ "Test Agent".into(),
+ "Test",
+ EditorMode::full(),
+ window,
+ cx,
+ )
+ });
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.add_item(
+ Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
+ true,
+ true,
+ None,
+ window,
+ cx,
+ );
+ });
+
+ message_editor
+ });
+
+ message_editor.update_in(&mut cx, |message_editor, window, cx| {
+ message_editor.editor.update(cx, |editor, cx| {
+ // Update the Agent Panel's Message Editor text to have 100
+ // lines, ensuring that the cursor is set at line 90 and that we
+ // then scroll all the way to the top, so the cursor's position
+ // remains off screen.
+ let mut lines = String::new();
+ for _ in 1..=100 {
+ lines.push_str(&"Another line in the agent panel's message editor\n");
+ }
+ editor.set_text(lines.as_str(), window, cx);
+ editor.change_selections(Default::default(), window, cx, |selections| {
+ selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]);
+ });
+ editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx);
+ });
+ });
+
+ cx.run_until_parked();
+
+ // Before proceeding, let's assert that the cursor is indeed off screen,
+ // otherwise the rest of the test doesn't make sense.
+ message_editor.update_in(&mut cx, |message_editor, window, cx| {
+ message_editor.editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(window, cx);
+ let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
+ let scroll_top = snapshot.scroll_position().y as u32;
+ let visible_lines = editor.visible_line_count().unwrap() as u32;
+ let visible_range = scroll_top..(scroll_top + visible_lines);
+
+ assert!(!visible_range.contains(&cursor_row));
+ })
+ });
+
+ // Now let's insert the selection in the Agent Panel's editor and
+ // confirm that, after the insertion, the cursor is now in the visible
+ // range.
+ message_editor.update_in(&mut cx, |message_editor, window, cx| {
+ message_editor.insert_selections(window, cx);
+ });
+
+ cx.run_until_parked();
+
+ message_editor.update_in(&mut cx, |message_editor, window, cx| {
+ message_editor.editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(window, cx);
+ let cursor_row = editor.selections.newest::<Point>(&snapshot).head().row;
+ let scroll_top = snapshot.scroll_position().y as u32;
+ let visible_lines = editor.visible_line_count().unwrap() as u32;
+ let visible_range = scroll_top..(scroll_top + visible_lines);
+
+ assert!(visible_range.contains(&cursor_row));
+ })
+ });
+ }
}