@@ -2,34 +2,36 @@ use crate::{
prompts::generate_content_prompt, AssistantPanel, CompletionProvider, Hunk,
LanguageModelRequest, LanguageModelRequestMessage, Role, StreamingDiff,
};
-use anyhow::Result;
+use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{
actions::{MoveDown, MoveUp, SelectAll},
display_map::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
+ ToDisplayPoint,
},
- scroll::{Autoscroll, AutoscrollStrategy},
- Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorStyle, ExcerptRange,
- GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+ Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle,
+ ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
};
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{
- AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global,
+ point, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global,
HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View,
ViewContext, WeakView, WhiteSpace, WindowContext,
};
-use language::{Buffer, Point, TransactionId};
+use language::{Buffer, Point, Selection, TransactionId};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use rope::Rope;
use settings::Settings;
use similar::TextDiff;
use std::{
- cmp, future, mem,
+ cmp, mem,
ops::{Range, RangeInclusive},
+ pin::Pin,
sync::Arc,
+ task::{self, Poll},
time::Instant,
};
use theme::ThemeSettings;
@@ -45,8 +47,10 @@ const PROMPT_HISTORY_MAX_LEN: usize = 20;
pub struct InlineAssistant {
next_assist_id: InlineAssistId,
- pending_assists: HashMap<InlineAssistId, PendingInlineAssist>,
- pending_assist_ids_by_editor: HashMap<WeakView<Editor>, Vec<InlineAssistId>>,
+ next_assist_group_id: InlineAssistGroupId,
+ assists: HashMap<InlineAssistId, InlineAssist>,
+ assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
+ assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
prompt_history: VecDeque<String>,
telemetry: Option<Arc<Telemetry>>,
}
@@ -57,8 +61,10 @@ impl InlineAssistant {
pub fn new(telemetry: Arc<Telemetry>) -> Self {
Self {
next_assist_id: InlineAssistId::default(),
- pending_assists: HashMap::default(),
- pending_assist_ids_by_editor: HashMap::default(),
+ next_assist_group_id: InlineAssistGroupId::default(),
+ assists: HashMap::default(),
+ assists_by_editor: HashMap::default(),
+ assist_groups: HashMap::default(),
prompt_history: VecDeque::default(),
telemetry: Some(telemetry),
}
@@ -71,380 +77,452 @@ impl InlineAssistant {
include_context: bool,
cx: &mut WindowContext,
) {
- let selection = editor.read(cx).selections.newest_anchor().clone();
- if selection.start.excerpt_id != selection.end.excerpt_id {
- return;
- }
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
- // Extend the selection to the start and the end of the line.
- let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
- if point_selection.end > point_selection.start {
- point_selection.start.column = 0;
- // If the selection ends at the start of the line, we don't want to include it.
- if point_selection.end.column == 0 {
- point_selection.end.row -= 1;
+ let mut selections = Vec::<Selection<Point>>::new();
+ let mut newest_selection = None;
+ for mut selection in editor.read(cx).selections.all::<Point>(cx) {
+ if selection.end > selection.start {
+ selection.start.column = 0;
+ // If the selection ends at the start of the line, we don't want to include it.
+ if selection.end.column == 0 {
+ selection.end.row -= 1;
+ }
+ selection.end.column = snapshot.line_len(MultiBufferRow(selection.end.row));
}
- point_selection.end.column = snapshot.line_len(MultiBufferRow(point_selection.end.row));
- }
- let codegen_kind = if point_selection.start == point_selection.end {
- CodegenKind::Generate {
- position: snapshot.anchor_after(point_selection.start),
+ if let Some(prev_selection) = selections.last_mut() {
+ if selection.start <= prev_selection.end {
+ prev_selection.end = selection.end;
+ continue;
+ }
}
- } else {
- CodegenKind::Transform {
- range: snapshot.anchor_before(point_selection.start)
- ..snapshot.anchor_after(point_selection.end),
+
+ let latest_selection = newest_selection.get_or_insert_with(|| selection.clone());
+ if selection.id > latest_selection.id {
+ *latest_selection = selection.clone();
}
- };
+ selections.push(selection);
+ }
+ let newest_selection = newest_selection.unwrap();
- let assist_id = self.next_assist_id.post_inc();
- let codegen = cx.new_model(|cx| {
- Codegen::new(
- editor.read(cx).buffer().clone(),
- codegen_kind,
- self.telemetry.clone(),
- cx,
- )
+ let mut codegen_ranges = Vec::new();
+ for (excerpt_id, buffer, buffer_range) in
+ snapshot.excerpts_in_ranges(selections.iter().map(|selection| {
+ snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
+ }))
+ {
+ let start = Anchor {
+ buffer_id: Some(buffer.remote_id()),
+ excerpt_id,
+ text_anchor: buffer.anchor_before(buffer_range.start),
+ };
+ let end = Anchor {
+ buffer_id: Some(buffer.remote_id()),
+ excerpt_id,
+ text_anchor: buffer.anchor_after(buffer_range.end),
+ };
+ codegen_ranges.push(start..end);
+ }
+
+ let assist_group_id = self.next_assist_group_id.post_inc();
+ let prompt_buffer = cx.new_model(|cx| Buffer::local("", cx));
+ let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
+
+ let mut assists = Vec::new();
+ let mut assist_blocks = Vec::new();
+ let mut assist_to_focus = None;
+ for range in codegen_ranges {
+ let assist_id = self.next_assist_id.post_inc();
+ let codegen = cx.new_model(|cx| {
+ Codegen::new(
+ editor.read(cx).buffer().clone(),
+ range.clone(),
+ self.telemetry.clone(),
+ cx,
+ )
+ });
+
+ let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
+ let prompt_editor = cx.new_view(|cx| {
+ PromptEditor::new(
+ assist_id,
+ gutter_dimensions.clone(),
+ self.prompt_history.clone(),
+ prompt_buffer.clone(),
+ codegen.clone(),
+ workspace.clone(),
+ cx,
+ )
+ });
+
+ if assist_to_focus.is_none() {
+ let focus_assist = if newest_selection.reversed {
+ range.start.to_point(&snapshot) == newest_selection.start
+ } else {
+ range.end.to_point(&snapshot) == newest_selection.end
+ };
+ if focus_assist {
+ assist_to_focus = Some(assist_id);
+ }
+ }
+
+ assist_blocks.push(BlockProperties {
+ style: BlockStyle::Sticky,
+ position: range.start,
+ height: prompt_editor.read(cx).height_in_lines,
+ render: build_assist_editor_renderer(&prompt_editor),
+ disposition: BlockDisposition::Above,
+ });
+ assist_blocks.push(BlockProperties {
+ style: BlockStyle::Sticky,
+ position: range.end,
+ height: 1,
+ render: Box::new(|cx| {
+ v_flex()
+ .h_full()
+ .w_full()
+ .border_t_1()
+ .border_color(cx.theme().status().info_border)
+ .into_any_element()
+ }),
+ disposition: BlockDisposition::Below,
+ });
+ assists.push((assist_id, prompt_editor));
+ }
+
+ let assist_block_ids = editor.update(cx, |editor, cx| {
+ editor.insert_blocks(assist_blocks, None, cx)
});
- let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
- let prompt_editor = cx.new_view(|cx| {
- InlineAssistEditor::new(
+ let editor_assists = self
+ .assists_by_editor
+ .entry(editor.downgrade())
+ .or_insert_with(|| EditorInlineAssists::new(&editor, cx));
+ let mut assist_group = InlineAssistGroup::new();
+ for ((assist_id, prompt_editor), block_ids) in
+ assists.into_iter().zip(assist_block_ids.chunks_exact(2))
+ {
+ self.assists.insert(
assist_id,
- gutter_dimensions.clone(),
- self.prompt_history.clone(),
- codegen.clone(),
- workspace.clone(),
- cx,
- )
- });
- let (prompt_block_id, end_block_id) = editor.update(cx, |editor, cx| {
- let start_anchor = snapshot.anchor_before(point_selection.start);
- let end_anchor = snapshot.anchor_after(point_selection.end);
- editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
- selections.select_anchor_ranges([start_anchor..start_anchor])
- });
- let block_ids = editor.insert_blocks(
- [
- BlockProperties {
- style: BlockStyle::Sticky,
- position: start_anchor,
- height: prompt_editor.read(cx).height_in_lines,
- render: build_inline_assist_editor_renderer(
- &prompt_editor,
- gutter_dimensions,
- ),
- disposition: BlockDisposition::Above,
- },
- BlockProperties {
- style: BlockStyle::Sticky,
- position: end_anchor,
- height: 1,
- render: Box::new(|cx| {
- v_flex()
- .h_full()
- .w_full()
- .border_t_1()
- .border_color(cx.theme().status().info_border)
- .into_any_element()
- }),
- disposition: BlockDisposition::Below,
- },
- ],
- Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)),
- cx,
+ InlineAssist::new(
+ assist_id,
+ assist_group_id,
+ include_context,
+ editor,
+ &prompt_editor,
+ block_ids[0],
+ block_ids[1],
+ prompt_editor.read(cx).codegen.clone(),
+ workspace.clone(),
+ cx,
+ ),
);
- (block_ids[0], block_ids[1])
- });
+ assist_group.assist_ids.push(assist_id);
+ editor_assists.assist_ids.push(assist_id);
+ }
+ self.assist_groups.insert(assist_group_id, assist_group);
- self.pending_assists.insert(
- assist_id,
- PendingInlineAssist {
- include_context,
- editor: editor.downgrade(),
- editor_decorations: Some(PendingInlineAssistDecorations {
- prompt_block_id,
- prompt_editor: prompt_editor.clone(),
- removed_line_block_ids: HashSet::default(),
- end_block_id,
- }),
- codegen: codegen.clone(),
- workspace,
- _subscriptions: vec![
- cx.subscribe(&prompt_editor, |inline_assist_editor, event, cx| {
- InlineAssistant::update_global(cx, |this, cx| {
- this.handle_inline_assistant_editor_event(
- inline_assist_editor,
- event,
- cx,
- )
- })
- }),
- editor.update(cx, |editor, _cx| {
- editor.register_action(
- move |_: &editor::actions::Newline, cx: &mut WindowContext| {
- InlineAssistant::update_global(cx, |this, cx| {
- this.handle_editor_newline(assist_id, cx)
- })
- },
- )
- }),
- editor.update(cx, |editor, _cx| {
- editor.register_action(
- move |_: &editor::actions::Cancel, cx: &mut WindowContext| {
- InlineAssistant::update_global(cx, |this, cx| {
- this.handle_editor_cancel(assist_id, cx)
- })
- },
- )
- }),
- cx.subscribe(editor, move |editor, event, cx| {
- InlineAssistant::update_global(cx, |this, cx| {
- this.handle_editor_event(assist_id, editor, event, cx)
- })
- }),
- cx.observe(&codegen, {
- let editor = editor.downgrade();
- move |_, cx| {
- if let Some(editor) = editor.upgrade() {
- InlineAssistant::update_global(cx, |this, cx| {
- this.update_editor_highlights(&editor, cx);
- this.update_editor_blocks(&editor, assist_id, cx);
- })
- }
- }
- }),
- cx.subscribe(&codegen, move |codegen, event, cx| {
- InlineAssistant::update_global(cx, |this, cx| match event {
- CodegenEvent::Undone => this.finish_inline_assist(assist_id, false, cx),
- CodegenEvent::Finished => {
- let pending_assist = if let Some(pending_assist) =
- this.pending_assists.get(&assist_id)
- {
- pending_assist
- } else {
- return;
- };
-
- if let CodegenStatus::Error(error) = &codegen.read(cx).status {
- if pending_assist.editor_decorations.is_none() {
- if let Some(workspace) = pending_assist
- .workspace
- .as_ref()
- .and_then(|workspace| workspace.upgrade())
- {
- let error =
- format!("Inline assistant error: {}", error);
- workspace.update(cx, |workspace, cx| {
- struct InlineAssistantError;
-
- let id = NotificationId::identified::<
- InlineAssistantError,
- >(
- assist_id.0
- );
+ if let Some(assist_id) = assist_to_focus {
+ self.focus_assist(assist_id, cx);
+ }
+ }
- workspace.show_toast(Toast::new(id, error), cx);
- })
- }
- }
- }
+ fn handle_prompt_editor_focus_in(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
+ let assist = &self.assists[&assist_id];
+ let Some(decorations) = assist.decorations.as_ref() else {
+ return;
+ };
+ let assist_group = self.assist_groups.get_mut(&assist.group_id).unwrap();
+ let editor_assists = self.assists_by_editor.get_mut(&assist.editor).unwrap();
+
+ assist_group.active_assist_id = Some(assist_id);
+ if assist_group.linked {
+ for assist_id in &assist_group.assist_ids {
+ if let Some(decorations) = self.assists[assist_id].decorations.as_ref() {
+ decorations.prompt_editor.update(cx, |prompt_editor, cx| {
+ prompt_editor.set_show_cursor_when_unfocused(true, cx)
+ });
+ }
+ }
+ }
- if pending_assist.editor_decorations.is_none() {
- this.finish_inline_assist(assist_id, false, cx);
- }
- }
- })
- }),
- ],
- },
- );
+ assist
+ .editor
+ .update(cx, |editor, cx| {
+ let scroll_top = editor.scroll_position(cx).y;
+ let scroll_bottom = scroll_top + editor.visible_line_count().unwrap_or(0.);
+ let prompt_row = editor
+ .row_for_block(decorations.prompt_block_id, cx)
+ .unwrap()
+ .0 as f32;
+
+ if (scroll_top..scroll_bottom).contains(&prompt_row) {
+ editor_assists.scroll_lock = Some(InlineAssistScrollLock {
+ assist_id,
+ distance_from_top: prompt_row - scroll_top,
+ });
+ } else {
+ editor_assists.scroll_lock = None;
+ }
+ })
+ .ok();
+ }
- self.pending_assist_ids_by_editor
- .entry(editor.downgrade())
- .or_default()
- .push(assist_id);
- self.update_editor_highlights(editor, cx);
+ fn handle_prompt_editor_focus_out(
+ &mut self,
+ assist_id: InlineAssistId,
+ cx: &mut WindowContext,
+ ) {
+ let assist = &self.assists[&assist_id];
+ let assist_group = self.assist_groups.get_mut(&assist.group_id).unwrap();
+ if assist_group.active_assist_id == Some(assist_id) {
+ assist_group.active_assist_id = None;
+ if assist_group.linked {
+ for assist_id in &assist_group.assist_ids {
+ if let Some(decorations) = self.assists[assist_id].decorations.as_ref() {
+ decorations.prompt_editor.update(cx, |prompt_editor, cx| {
+ prompt_editor.set_show_cursor_when_unfocused(false, cx)
+ });
+ }
+ }
+ }
+ }
}
- fn handle_inline_assistant_editor_event(
+ fn handle_prompt_editor_event(
&mut self,
- inline_assist_editor: View<InlineAssistEditor>,
- event: &InlineAssistEditorEvent,
+ prompt_editor: View<PromptEditor>,
+ event: &PromptEditorEvent,
cx: &mut WindowContext,
) {
- let assist_id = inline_assist_editor.read(cx).id;
+ let assist_id = prompt_editor.read(cx).id;
match event {
- InlineAssistEditorEvent::StartRequested => {
- self.start_inline_assist(assist_id, cx);
+ PromptEditorEvent::StartRequested => {
+ self.start_assist(assist_id, cx);
}
- InlineAssistEditorEvent::StopRequested => {
- self.stop_inline_assist(assist_id, cx);
+ PromptEditorEvent::StopRequested => {
+ self.stop_assist(assist_id, cx);
}
- InlineAssistEditorEvent::ConfirmRequested => {
- self.finish_inline_assist(assist_id, false, cx);
+ PromptEditorEvent::ConfirmRequested => {
+ self.finish_assist(assist_id, false, cx);
}
- InlineAssistEditorEvent::CancelRequested => {
- self.finish_inline_assist(assist_id, true, cx);
+ PromptEditorEvent::CancelRequested => {
+ self.finish_assist(assist_id, true, cx);
}
- InlineAssistEditorEvent::DismissRequested => {
- self.dismiss_inline_assist(assist_id, cx);
+ PromptEditorEvent::DismissRequested => {
+ self.dismiss_assist(assist_id, cx);
}
- InlineAssistEditorEvent::Resized { height_in_lines } => {
- self.resize_inline_assist(assist_id, *height_in_lines, cx);
+ PromptEditorEvent::Resized { height_in_lines } => {
+ self.resize_assist(assist_id, *height_in_lines, cx);
}
}
}
- fn handle_editor_newline(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
- let Some(assist) = self.pending_assists.get(&assist_id) else {
+ fn handle_editor_newline(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
+ let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
return;
};
- let Some(editor) = assist.editor.upgrade() else {
+
+ let editor = editor.read(cx);
+ if editor.selections.count() == 1 {
+ let selection = editor.selections.newest::<usize>(cx);
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ for assist_id in &editor_assists.assist_ids {
+ let assist = &self.assists[assist_id];
+ let assist_range = assist.codegen.read(cx).range.to_offset(&buffer);
+ if assist_range.contains(&selection.start) && assist_range.contains(&selection.end)
+ {
+ if matches!(assist.codegen.read(cx).status, CodegenStatus::Pending) {
+ self.dismiss_assist(*assist_id, cx);
+ } else {
+ self.finish_assist(*assist_id, false, cx);
+ }
+
+ return;
+ }
+ }
+ }
+
+ cx.propagate();
+ }
+
+ fn handle_editor_cancel(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
+ let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
return;
};
- let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
- let assist_range = assist.codegen.read(cx).range().to_offset(&buffer);
let editor = editor.read(cx);
if editor.selections.count() == 1 {
let selection = editor.selections.newest::<usize>(cx);
- if assist_range.contains(&selection.start) && assist_range.contains(&selection.end) {
- if matches!(assist.codegen.read(cx).status, CodegenStatus::Pending) {
- self.dismiss_inline_assist(assist_id, cx);
- } else {
- self.finish_inline_assist(assist_id, false, cx);
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ for assist_id in &editor_assists.assist_ids {
+ let assist = &self.assists[assist_id];
+ let assist_range = assist.codegen.read(cx).range.to_offset(&buffer);
+ if assist.decorations.is_some()
+ && assist_range.contains(&selection.start)
+ && assist_range.contains(&selection.end)
+ {
+ self.focus_assist(*assist_id, cx);
+ return;
}
-
- return;
}
}
cx.propagate();
}
- fn handle_editor_cancel(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
- let Some(assist) = self.pending_assists.get(&assist_id) else {
+ fn handle_editor_change(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
+ let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
return;
};
- let Some(editor) = assist.editor.upgrade() else {
+ let Some(scroll_lock) = editor_assists.scroll_lock.as_ref() else {
+ return;
+ };
+ let assist = &self.assists[&scroll_lock.assist_id];
+ let Some(decorations) = assist.decorations.as_ref() else {
return;
};
- let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
- let assist_range = assist.codegen.read(cx).range().to_offset(&buffer);
- let propagate = editor.update(cx, |editor, cx| {
- if let Some(decorations) = assist.editor_decorations.as_ref() {
- if editor.selections.count() == 1 {
- let selection = editor.selections.newest::<usize>(cx);
- if assist_range.contains(&selection.start)
- && assist_range.contains(&selection.end)
- {
- editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
- selections.select_ranges([assist_range.start..assist_range.start]);
- });
- decorations.prompt_editor.update(cx, |prompt_editor, cx| {
- prompt_editor.editor.update(cx, |prompt_editor, cx| {
- prompt_editor.select_all(&SelectAll, cx);
- prompt_editor.focus(cx);
- });
- });
- return false;
- }
- }
+ editor.update(cx, |editor, cx| {
+ let scroll_position = editor.scroll_position(cx);
+ let target_scroll_top = editor
+ .row_for_block(decorations.prompt_block_id, cx)
+ .unwrap()
+ .0 as f32
+ - scroll_lock.distance_from_top;
+ if target_scroll_top != scroll_position.y {
+ editor.set_scroll_position(point(scroll_position.x, target_scroll_top), cx);
}
- true
});
-
- if propagate {
- cx.propagate();
- }
}
fn handle_editor_event(
&mut self,
- assist_id: InlineAssistId,
editor: View<Editor>,
event: &EditorEvent,
cx: &mut WindowContext,
) {
- let Some(assist) = self.pending_assists.get(&assist_id) else {
+ let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) else {
return;
};
match event {
- EditorEvent::SelectionsChanged { local } if *local => {
- if let CodegenStatus::Idle = &assist.codegen.read(cx).status {
- self.finish_inline_assist(assist_id, true, cx);
- }
- }
EditorEvent::Saved => {
- if let CodegenStatus::Done = &assist.codegen.read(cx).status {
- self.finish_inline_assist(assist_id, false, cx)
+ for assist_id in editor_assists.assist_ids.clone() {
+ let assist = &self.assists[&assist_id];
+ if let CodegenStatus::Done = &assist.codegen.read(cx).status {
+ self.finish_assist(assist_id, false, cx)
+ }
}
}
- EditorEvent::Edited { transaction_id }
- if matches!(
- assist.codegen.read(cx).status,
- CodegenStatus::Error(_) | CodegenStatus::Done
- ) =>
- {
+ EditorEvent::Edited { transaction_id } => {
let buffer = editor.read(cx).buffer().read(cx);
let edited_ranges =
buffer.edited_ranges_for_transaction::<usize>(*transaction_id, cx);
- let assist_range = assist.codegen.read(cx).range().to_offset(&buffer.read(cx));
- if edited_ranges
- .iter()
- .any(|range| range.overlaps(&assist_range))
- {
- self.finish_inline_assist(assist_id, false, cx);
+ let snapshot = buffer.snapshot(cx);
+
+ for assist_id in editor_assists.assist_ids.clone() {
+ let assist = &self.assists[&assist_id];
+ if matches!(
+ assist.codegen.read(cx).status,
+ CodegenStatus::Error(_) | CodegenStatus::Done
+ ) {
+ let assist_range = assist.codegen.read(cx).range.to_offset(&snapshot);
+ if edited_ranges
+ .iter()
+ .any(|range| range.overlaps(&assist_range))
+ {
+ self.finish_assist(assist_id, false, cx);
+ }
+ }
+ }
+ }
+ EditorEvent::ScrollPositionChanged { .. } => {
+ if let Some(scroll_lock) = editor_assists.scroll_lock.as_ref() {
+ let assist = &self.assists[&scroll_lock.assist_id];
+ if let Some(decorations) = assist.decorations.as_ref() {
+ let distance_from_top = editor.update(cx, |editor, cx| {
+ let scroll_top = editor.scroll_position(cx).y;
+ let prompt_row = editor
+ .row_for_block(decorations.prompt_block_id, cx)
+ .unwrap()
+ .0 as f32;
+ prompt_row - scroll_top
+ });
+
+ if distance_from_top != scroll_lock.distance_from_top {
+ editor_assists.scroll_lock = None;
+ }
+ }
}
}
+ EditorEvent::SelectionsChanged { .. } => {
+ for assist_id in editor_assists.assist_ids.clone() {
+ let assist = &self.assists[&assist_id];
+ if let Some(decorations) = assist.decorations.as_ref() {
+ if decorations.prompt_editor.focus_handle(cx).is_focused(cx) {
+ return;
+ }
+ }
+ }
+
+ editor_assists.scroll_lock = None;
+ }
_ => {}
}
}
- fn finish_inline_assist(
- &mut self,
- assist_id: InlineAssistId,
- undo: bool,
- cx: &mut WindowContext,
- ) {
- self.dismiss_inline_assist(assist_id, cx);
+ fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) {
+ if let Some(assist) = self.assists.get(&assist_id) {
+ let assist_group_id = assist.group_id;
+ if self.assist_groups[&assist_group_id].linked {
+ for assist_id in self.unlink_assist_group(assist_group_id, cx) {
+ self.finish_assist(assist_id, undo, cx);
+ }
+ return;
+ }
+ }
+
+ self.dismiss_assist(assist_id, cx);
- if let Some(pending_assist) = self.pending_assists.remove(&assist_id) {
- if let hash_map::Entry::Occupied(mut entry) = self
- .pending_assist_ids_by_editor
- .entry(pending_assist.editor.clone())
+ if let Some(assist) = self.assists.remove(&assist_id) {
+ if let hash_map::Entry::Occupied(mut entry) = self.assist_groups.entry(assist.group_id)
{
- entry.get_mut().retain(|id| *id != assist_id);
- if entry.get().is_empty() {
+ entry.get_mut().assist_ids.retain(|id| *id != assist_id);
+ if entry.get().assist_ids.is_empty() {
entry.remove();
}
}
- if let Some(editor) = pending_assist.editor.upgrade() {
- self.update_editor_highlights(&editor, cx);
-
- if undo {
- pending_assist
- .codegen
- .update(cx, |codegen, cx| codegen.undo(cx));
+ if let hash_map::Entry::Occupied(mut entry) =
+ self.assists_by_editor.entry(assist.editor.clone())
+ {
+ entry.get_mut().assist_ids.retain(|id| *id != assist_id);
+ if entry.get().assist_ids.is_empty() {
+ entry.remove();
+ if let Some(editor) = assist.editor.upgrade() {
+ self.update_editor_highlights(&editor, cx);
+ }
+ } else {
+ entry.get().highlight_updates.send(()).ok();
}
}
+
+ if undo {
+ assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
+ }
}
}
- fn dismiss_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
- let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) else {
+ fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
+ let Some(assist) = self.assists.get_mut(&assist_id) else {
return false;
};
- let Some(editor) = pending_assist.editor.upgrade() else {
+ let Some(editor) = assist.editor.upgrade() else {
return false;
};
- let Some(decorations) = pending_assist.editor_decorations.take() else {
+ let Some(decorations) = assist.decorations.take() else {
return false;
};
@@ -453,39 +531,136 @@ impl InlineAssistant {
to_remove.insert(decorations.prompt_block_id);
to_remove.insert(decorations.end_block_id);
editor.remove_blocks(to_remove, None, cx);
- if decorations
- .prompt_editor
- .focus_handle(cx)
- .contains_focused(cx)
+ });
+
+ if decorations
+ .prompt_editor
+ .focus_handle(cx)
+ .contains_focused(cx)
+ {
+ self.focus_next_assist(assist_id, cx);
+ }
+
+ if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) {
+ if editor_assists
+ .scroll_lock
+ .as_ref()
+ .map_or(false, |lock| lock.assist_id == assist_id)
{
- editor.focus(cx);
+ editor_assists.scroll_lock = None;
}
- });
+ editor_assists.highlight_updates.send(()).ok();
+ }
- self.update_editor_highlights(&editor, cx);
true
}
- fn resize_inline_assist(
+ fn focus_next_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
+ let Some(assist) = self.assists.get(&assist_id) else {
+ return;
+ };
+
+ let assist_group = &self.assist_groups[&assist.group_id];
+ let assist_ix = assist_group
+ .assist_ids
+ .iter()
+ .position(|id| *id == assist_id)
+ .unwrap();
+ let assist_ids = assist_group
+ .assist_ids
+ .iter()
+ .skip(assist_ix + 1)
+ .chain(assist_group.assist_ids.iter().take(assist_ix));
+
+ for assist_id in assist_ids {
+ let assist = &self.assists[assist_id];
+ if assist.decorations.is_some() {
+ self.focus_assist(*assist_id, cx);
+ return;
+ }
+ }
+
+ assist.editor.update(cx, |editor, cx| editor.focus(cx)).ok();
+ }
+
+ fn focus_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
+ let assist = &self.assists[&assist_id];
+ let Some(editor) = assist.editor.upgrade() else {
+ return;
+ };
+
+ if let Some(decorations) = assist.decorations.as_ref() {
+ decorations.prompt_editor.update(cx, |prompt_editor, cx| {
+ prompt_editor.editor.update(cx, |editor, cx| {
+ editor.focus(cx);
+ editor.select_all(&SelectAll, cx);
+ })
+ });
+ }
+
+ let position = assist.codegen.read(cx).range.start;
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |selections| {
+ selections.select_anchor_ranges([position..position])
+ });
+
+ let mut scroll_target_top;
+ let mut scroll_target_bottom;
+ if let Some(decorations) = assist.decorations.as_ref() {
+ scroll_target_top = editor
+ .row_for_block(decorations.prompt_block_id, cx)
+ .unwrap()
+ .0 as f32;
+ scroll_target_bottom = editor
+ .row_for_block(decorations.end_block_id, cx)
+ .unwrap()
+ .0 as f32;
+ } else {
+ let snapshot = editor.snapshot(cx);
+ let codegen = assist.codegen.read(cx);
+ let start_row = codegen
+ .range
+ .start
+ .to_display_point(&snapshot.display_snapshot)
+ .row();
+ scroll_target_top = start_row.0 as f32;
+ scroll_target_bottom = scroll_target_top + 1.;
+ }
+ scroll_target_top -= editor.vertical_scroll_margin() as f32;
+ scroll_target_bottom += editor.vertical_scroll_margin() as f32;
+
+ let height_in_lines = editor.visible_line_count().unwrap_or(0.);
+ let scroll_top = editor.scroll_position(cx).y;
+ let scroll_bottom = scroll_top + height_in_lines;
+
+ if scroll_target_top < scroll_top {
+ editor.set_scroll_position(point(0., scroll_target_top), cx);
+ } else if scroll_target_bottom > scroll_bottom {
+ if (scroll_target_bottom - scroll_target_top) <= height_in_lines {
+ editor
+ .set_scroll_position(point(0., scroll_target_bottom - height_in_lines), cx);
+ } else {
+ editor.set_scroll_position(point(0., scroll_target_top), cx);
+ }
+ }
+ });
+ }
+
+ fn resize_assist(
&mut self,
assist_id: InlineAssistId,
height_in_lines: u8,
cx: &mut WindowContext,
) {
- if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) {
- if let Some(editor) = pending_assist.editor.upgrade() {
- if let Some(decorations) = pending_assist.editor_decorations.as_ref() {
- let gutter_dimensions =
- decorations.prompt_editor.read(cx).gutter_dimensions.clone();
+ if let Some(assist) = self.assists.get_mut(&assist_id) {
+ if let Some(editor) = assist.editor.upgrade() {
+ if let Some(decorations) = assist.decorations.as_ref() {
let mut new_blocks = HashMap::default();
new_blocks.insert(
decorations.prompt_block_id,
(
Some(height_in_lines),
- build_inline_assist_editor_renderer(
- &decorations.prompt_editor,
- gutter_dimensions,
- ),
+ build_assist_editor_renderer(&decorations.prompt_editor),
),
);
editor.update(cx, |editor, cx| {
@@ -498,28 +673,51 @@ impl InlineAssistant {
}
}
- fn start_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
- let pending_assist = if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id)
- {
- pending_assist
+ fn unlink_assist_group(
+ &mut self,
+ assist_group_id: InlineAssistGroupId,
+ cx: &mut WindowContext,
+ ) -> Vec<InlineAssistId> {
+ let assist_group = self.assist_groups.get_mut(&assist_group_id).unwrap();
+ assist_group.linked = false;
+ for assist_id in &assist_group.assist_ids {
+ let assist = self.assists.get_mut(assist_id).unwrap();
+ if let Some(editor_decorations) = assist.decorations.as_ref() {
+ editor_decorations
+ .prompt_editor
+ .update(cx, |prompt_editor, cx| prompt_editor.unlink(cx));
+ }
+ }
+ assist_group.assist_ids.clone()
+ }
+
+ fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
+ let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
+ assist
} else {
return;
};
- pending_assist
- .codegen
- .update(cx, |codegen, cx| codegen.undo(cx));
+ let assist_group_id = assist.group_id;
+ if self.assist_groups[&assist_group_id].linked {
+ for assist_id in self.unlink_assist_group(assist_group_id, cx) {
+ self.start_assist(assist_id, cx);
+ }
+ return;
+ }
+
+ assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
- let Some(user_prompt) = pending_assist
- .editor_decorations
+ let Some(user_prompt) = assist
+ .decorations
.as_ref()
.map(|decorations| decorations.prompt_editor.read(cx).prompt(cx))
else {
return;
};
- let context = if pending_assist.include_context {
- pending_assist.workspace.as_ref().and_then(|workspace| {
+ let context = if assist.include_context {
+ assist.workspace.as_ref().and_then(|workspace| {
let workspace = workspace.upgrade()?.read(cx);
let assistant_panel = workspace.panel::<AssistantPanel>(cx)?;
assistant_panel.read(cx).active_context(cx)