1use std::cmp;
2use std::mem;
3use std::ops::Range;
4use std::rc::Rc;
5use std::sync::Arc;
6
7use anyhow::{Context as _, Result};
8use assistant_settings::AssistantSettings;
9use client::telemetry::Telemetry;
10use collections::{hash_map, HashMap, HashSet, VecDeque};
11use editor::{
12 actions::SelectAll,
13 display_map::{
14 BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
15 ToDisplayPoint,
16 },
17 Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
18 GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
19};
20use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _};
21use fs::Fs;
22use gpui::{
23 point, App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task,
24 UpdateGlobal, WeakEntity, Window,
25};
26use language::{Buffer, Point, Selection, TransactionId};
27use language_model::{report_assistant_event, LanguageModelRegistry};
28use multi_buffer::MultiBufferRow;
29use parking_lot::Mutex;
30use project::ActionVariant;
31use project::{CodeAction, ProjectTransaction};
32use prompt_store::PromptBuilder;
33use settings::{Settings, SettingsStore};
34use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
35use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
36use text::{OffsetRangeExt, ToPoint as _};
37use ui::prelude::*;
38use util::RangeExt;
39use util::ResultExt;
40use workspace::{dock::Panel, ShowConfiguration};
41use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
42
43use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
44use crate::context_store::ContextStore;
45use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent};
46use crate::terminal_inline_assistant::TerminalInlineAssistant;
47use crate::thread_store::ThreadStore;
48use crate::AssistantPanel;
49
50pub fn init(
51 fs: Arc<dyn Fs>,
52 prompt_builder: Arc<PromptBuilder>,
53 telemetry: Arc<Telemetry>,
54 cx: &mut App,
55) {
56 cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
57 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
58 let Some(window) = window else {
59 return;
60 };
61 let workspace = cx.entity().clone();
62 InlineAssistant::update_global(cx, |inline_assistant, cx| {
63 inline_assistant.register_workspace(&workspace, window, cx)
64 });
65
66 cx.observe_flag::<Assistant2FeatureFlag, _>(window, {
67 |is_assistant2_enabled, _workspace, _window, cx| {
68 InlineAssistant::update_global(cx, |inline_assistant, _cx| {
69 inline_assistant.is_assistant2_enabled = is_assistant2_enabled;
70 });
71 }
72 })
73 .detach();
74 })
75 .detach();
76}
77
78const PROMPT_HISTORY_MAX_LEN: usize = 20;
79
80enum InlineAssistTarget {
81 Editor(Entity<Editor>),
82 Terminal(Entity<TerminalView>),
83}
84
85pub struct InlineAssistant {
86 next_assist_id: InlineAssistId,
87 next_assist_group_id: InlineAssistGroupId,
88 assists: HashMap<InlineAssistId, InlineAssist>,
89 assists_by_editor: HashMap<WeakEntity<Editor>, EditorInlineAssists>,
90 assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
91 confirmed_assists: HashMap<InlineAssistId, Entity<CodegenAlternative>>,
92 prompt_history: VecDeque<String>,
93 prompt_builder: Arc<PromptBuilder>,
94 telemetry: Arc<Telemetry>,
95 fs: Arc<dyn Fs>,
96 is_assistant2_enabled: bool,
97}
98
99impl Global for InlineAssistant {}
100
101impl InlineAssistant {
102 pub fn new(
103 fs: Arc<dyn Fs>,
104 prompt_builder: Arc<PromptBuilder>,
105 telemetry: Arc<Telemetry>,
106 ) -> Self {
107 Self {
108 next_assist_id: InlineAssistId::default(),
109 next_assist_group_id: InlineAssistGroupId::default(),
110 assists: HashMap::default(),
111 assists_by_editor: HashMap::default(),
112 assist_groups: HashMap::default(),
113 confirmed_assists: HashMap::default(),
114 prompt_history: VecDeque::default(),
115 prompt_builder,
116 telemetry,
117 fs,
118 is_assistant2_enabled: false,
119 }
120 }
121
122 pub fn register_workspace(
123 &mut self,
124 workspace: &Entity<Workspace>,
125 window: &mut Window,
126 cx: &mut App,
127 ) {
128 window
129 .subscribe(workspace, cx, |workspace, event, window, cx| {
130 Self::update_global(cx, |this, cx| {
131 this.handle_workspace_event(workspace, event, window, cx)
132 });
133 })
134 .detach();
135
136 let workspace = workspace.downgrade();
137 cx.observe_global::<SettingsStore>(move |cx| {
138 let Some(workspace) = workspace.upgrade() else {
139 return;
140 };
141 let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
142 return;
143 };
144 let enabled = AssistantSettings::get_global(cx).enabled;
145 terminal_panel.update(cx, |terminal_panel, cx| {
146 terminal_panel.set_assistant_enabled(enabled, cx)
147 });
148 })
149 .detach();
150 }
151
152 fn handle_workspace_event(
153 &mut self,
154 workspace: Entity<Workspace>,
155 event: &workspace::Event,
156 window: &mut Window,
157 cx: &mut App,
158 ) {
159 match event {
160 workspace::Event::UserSavedItem { item, .. } => {
161 // When the user manually saves an editor, automatically accepts all finished transformations.
162 if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx)) {
163 if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
164 for assist_id in editor_assists.assist_ids.clone() {
165 let assist = &self.assists[&assist_id];
166 if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) {
167 self.finish_assist(assist_id, false, window, cx)
168 }
169 }
170 }
171 }
172 }
173 workspace::Event::ItemAdded { item } => {
174 self.register_workspace_item(&workspace, item.as_ref(), window, cx);
175 }
176 _ => (),
177 }
178 }
179
180 fn register_workspace_item(
181 &mut self,
182 workspace: &Entity<Workspace>,
183 item: &dyn ItemHandle,
184 window: &mut Window,
185 cx: &mut App,
186 ) {
187 let is_assistant2_enabled = self.is_assistant2_enabled;
188
189 if let Some(editor) = item.act_as::<Editor>(cx) {
190 editor.update(cx, |editor, cx| {
191 if is_assistant2_enabled {
192 let thread_store = workspace
193 .read(cx)
194 .panel::<AssistantPanel>(cx)
195 .map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
196
197 editor.add_code_action_provider(
198 Rc::new(AssistantCodeActionProvider {
199 editor: cx.entity().downgrade(),
200 workspace: workspace.downgrade(),
201 thread_store,
202 }),
203 window,
204 cx,
205 );
206
207 // Remove the Assistant1 code action provider, as it still might be registered.
208 editor.remove_code_action_provider("assistant".into(), window, cx);
209 } else {
210 editor.remove_code_action_provider(
211 ASSISTANT_CODE_ACTION_PROVIDER_ID.into(),
212 window,
213 cx,
214 );
215 }
216 });
217 }
218 }
219
220 pub fn inline_assist(
221 workspace: &mut Workspace,
222 _action: &zed_actions::assistant::InlineAssist,
223 window: &mut Window,
224 cx: &mut Context<Workspace>,
225 ) {
226 let settings = AssistantSettings::get_global(cx);
227 if !settings.enabled {
228 return;
229 }
230
231 let Some(inline_assist_target) = Self::resolve_inline_assist_target(
232 workspace,
233 workspace.panel::<AssistantPanel>(cx),
234 window,
235 cx,
236 ) else {
237 return;
238 };
239
240 let is_authenticated = || {
241 LanguageModelRegistry::read_global(cx)
242 .active_provider()
243 .map_or(false, |provider| provider.is_authenticated(cx))
244 };
245
246 let thread_store = workspace
247 .panel::<AssistantPanel>(cx)
248 .map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
249
250 let handle_assist =
251 |window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
252 InlineAssistTarget::Editor(active_editor) => {
253 InlineAssistant::update_global(cx, |assistant, cx| {
254 assistant.assist(
255 &active_editor,
256 cx.entity().downgrade(),
257 thread_store,
258 window,
259 cx,
260 )
261 })
262 }
263 InlineAssistTarget::Terminal(active_terminal) => {
264 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
265 assistant.assist(
266 &active_terminal,
267 cx.entity().downgrade(),
268 thread_store,
269 window,
270 cx,
271 )
272 })
273 }
274 };
275
276 if is_authenticated() {
277 handle_assist(window, cx);
278 } else {
279 cx.spawn_in(window, |_workspace, mut cx| async move {
280 let Some(task) = cx.update(|_, cx| {
281 LanguageModelRegistry::read_global(cx)
282 .active_provider()
283 .map_or(None, |provider| Some(provider.authenticate(cx)))
284 })?
285 else {
286 let answer = cx
287 .prompt(
288 gpui::PromptLevel::Warning,
289 "No language model provider configured",
290 None,
291 &["Configure", "Cancel"],
292 )
293 .await
294 .ok();
295 if let Some(answer) = answer {
296 if answer == 0 {
297 cx.update(|window, cx| {
298 window.dispatch_action(Box::new(ShowConfiguration), cx)
299 })
300 .ok();
301 }
302 }
303 return Ok(());
304 };
305 task.await?;
306
307 anyhow::Ok(())
308 })
309 .detach_and_log_err(cx);
310
311 if is_authenticated() {
312 handle_assist(window, cx);
313 }
314 }
315 }
316
317 pub fn assist(
318 &mut self,
319 editor: &Entity<Editor>,
320 workspace: WeakEntity<Workspace>,
321 thread_store: Option<WeakEntity<ThreadStore>>,
322 window: &mut Window,
323 cx: &mut App,
324 ) {
325 let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
326 (
327 editor.buffer().read(cx).snapshot(cx),
328 editor.selections.all::<Point>(cx),
329 )
330 });
331
332 let mut selections = Vec::<Selection<Point>>::new();
333 let mut newest_selection = None;
334 for mut selection in initial_selections {
335 if selection.end > selection.start {
336 selection.start.column = 0;
337 // If the selection ends at the start of the line, we don't want to include it.
338 if selection.end.column == 0 {
339 selection.end.row -= 1;
340 }
341 selection.end.column = snapshot.line_len(MultiBufferRow(selection.end.row));
342 }
343
344 if let Some(prev_selection) = selections.last_mut() {
345 if selection.start <= prev_selection.end {
346 prev_selection.end = selection.end;
347 continue;
348 }
349 }
350
351 let latest_selection = newest_selection.get_or_insert_with(|| selection.clone());
352 if selection.id > latest_selection.id {
353 *latest_selection = selection.clone();
354 }
355 selections.push(selection);
356 }
357 let newest_selection = newest_selection.unwrap();
358
359 let mut codegen_ranges = Vec::new();
360 for (buffer, buffer_range, excerpt_id) in
361 snapshot.ranges_to_buffer_ranges(selections.iter().map(|selection| {
362 snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
363 }))
364 {
365 let anchor_range = Anchor::range_in_buffer(
366 excerpt_id,
367 buffer.remote_id(),
368 buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end),
369 );
370
371 codegen_ranges.push(anchor_range);
372
373 if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
374 self.telemetry.report_assistant_event(AssistantEvent {
375 conversation_id: None,
376 kind: AssistantKind::Inline,
377 phase: AssistantPhase::Invoked,
378 message_id: None,
379 model: model.telemetry_id(),
380 model_provider: model.provider_id().to_string(),
381 response_latency: None,
382 error_message: None,
383 language_name: buffer.language().map(|language| language.name().to_proto()),
384 });
385 }
386 }
387
388 let assist_group_id = self.next_assist_group_id.post_inc();
389 let prompt_buffer =
390 cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local(String::new(), cx)), cx));
391
392 let mut assists = Vec::new();
393 let mut assist_to_focus = None;
394 for range in codegen_ranges {
395 let assist_id = self.next_assist_id.post_inc();
396 let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
397 let codegen = cx.new(|cx| {
398 BufferCodegen::new(
399 editor.read(cx).buffer().clone(),
400 range.clone(),
401 None,
402 context_store.clone(),
403 self.telemetry.clone(),
404 self.prompt_builder.clone(),
405 cx,
406 )
407 });
408
409 let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
410 let prompt_editor = cx.new(|cx| {
411 PromptEditor::new_buffer(
412 assist_id,
413 gutter_dimensions.clone(),
414 self.prompt_history.clone(),
415 prompt_buffer.clone(),
416 codegen.clone(),
417 self.fs.clone(),
418 context_store,
419 workspace.clone(),
420 thread_store.clone(),
421 window,
422 cx,
423 )
424 });
425
426 if assist_to_focus.is_none() {
427 let focus_assist = if newest_selection.reversed {
428 range.start.to_point(&snapshot) == newest_selection.start
429 } else {
430 range.end.to_point(&snapshot) == newest_selection.end
431 };
432 if focus_assist {
433 assist_to_focus = Some(assist_id);
434 }
435 }
436
437 let [prompt_block_id, end_block_id] =
438 self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
439
440 assists.push((
441 assist_id,
442 range,
443 prompt_editor,
444 prompt_block_id,
445 end_block_id,
446 ));
447 }
448
449 let editor_assists = self
450 .assists_by_editor
451 .entry(editor.downgrade())
452 .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx));
453 let mut assist_group = InlineAssistGroup::new();
454 for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists {
455 let codegen = prompt_editor.read(cx).codegen().clone();
456
457 self.assists.insert(
458 assist_id,
459 InlineAssist::new(
460 assist_id,
461 assist_group_id,
462 editor,
463 &prompt_editor,
464 prompt_block_id,
465 end_block_id,
466 range,
467 codegen,
468 workspace.clone(),
469 window,
470 cx,
471 ),
472 );
473 assist_group.assist_ids.push(assist_id);
474 editor_assists.assist_ids.push(assist_id);
475 }
476 self.assist_groups.insert(assist_group_id, assist_group);
477
478 if let Some(assist_id) = assist_to_focus {
479 self.focus_assist(assist_id, window, cx);
480 }
481 }
482
483 #[allow(clippy::too_many_arguments)]
484 pub fn suggest_assist(
485 &mut self,
486 editor: &Entity<Editor>,
487 mut range: Range<Anchor>,
488 initial_prompt: String,
489 initial_transaction_id: Option<TransactionId>,
490 focus: bool,
491 workspace: WeakEntity<Workspace>,
492 thread_store: Option<WeakEntity<ThreadStore>>,
493 window: &mut Window,
494 cx: &mut App,
495 ) -> InlineAssistId {
496 let assist_group_id = self.next_assist_group_id.post_inc();
497 let prompt_buffer = cx.new(|cx| Buffer::local(&initial_prompt, cx));
498 let prompt_buffer = cx.new(|cx| MultiBuffer::singleton(prompt_buffer, cx));
499
500 let assist_id = self.next_assist_id.post_inc();
501
502 let buffer = editor.read(cx).buffer().clone();
503 {
504 let snapshot = buffer.read(cx).read(cx);
505 range.start = range.start.bias_left(&snapshot);
506 range.end = range.end.bias_right(&snapshot);
507 }
508
509 let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
510
511 let codegen = cx.new(|cx| {
512 BufferCodegen::new(
513 editor.read(cx).buffer().clone(),
514 range.clone(),
515 initial_transaction_id,
516 context_store.clone(),
517 self.telemetry.clone(),
518 self.prompt_builder.clone(),
519 cx,
520 )
521 });
522
523 let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
524 let prompt_editor = cx.new(|cx| {
525 PromptEditor::new_buffer(
526 assist_id,
527 gutter_dimensions.clone(),
528 self.prompt_history.clone(),
529 prompt_buffer.clone(),
530 codegen.clone(),
531 self.fs.clone(),
532 context_store,
533 workspace.clone(),
534 thread_store,
535 window,
536 cx,
537 )
538 });
539
540 let [prompt_block_id, end_block_id] =
541 self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
542
543 let editor_assists = self
544 .assists_by_editor
545 .entry(editor.downgrade())
546 .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx));
547
548 let mut assist_group = InlineAssistGroup::new();
549 self.assists.insert(
550 assist_id,
551 InlineAssist::new(
552 assist_id,
553 assist_group_id,
554 editor,
555 &prompt_editor,
556 prompt_block_id,
557 end_block_id,
558 range,
559 codegen.clone(),
560 workspace.clone(),
561 window,
562 cx,
563 ),
564 );
565 assist_group.assist_ids.push(assist_id);
566 editor_assists.assist_ids.push(assist_id);
567 self.assist_groups.insert(assist_group_id, assist_group);
568
569 if focus {
570 self.focus_assist(assist_id, window, cx);
571 }
572
573 assist_id
574 }
575
576 fn insert_assist_blocks(
577 &self,
578 editor: &Entity<Editor>,
579 range: &Range<Anchor>,
580 prompt_editor: &Entity<PromptEditor<BufferCodegen>>,
581 cx: &mut App,
582 ) -> [CustomBlockId; 2] {
583 let prompt_editor_height = prompt_editor.update(cx, |prompt_editor, cx| {
584 prompt_editor
585 .editor
586 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1 + 2)
587 });
588 let assist_blocks = vec![
589 BlockProperties {
590 style: BlockStyle::Sticky,
591 placement: BlockPlacement::Above(range.start),
592 height: prompt_editor_height,
593 render: build_assist_editor_renderer(prompt_editor),
594 priority: 0,
595 },
596 BlockProperties {
597 style: BlockStyle::Sticky,
598 placement: BlockPlacement::Below(range.end),
599 height: 0,
600 render: Arc::new(|cx| {
601 v_flex()
602 .h_full()
603 .w_full()
604 .border_t_1()
605 .border_color(cx.theme().status().info_border)
606 .into_any_element()
607 }),
608 priority: 0,
609 },
610 ];
611
612 editor.update(cx, |editor, cx| {
613 let block_ids = editor.insert_blocks(assist_blocks, None, cx);
614 [block_ids[0], block_ids[1]]
615 })
616 }
617
618 fn handle_prompt_editor_focus_in(&mut self, assist_id: InlineAssistId, cx: &mut App) {
619 let assist = &self.assists[&assist_id];
620 let Some(decorations) = assist.decorations.as_ref() else {
621 return;
622 };
623 let assist_group = self.assist_groups.get_mut(&assist.group_id).unwrap();
624 let editor_assists = self.assists_by_editor.get_mut(&assist.editor).unwrap();
625
626 assist_group.active_assist_id = Some(assist_id);
627 if assist_group.linked {
628 for assist_id in &assist_group.assist_ids {
629 if let Some(decorations) = self.assists[assist_id].decorations.as_ref() {
630 decorations.prompt_editor.update(cx, |prompt_editor, cx| {
631 prompt_editor.set_show_cursor_when_unfocused(true, cx)
632 });
633 }
634 }
635 }
636
637 assist
638 .editor
639 .update(cx, |editor, cx| {
640 let scroll_top = editor.scroll_position(cx).y;
641 let scroll_bottom = scroll_top + editor.visible_line_count().unwrap_or(0.);
642 let prompt_row = editor
643 .row_for_block(decorations.prompt_block_id, cx)
644 .unwrap()
645 .0 as f32;
646
647 if (scroll_top..scroll_bottom).contains(&prompt_row) {
648 editor_assists.scroll_lock = Some(InlineAssistScrollLock {
649 assist_id,
650 distance_from_top: prompt_row - scroll_top,
651 });
652 } else {
653 editor_assists.scroll_lock = None;
654 }
655 })
656 .ok();
657 }
658
659 fn handle_prompt_editor_focus_out(&mut self, assist_id: InlineAssistId, cx: &mut App) {
660 let assist = &self.assists[&assist_id];
661 let assist_group = self.assist_groups.get_mut(&assist.group_id).unwrap();
662 if assist_group.active_assist_id == Some(assist_id) {
663 assist_group.active_assist_id = None;
664 if assist_group.linked {
665 for assist_id in &assist_group.assist_ids {
666 if let Some(decorations) = self.assists[assist_id].decorations.as_ref() {
667 decorations.prompt_editor.update(cx, |prompt_editor, cx| {
668 prompt_editor.set_show_cursor_when_unfocused(false, cx)
669 });
670 }
671 }
672 }
673 }
674 }
675
676 fn handle_prompt_editor_event(
677 &mut self,
678 prompt_editor: Entity<PromptEditor<BufferCodegen>>,
679 event: &PromptEditorEvent,
680 window: &mut Window,
681 cx: &mut App,
682 ) {
683 let assist_id = prompt_editor.read(cx).id();
684 match event {
685 PromptEditorEvent::StartRequested => {
686 self.start_assist(assist_id, window, cx);
687 }
688 PromptEditorEvent::StopRequested => {
689 self.stop_assist(assist_id, cx);
690 }
691 PromptEditorEvent::ConfirmRequested { execute: _ } => {
692 self.finish_assist(assist_id, false, window, cx);
693 }
694 PromptEditorEvent::CancelRequested => {
695 self.finish_assist(assist_id, true, window, cx);
696 }
697 PromptEditorEvent::DismissRequested => {
698 self.dismiss_assist(assist_id, window, cx);
699 }
700 PromptEditorEvent::Resized { .. } => {
701 // This only matters for the terminal inline assistant
702 }
703 }
704 }
705
706 fn handle_editor_newline(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut App) {
707 let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
708 return;
709 };
710
711 if editor.read(cx).selections.count() == 1 {
712 let (selection, buffer) = editor.update(cx, |editor, cx| {
713 (
714 editor.selections.newest::<usize>(cx),
715 editor.buffer().read(cx).snapshot(cx),
716 )
717 });
718 for assist_id in &editor_assists.assist_ids {
719 let assist = &self.assists[assist_id];
720 let assist_range = assist.range.to_offset(&buffer);
721 if assist_range.contains(&selection.start) && assist_range.contains(&selection.end)
722 {
723 if matches!(assist.codegen.read(cx).status(cx), CodegenStatus::Pending) {
724 self.dismiss_assist(*assist_id, window, cx);
725 } else {
726 self.finish_assist(*assist_id, false, window, cx);
727 }
728
729 return;
730 }
731 }
732 }
733
734 cx.propagate();
735 }
736
737 fn handle_editor_cancel(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut App) {
738 let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
739 return;
740 };
741
742 if editor.read(cx).selections.count() == 1 {
743 let (selection, buffer) = editor.update(cx, |editor, cx| {
744 (
745 editor.selections.newest::<usize>(cx),
746 editor.buffer().read(cx).snapshot(cx),
747 )
748 });
749 let mut closest_assist_fallback = None;
750 for assist_id in &editor_assists.assist_ids {
751 let assist = &self.assists[assist_id];
752 let assist_range = assist.range.to_offset(&buffer);
753 if assist.decorations.is_some() {
754 if assist_range.contains(&selection.start)
755 && assist_range.contains(&selection.end)
756 {
757 self.focus_assist(*assist_id, window, cx);
758 return;
759 } else {
760 let distance_from_selection = assist_range
761 .start
762 .abs_diff(selection.start)
763 .min(assist_range.start.abs_diff(selection.end))
764 + assist_range
765 .end
766 .abs_diff(selection.start)
767 .min(assist_range.end.abs_diff(selection.end));
768 match closest_assist_fallback {
769 Some((_, old_distance)) => {
770 if distance_from_selection < old_distance {
771 closest_assist_fallback =
772 Some((assist_id, distance_from_selection));
773 }
774 }
775 None => {
776 closest_assist_fallback = Some((assist_id, distance_from_selection))
777 }
778 }
779 }
780 }
781 }
782
783 if let Some((&assist_id, _)) = closest_assist_fallback {
784 self.focus_assist(assist_id, window, cx);
785 }
786 }
787
788 cx.propagate();
789 }
790
791 fn handle_editor_release(
792 &mut self,
793 editor: WeakEntity<Editor>,
794 window: &mut Window,
795 cx: &mut App,
796 ) {
797 if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor) {
798 for assist_id in editor_assists.assist_ids.clone() {
799 self.finish_assist(assist_id, true, window, cx);
800 }
801 }
802 }
803
804 fn handle_editor_change(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut App) {
805 let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) else {
806 return;
807 };
808 let Some(scroll_lock) = editor_assists.scroll_lock.as_ref() else {
809 return;
810 };
811 let assist = &self.assists[&scroll_lock.assist_id];
812 let Some(decorations) = assist.decorations.as_ref() else {
813 return;
814 };
815
816 editor.update(cx, |editor, cx| {
817 let scroll_position = editor.scroll_position(cx);
818 let target_scroll_top = editor
819 .row_for_block(decorations.prompt_block_id, cx)
820 .unwrap()
821 .0 as f32
822 - scroll_lock.distance_from_top;
823 if target_scroll_top != scroll_position.y {
824 editor.set_scroll_position(point(scroll_position.x, target_scroll_top), window, cx);
825 }
826 });
827 }
828
829 fn handle_editor_event(
830 &mut self,
831 editor: Entity<Editor>,
832 event: &EditorEvent,
833 window: &mut Window,
834 cx: &mut App,
835 ) {
836 let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) else {
837 return;
838 };
839
840 match event {
841 EditorEvent::Edited { transaction_id } => {
842 let buffer = editor.read(cx).buffer().read(cx);
843 let edited_ranges =
844 buffer.edited_ranges_for_transaction::<usize>(*transaction_id, cx);
845 let snapshot = buffer.snapshot(cx);
846
847 for assist_id in editor_assists.assist_ids.clone() {
848 let assist = &self.assists[&assist_id];
849 if matches!(
850 assist.codegen.read(cx).status(cx),
851 CodegenStatus::Error(_) | CodegenStatus::Done
852 ) {
853 let assist_range = assist.range.to_offset(&snapshot);
854 if edited_ranges
855 .iter()
856 .any(|range| range.overlaps(&assist_range))
857 {
858 self.finish_assist(assist_id, false, window, cx);
859 }
860 }
861 }
862 }
863 EditorEvent::ScrollPositionChanged { .. } => {
864 if let Some(scroll_lock) = editor_assists.scroll_lock.as_ref() {
865 let assist = &self.assists[&scroll_lock.assist_id];
866 if let Some(decorations) = assist.decorations.as_ref() {
867 let distance_from_top = editor.update(cx, |editor, cx| {
868 let scroll_top = editor.scroll_position(cx).y;
869 let prompt_row = editor
870 .row_for_block(decorations.prompt_block_id, cx)
871 .unwrap()
872 .0 as f32;
873 prompt_row - scroll_top
874 });
875
876 if distance_from_top != scroll_lock.distance_from_top {
877 editor_assists.scroll_lock = None;
878 }
879 }
880 }
881 }
882 EditorEvent::SelectionsChanged { .. } => {
883 for assist_id in editor_assists.assist_ids.clone() {
884 let assist = &self.assists[&assist_id];
885 if let Some(decorations) = assist.decorations.as_ref() {
886 if decorations
887 .prompt_editor
888 .focus_handle(cx)
889 .is_focused(window)
890 {
891 return;
892 }
893 }
894 }
895
896 editor_assists.scroll_lock = None;
897 }
898 _ => {}
899 }
900 }
901
902 pub fn finish_assist(
903 &mut self,
904 assist_id: InlineAssistId,
905 undo: bool,
906 window: &mut Window,
907 cx: &mut App,
908 ) {
909 if let Some(assist) = self.assists.get(&assist_id) {
910 let assist_group_id = assist.group_id;
911 if self.assist_groups[&assist_group_id].linked {
912 for assist_id in self.unlink_assist_group(assist_group_id, window, cx) {
913 self.finish_assist(assist_id, undo, window, cx);
914 }
915 return;
916 }
917 }
918
919 self.dismiss_assist(assist_id, window, cx);
920
921 if let Some(assist) = self.assists.remove(&assist_id) {
922 if let hash_map::Entry::Occupied(mut entry) = self.assist_groups.entry(assist.group_id)
923 {
924 entry.get_mut().assist_ids.retain(|id| *id != assist_id);
925 if entry.get().assist_ids.is_empty() {
926 entry.remove();
927 }
928 }
929
930 if let hash_map::Entry::Occupied(mut entry) =
931 self.assists_by_editor.entry(assist.editor.clone())
932 {
933 entry.get_mut().assist_ids.retain(|id| *id != assist_id);
934 if entry.get().assist_ids.is_empty() {
935 entry.remove();
936 if let Some(editor) = assist.editor.upgrade() {
937 self.update_editor_highlights(&editor, cx);
938 }
939 } else {
940 entry.get().highlight_updates.send(()).ok();
941 }
942 }
943
944 let active_alternative = assist.codegen.read(cx).active_alternative().clone();
945 let message_id = active_alternative.read(cx).message_id.clone();
946
947 if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
948 let language_name = assist.editor.upgrade().and_then(|editor| {
949 let multibuffer = editor.read(cx).buffer().read(cx);
950 let snapshot = multibuffer.snapshot(cx);
951 let ranges = snapshot.range_to_buffer_ranges(assist.range.clone());
952 ranges
953 .first()
954 .and_then(|(buffer, _, _)| buffer.language())
955 .map(|language| language.name())
956 });
957 report_assistant_event(
958 AssistantEvent {
959 conversation_id: None,
960 kind: AssistantKind::Inline,
961 message_id,
962 phase: if undo {
963 AssistantPhase::Rejected
964 } else {
965 AssistantPhase::Accepted
966 },
967 model: model.telemetry_id(),
968 model_provider: model.provider_id().to_string(),
969 response_latency: None,
970 error_message: None,
971 language_name: language_name.map(|name| name.to_proto()),
972 },
973 Some(self.telemetry.clone()),
974 cx.http_client(),
975 model.api_key(cx),
976 cx.background_executor(),
977 );
978 }
979
980 if undo {
981 assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
982 } else {
983 self.confirmed_assists.insert(assist_id, active_alternative);
984 }
985 }
986 }
987
988 fn dismiss_assist(
989 &mut self,
990 assist_id: InlineAssistId,
991 window: &mut Window,
992 cx: &mut App,
993 ) -> bool {
994 let Some(assist) = self.assists.get_mut(&assist_id) else {
995 return false;
996 };
997 let Some(editor) = assist.editor.upgrade() else {
998 return false;
999 };
1000 let Some(decorations) = assist.decorations.take() else {
1001 return false;
1002 };
1003
1004 editor.update(cx, |editor, cx| {
1005 let mut to_remove = decorations.removed_line_block_ids;
1006 to_remove.insert(decorations.prompt_block_id);
1007 to_remove.insert(decorations.end_block_id);
1008 editor.remove_blocks(to_remove, None, cx);
1009 });
1010
1011 if decorations
1012 .prompt_editor
1013 .focus_handle(cx)
1014 .contains_focused(window, cx)
1015 {
1016 self.focus_next_assist(assist_id, window, cx);
1017 }
1018
1019 if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) {
1020 if editor_assists
1021 .scroll_lock
1022 .as_ref()
1023 .map_or(false, |lock| lock.assist_id == assist_id)
1024 {
1025 editor_assists.scroll_lock = None;
1026 }
1027 editor_assists.highlight_updates.send(()).ok();
1028 }
1029
1030 true
1031 }
1032
1033 fn focus_next_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) {
1034 let Some(assist) = self.assists.get(&assist_id) else {
1035 return;
1036 };
1037
1038 let assist_group = &self.assist_groups[&assist.group_id];
1039 let assist_ix = assist_group
1040 .assist_ids
1041 .iter()
1042 .position(|id| *id == assist_id)
1043 .unwrap();
1044 let assist_ids = assist_group
1045 .assist_ids
1046 .iter()
1047 .skip(assist_ix + 1)
1048 .chain(assist_group.assist_ids.iter().take(assist_ix));
1049
1050 for assist_id in assist_ids {
1051 let assist = &self.assists[assist_id];
1052 if assist.decorations.is_some() {
1053 self.focus_assist(*assist_id, window, cx);
1054 return;
1055 }
1056 }
1057
1058 assist
1059 .editor
1060 .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
1061 .ok();
1062 }
1063
1064 fn focus_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) {
1065 let Some(assist) = self.assists.get(&assist_id) else {
1066 return;
1067 };
1068
1069 if let Some(decorations) = assist.decorations.as_ref() {
1070 decorations.prompt_editor.update(cx, |prompt_editor, cx| {
1071 prompt_editor.editor.update(cx, |editor, cx| {
1072 window.focus(&editor.focus_handle(cx));
1073 editor.select_all(&SelectAll, window, cx);
1074 })
1075 });
1076 }
1077
1078 self.scroll_to_assist(assist_id, window, cx);
1079 }
1080
1081 pub fn scroll_to_assist(
1082 &mut self,
1083 assist_id: InlineAssistId,
1084 window: &mut Window,
1085 cx: &mut App,
1086 ) {
1087 let Some(assist) = self.assists.get(&assist_id) else {
1088 return;
1089 };
1090 let Some(editor) = assist.editor.upgrade() else {
1091 return;
1092 };
1093
1094 let position = assist.range.start;
1095 editor.update(cx, |editor, cx| {
1096 editor.change_selections(None, window, cx, |selections| {
1097 selections.select_anchor_ranges([position..position])
1098 });
1099
1100 let mut scroll_target_top;
1101 let mut scroll_target_bottom;
1102 if let Some(decorations) = assist.decorations.as_ref() {
1103 scroll_target_top = editor
1104 .row_for_block(decorations.prompt_block_id, cx)
1105 .unwrap()
1106 .0 as f32;
1107 scroll_target_bottom = editor
1108 .row_for_block(decorations.end_block_id, cx)
1109 .unwrap()
1110 .0 as f32;
1111 } else {
1112 let snapshot = editor.snapshot(window, cx);
1113 let start_row = assist
1114 .range
1115 .start
1116 .to_display_point(&snapshot.display_snapshot)
1117 .row();
1118 scroll_target_top = start_row.0 as f32;
1119 scroll_target_bottom = scroll_target_top + 1.;
1120 }
1121 scroll_target_top -= editor.vertical_scroll_margin() as f32;
1122 scroll_target_bottom += editor.vertical_scroll_margin() as f32;
1123
1124 let height_in_lines = editor.visible_line_count().unwrap_or(0.);
1125 let scroll_top = editor.scroll_position(cx).y;
1126 let scroll_bottom = scroll_top + height_in_lines;
1127
1128 if scroll_target_top < scroll_top {
1129 editor.set_scroll_position(point(0., scroll_target_top), window, cx);
1130 } else if scroll_target_bottom > scroll_bottom {
1131 if (scroll_target_bottom - scroll_target_top) <= height_in_lines {
1132 editor.set_scroll_position(
1133 point(0., scroll_target_bottom - height_in_lines),
1134 window,
1135 cx,
1136 );
1137 } else {
1138 editor.set_scroll_position(point(0., scroll_target_top), window, cx);
1139 }
1140 }
1141 });
1142 }
1143
1144 fn unlink_assist_group(
1145 &mut self,
1146 assist_group_id: InlineAssistGroupId,
1147 window: &mut Window,
1148 cx: &mut App,
1149 ) -> Vec<InlineAssistId> {
1150 let assist_group = self.assist_groups.get_mut(&assist_group_id).unwrap();
1151 assist_group.linked = false;
1152 for assist_id in &assist_group.assist_ids {
1153 let assist = self.assists.get_mut(assist_id).unwrap();
1154 if let Some(editor_decorations) = assist.decorations.as_ref() {
1155 editor_decorations
1156 .prompt_editor
1157 .update(cx, |prompt_editor, cx| prompt_editor.unlink(window, cx));
1158 }
1159 }
1160 assist_group.assist_ids.clone()
1161 }
1162
1163 pub fn start_assist(&mut self, assist_id: InlineAssistId, window: &mut Window, cx: &mut App) {
1164 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
1165 assist
1166 } else {
1167 return;
1168 };
1169
1170 let assist_group_id = assist.group_id;
1171 if self.assist_groups[&assist_group_id].linked {
1172 for assist_id in self.unlink_assist_group(assist_group_id, window, cx) {
1173 self.start_assist(assist_id, window, cx);
1174 }
1175 return;
1176 }
1177
1178 let Some(user_prompt) = assist.user_prompt(cx) else {
1179 return;
1180 };
1181
1182 self.prompt_history.retain(|prompt| *prompt != user_prompt);
1183 self.prompt_history.push_back(user_prompt.clone());
1184 if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
1185 self.prompt_history.pop_front();
1186 }
1187
1188 assist
1189 .codegen
1190 .update(cx, |codegen, cx| codegen.start(user_prompt, cx))
1191 .log_err();
1192 }
1193
1194 pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut App) {
1195 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
1196 assist
1197 } else {
1198 return;
1199 };
1200
1201 assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
1202 }
1203
1204 fn update_editor_highlights(&self, editor: &Entity<Editor>, cx: &mut App) {
1205 let mut gutter_pending_ranges = Vec::new();
1206 let mut gutter_transformed_ranges = Vec::new();
1207 let mut foreground_ranges = Vec::new();
1208 let mut inserted_row_ranges = Vec::new();
1209 let empty_assist_ids = Vec::new();
1210 let assist_ids = self
1211 .assists_by_editor
1212 .get(&editor.downgrade())
1213 .map_or(&empty_assist_ids, |editor_assists| {
1214 &editor_assists.assist_ids
1215 });
1216
1217 for assist_id in assist_ids {
1218 if let Some(assist) = self.assists.get(assist_id) {
1219 let codegen = assist.codegen.read(cx);
1220 let buffer = codegen.buffer(cx).read(cx).read(cx);
1221 foreground_ranges.extend(codegen.last_equal_ranges(cx).iter().cloned());
1222
1223 let pending_range =
1224 codegen.edit_position(cx).unwrap_or(assist.range.start)..assist.range.end;
1225 if pending_range.end.to_offset(&buffer) > pending_range.start.to_offset(&buffer) {
1226 gutter_pending_ranges.push(pending_range);
1227 }
1228
1229 if let Some(edit_position) = codegen.edit_position(cx) {
1230 let edited_range = assist.range.start..edit_position;
1231 if edited_range.end.to_offset(&buffer) > edited_range.start.to_offset(&buffer) {
1232 gutter_transformed_ranges.push(edited_range);
1233 }
1234 }
1235
1236 if assist.decorations.is_some() {
1237 inserted_row_ranges
1238 .extend(codegen.diff(cx).inserted_row_ranges.iter().cloned());
1239 }
1240 }
1241 }
1242
1243 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
1244 merge_ranges(&mut foreground_ranges, &snapshot);
1245 merge_ranges(&mut gutter_pending_ranges, &snapshot);
1246 merge_ranges(&mut gutter_transformed_ranges, &snapshot);
1247 editor.update(cx, |editor, cx| {
1248 enum GutterPendingRange {}
1249 if gutter_pending_ranges.is_empty() {
1250 editor.clear_gutter_highlights::<GutterPendingRange>(cx);
1251 } else {
1252 editor.highlight_gutter::<GutterPendingRange>(
1253 &gutter_pending_ranges,
1254 |cx| cx.theme().status().info_background,
1255 cx,
1256 )
1257 }
1258
1259 enum GutterTransformedRange {}
1260 if gutter_transformed_ranges.is_empty() {
1261 editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
1262 } else {
1263 editor.highlight_gutter::<GutterTransformedRange>(
1264 &gutter_transformed_ranges,
1265 |cx| cx.theme().status().info,
1266 cx,
1267 )
1268 }
1269
1270 if foreground_ranges.is_empty() {
1271 editor.clear_highlights::<InlineAssist>(cx);
1272 } else {
1273 editor.highlight_text::<InlineAssist>(
1274 foreground_ranges,
1275 HighlightStyle {
1276 fade_out: Some(0.6),
1277 ..Default::default()
1278 },
1279 cx,
1280 );
1281 }
1282
1283 editor.clear_row_highlights::<InlineAssist>();
1284 for row_range in inserted_row_ranges {
1285 editor.highlight_rows::<InlineAssist>(
1286 row_range,
1287 cx.theme().status().info_background,
1288 false,
1289 cx,
1290 );
1291 }
1292 });
1293 }
1294
1295 fn update_editor_blocks(
1296 &mut self,
1297 editor: &Entity<Editor>,
1298 assist_id: InlineAssistId,
1299 window: &mut Window,
1300 cx: &mut App,
1301 ) {
1302 let Some(assist) = self.assists.get_mut(&assist_id) else {
1303 return;
1304 };
1305 let Some(decorations) = assist.decorations.as_mut() else {
1306 return;
1307 };
1308
1309 let codegen = assist.codegen.read(cx);
1310 let old_snapshot = codegen.snapshot(cx);
1311 let old_buffer = codegen.old_buffer(cx);
1312 let deleted_row_ranges = codegen.diff(cx).deleted_row_ranges.clone();
1313
1314 editor.update(cx, |editor, cx| {
1315 let old_blocks = mem::take(&mut decorations.removed_line_block_ids);
1316 editor.remove_blocks(old_blocks, None, cx);
1317
1318 let mut new_blocks = Vec::new();
1319 for (new_row, old_row_range) in deleted_row_ranges {
1320 let (_, buffer_start) = old_snapshot
1321 .point_to_buffer_offset(Point::new(*old_row_range.start(), 0))
1322 .unwrap();
1323 let (_, buffer_end) = old_snapshot
1324 .point_to_buffer_offset(Point::new(
1325 *old_row_range.end(),
1326 old_snapshot.line_len(MultiBufferRow(*old_row_range.end())),
1327 ))
1328 .unwrap();
1329
1330 let deleted_lines_editor = cx.new(|cx| {
1331 let multi_buffer =
1332 cx.new(|_| MultiBuffer::without_headers(language::Capability::ReadOnly));
1333 multi_buffer.update(cx, |multi_buffer, cx| {
1334 multi_buffer.push_excerpts(
1335 old_buffer.clone(),
1336 Some(ExcerptRange {
1337 context: buffer_start..buffer_end,
1338 primary: None,
1339 }),
1340 cx,
1341 );
1342 });
1343
1344 enum DeletedLines {}
1345 let mut editor = Editor::for_multibuffer(multi_buffer, None, true, window, cx);
1346 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
1347 editor.set_show_wrap_guides(false, cx);
1348 editor.set_show_gutter(false, cx);
1349 editor.scroll_manager.set_forbid_vertical_scroll(true);
1350 editor.set_show_scrollbars(false, cx);
1351 editor.set_read_only(true);
1352 editor.set_show_edit_predictions(Some(false), window, cx);
1353 editor.highlight_rows::<DeletedLines>(
1354 Anchor::min()..Anchor::max(),
1355 cx.theme().status().deleted_background,
1356 false,
1357 cx,
1358 );
1359 editor
1360 });
1361
1362 let height =
1363 deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
1364 new_blocks.push(BlockProperties {
1365 placement: BlockPlacement::Above(new_row),
1366 height,
1367 style: BlockStyle::Flex,
1368 render: Arc::new(move |cx| {
1369 div()
1370 .block_mouse_down()
1371 .bg(cx.theme().status().deleted_background)
1372 .size_full()
1373 .h(height as f32 * cx.window.line_height())
1374 .pl(cx.gutter_dimensions.full_width())
1375 .child(deleted_lines_editor.clone())
1376 .into_any_element()
1377 }),
1378 priority: 0,
1379 });
1380 }
1381
1382 decorations.removed_line_block_ids = editor
1383 .insert_blocks(new_blocks, None, cx)
1384 .into_iter()
1385 .collect();
1386 })
1387 }
1388
1389 fn resolve_inline_assist_target(
1390 workspace: &mut Workspace,
1391 assistant_panel: Option<Entity<AssistantPanel>>,
1392 window: &mut Window,
1393 cx: &mut App,
1394 ) -> Option<InlineAssistTarget> {
1395 if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
1396 if terminal_panel
1397 .read(cx)
1398 .focus_handle(cx)
1399 .contains_focused(window, cx)
1400 {
1401 if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
1402 pane.read(cx)
1403 .active_item()
1404 .and_then(|t| t.downcast::<TerminalView>())
1405 }) {
1406 return Some(InlineAssistTarget::Terminal(terminal_view));
1407 }
1408 }
1409 }
1410
1411 let context_editor = assistant_panel
1412 .and_then(|panel| panel.read(cx).active_context_editor())
1413 .and_then(|editor| {
1414 let editor = &editor.read(cx).editor().clone();
1415 if editor.read(cx).is_focused(window) {
1416 Some(editor.clone())
1417 } else {
1418 None
1419 }
1420 });
1421
1422 if let Some(context_editor) = context_editor {
1423 Some(InlineAssistTarget::Editor(context_editor))
1424 } else if let Some(workspace_editor) = workspace
1425 .active_item(cx)
1426 .and_then(|item| item.act_as::<Editor>(cx))
1427 {
1428 Some(InlineAssistTarget::Editor(workspace_editor))
1429 } else if let Some(terminal_view) = workspace
1430 .active_item(cx)
1431 .and_then(|item| item.act_as::<TerminalView>(cx))
1432 {
1433 Some(InlineAssistTarget::Terminal(terminal_view))
1434 } else {
1435 None
1436 }
1437 }
1438}
1439
1440struct EditorInlineAssists {
1441 assist_ids: Vec<InlineAssistId>,
1442 scroll_lock: Option<InlineAssistScrollLock>,
1443 highlight_updates: async_watch::Sender<()>,
1444 _update_highlights: Task<Result<()>>,
1445 _subscriptions: Vec<gpui::Subscription>,
1446}
1447
1448struct InlineAssistScrollLock {
1449 assist_id: InlineAssistId,
1450 distance_from_top: f32,
1451}
1452
1453impl EditorInlineAssists {
1454 #[allow(clippy::too_many_arguments)]
1455 fn new(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) -> Self {
1456 let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
1457 Self {
1458 assist_ids: Vec::new(),
1459 scroll_lock: None,
1460 highlight_updates: highlight_updates_tx,
1461 _update_highlights: cx.spawn(|cx| {
1462 let editor = editor.downgrade();
1463 async move {
1464 while let Ok(()) = highlight_updates_rx.changed().await {
1465 let editor = editor.upgrade().context("editor was dropped")?;
1466 cx.update_global(|assistant: &mut InlineAssistant, cx| {
1467 assistant.update_editor_highlights(&editor, cx);
1468 })?;
1469 }
1470 Ok(())
1471 }
1472 }),
1473 _subscriptions: vec![
1474 cx.observe_release_in(editor, window, {
1475 let editor = editor.downgrade();
1476 |_, window, cx| {
1477 InlineAssistant::update_global(cx, |this, cx| {
1478 this.handle_editor_release(editor, window, cx);
1479 })
1480 }
1481 }),
1482 window.observe(editor, cx, move |editor, window, cx| {
1483 InlineAssistant::update_global(cx, |this, cx| {
1484 this.handle_editor_change(editor, window, cx)
1485 })
1486 }),
1487 window.subscribe(editor, cx, move |editor, event, window, cx| {
1488 InlineAssistant::update_global(cx, |this, cx| {
1489 this.handle_editor_event(editor, event, window, cx)
1490 })
1491 }),
1492 editor.update(cx, |editor, cx| {
1493 let editor_handle = cx.entity().downgrade();
1494 editor.register_action(move |_: &editor::actions::Newline, window, cx| {
1495 InlineAssistant::update_global(cx, |this, cx| {
1496 if let Some(editor) = editor_handle.upgrade() {
1497 this.handle_editor_newline(editor, window, cx)
1498 }
1499 })
1500 })
1501 }),
1502 editor.update(cx, |editor, cx| {
1503 let editor_handle = cx.entity().downgrade();
1504 editor.register_action(move |_: &editor::actions::Cancel, window, cx| {
1505 InlineAssistant::update_global(cx, |this, cx| {
1506 if let Some(editor) = editor_handle.upgrade() {
1507 this.handle_editor_cancel(editor, window, cx)
1508 }
1509 })
1510 })
1511 }),
1512 ],
1513 }
1514 }
1515}
1516
1517struct InlineAssistGroup {
1518 assist_ids: Vec<InlineAssistId>,
1519 linked: bool,
1520 active_assist_id: Option<InlineAssistId>,
1521}
1522
1523impl InlineAssistGroup {
1524 fn new() -> Self {
1525 Self {
1526 assist_ids: Vec::new(),
1527 linked: true,
1528 active_assist_id: None,
1529 }
1530 }
1531}
1532
1533fn build_assist_editor_renderer(editor: &Entity<PromptEditor<BufferCodegen>>) -> RenderBlock {
1534 let editor = editor.clone();
1535
1536 Arc::new(move |cx: &mut BlockContext| {
1537 let gutter_dimensions = editor.read(cx).gutter_dimensions();
1538
1539 *gutter_dimensions.lock() = *cx.gutter_dimensions;
1540 editor.clone().into_any_element()
1541 })
1542}
1543
1544#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1545struct InlineAssistGroupId(usize);
1546
1547impl InlineAssistGroupId {
1548 fn post_inc(&mut self) -> InlineAssistGroupId {
1549 let id = *self;
1550 self.0 += 1;
1551 id
1552 }
1553}
1554
1555pub struct InlineAssist {
1556 group_id: InlineAssistGroupId,
1557 range: Range<Anchor>,
1558 editor: WeakEntity<Editor>,
1559 decorations: Option<InlineAssistDecorations>,
1560 codegen: Entity<BufferCodegen>,
1561 _subscriptions: Vec<Subscription>,
1562 workspace: WeakEntity<Workspace>,
1563}
1564
1565impl InlineAssist {
1566 #[allow(clippy::too_many_arguments)]
1567 fn new(
1568 assist_id: InlineAssistId,
1569 group_id: InlineAssistGroupId,
1570 editor: &Entity<Editor>,
1571 prompt_editor: &Entity<PromptEditor<BufferCodegen>>,
1572 prompt_block_id: CustomBlockId,
1573 end_block_id: CustomBlockId,
1574 range: Range<Anchor>,
1575 codegen: Entity<BufferCodegen>,
1576 workspace: WeakEntity<Workspace>,
1577 window: &mut Window,
1578 cx: &mut App,
1579 ) -> Self {
1580 let prompt_editor_focus_handle = prompt_editor.focus_handle(cx);
1581 InlineAssist {
1582 group_id,
1583 editor: editor.downgrade(),
1584 decorations: Some(InlineAssistDecorations {
1585 prompt_block_id,
1586 prompt_editor: prompt_editor.clone(),
1587 removed_line_block_ids: HashSet::default(),
1588 end_block_id,
1589 }),
1590 range,
1591 codegen: codegen.clone(),
1592 workspace: workspace.clone(),
1593 _subscriptions: vec![
1594 window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| {
1595 InlineAssistant::update_global(cx, |this, cx| {
1596 this.handle_prompt_editor_focus_in(assist_id, cx)
1597 })
1598 }),
1599 window.on_focus_out(&prompt_editor_focus_handle, cx, move |_, _, cx| {
1600 InlineAssistant::update_global(cx, |this, cx| {
1601 this.handle_prompt_editor_focus_out(assist_id, cx)
1602 })
1603 }),
1604 window.subscribe(prompt_editor, cx, |prompt_editor, event, window, cx| {
1605 InlineAssistant::update_global(cx, |this, cx| {
1606 this.handle_prompt_editor_event(prompt_editor, event, window, cx)
1607 })
1608 }),
1609 window.observe(&codegen, cx, {
1610 let editor = editor.downgrade();
1611 move |_, window, cx| {
1612 if let Some(editor) = editor.upgrade() {
1613 InlineAssistant::update_global(cx, |this, cx| {
1614 if let Some(editor_assists) =
1615 this.assists_by_editor.get(&editor.downgrade())
1616 {
1617 editor_assists.highlight_updates.send(()).ok();
1618 }
1619
1620 this.update_editor_blocks(&editor, assist_id, window, cx);
1621 })
1622 }
1623 }
1624 }),
1625 window.subscribe(&codegen, cx, move |codegen, event, window, cx| {
1626 InlineAssistant::update_global(cx, |this, cx| match event {
1627 CodegenEvent::Undone => this.finish_assist(assist_id, false, window, cx),
1628 CodegenEvent::Finished => {
1629 let assist = if let Some(assist) = this.assists.get(&assist_id) {
1630 assist
1631 } else {
1632 return;
1633 };
1634
1635 if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) {
1636 if assist.decorations.is_none() {
1637 if let Some(workspace) = assist.workspace.upgrade() {
1638 let error = format!("Inline assistant error: {}", error);
1639 workspace.update(cx, |workspace, cx| {
1640 struct InlineAssistantError;
1641
1642 let id =
1643 NotificationId::composite::<InlineAssistantError>(
1644 assist_id.0,
1645 );
1646
1647 workspace.show_toast(Toast::new(id, error), cx);
1648 })
1649 }
1650 }
1651 }
1652
1653 if assist.decorations.is_none() {
1654 this.finish_assist(assist_id, false, window, cx);
1655 }
1656 }
1657 })
1658 }),
1659 ],
1660 }
1661 }
1662
1663 fn user_prompt(&self, cx: &App) -> Option<String> {
1664 let decorations = self.decorations.as_ref()?;
1665 Some(decorations.prompt_editor.read(cx).prompt(cx))
1666 }
1667}
1668
1669struct InlineAssistDecorations {
1670 prompt_block_id: CustomBlockId,
1671 prompt_editor: Entity<PromptEditor<BufferCodegen>>,
1672 removed_line_block_ids: HashSet<CustomBlockId>,
1673 end_block_id: CustomBlockId,
1674}
1675
1676struct AssistantCodeActionProvider {
1677 editor: WeakEntity<Editor>,
1678 workspace: WeakEntity<Workspace>,
1679 thread_store: Option<WeakEntity<ThreadStore>>,
1680}
1681
1682const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2";
1683
1684impl CodeActionProvider for AssistantCodeActionProvider {
1685 fn id(&self) -> Arc<str> {
1686 ASSISTANT_CODE_ACTION_PROVIDER_ID.into()
1687 }
1688
1689 fn code_actions(
1690 &self,
1691 buffer: &Entity<Buffer>,
1692 range: Range<text::Anchor>,
1693 _: &mut Window,
1694 cx: &mut App,
1695 ) -> Task<Result<Vec<CodeAction>>> {
1696 if !AssistantSettings::get_global(cx).enabled {
1697 return Task::ready(Ok(Vec::new()));
1698 }
1699
1700 let snapshot = buffer.read(cx).snapshot();
1701 let mut range = range.to_point(&snapshot);
1702
1703 // Expand the range to line boundaries.
1704 range.start.column = 0;
1705 range.end.column = snapshot.line_len(range.end.row);
1706
1707 let mut has_diagnostics = false;
1708 for diagnostic in snapshot.diagnostics_in_range::<_, Point>(range.clone(), false) {
1709 range.start = cmp::min(range.start, diagnostic.range.start);
1710 range.end = cmp::max(range.end, diagnostic.range.end);
1711 has_diagnostics = true;
1712 }
1713 if has_diagnostics {
1714 if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) {
1715 if let Some(symbol) = symbols_containing_start.last() {
1716 range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
1717 range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
1718 }
1719 }
1720
1721 if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None) {
1722 if let Some(symbol) = symbols_containing_end.last() {
1723 range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
1724 range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
1725 }
1726 }
1727
1728 Task::ready(Ok(vec![CodeAction {
1729 server_id: language::LanguageServerId(0),
1730 range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
1731 lsp_action: ActionVariant::Action(Box::new(lsp::CodeAction {
1732 title: "Fix with Assistant".into(),
1733 ..Default::default()
1734 })),
1735 }]))
1736 } else {
1737 Task::ready(Ok(Vec::new()))
1738 }
1739 }
1740
1741 fn apply_code_action(
1742 &self,
1743 buffer: Entity<Buffer>,
1744 action: CodeAction,
1745 excerpt_id: ExcerptId,
1746 _push_to_history: bool,
1747 window: &mut Window,
1748 cx: &mut App,
1749 ) -> Task<Result<ProjectTransaction>> {
1750 let editor = self.editor.clone();
1751 let workspace = self.workspace.clone();
1752 let thread_store = self.thread_store.clone();
1753 window.spawn(cx, |mut cx| async move {
1754 let editor = editor.upgrade().context("editor was released")?;
1755 let range = editor
1756 .update(&mut cx, |editor, cx| {
1757 editor.buffer().update(cx, |multibuffer, cx| {
1758 let buffer = buffer.read(cx);
1759 let multibuffer_snapshot = multibuffer.read(cx);
1760
1761 let old_context_range =
1762 multibuffer_snapshot.context_range_for_excerpt(excerpt_id)?;
1763 let mut new_context_range = old_context_range.clone();
1764 if action
1765 .range
1766 .start
1767 .cmp(&old_context_range.start, buffer)
1768 .is_lt()
1769 {
1770 new_context_range.start = action.range.start;
1771 }
1772 if action.range.end.cmp(&old_context_range.end, buffer).is_gt() {
1773 new_context_range.end = action.range.end;
1774 }
1775 drop(multibuffer_snapshot);
1776
1777 if new_context_range != old_context_range {
1778 multibuffer.resize_excerpt(excerpt_id, new_context_range, cx);
1779 }
1780
1781 let multibuffer_snapshot = multibuffer.read(cx);
1782 Some(
1783 multibuffer_snapshot
1784 .anchor_in_excerpt(excerpt_id, action.range.start)?
1785 ..multibuffer_snapshot
1786 .anchor_in_excerpt(excerpt_id, action.range.end)?,
1787 )
1788 })
1789 })?
1790 .context("invalid range")?;
1791
1792 cx.update_global(|assistant: &mut InlineAssistant, window, cx| {
1793 let assist_id = assistant.suggest_assist(
1794 &editor,
1795 range,
1796 "Fix Diagnostics".into(),
1797 None,
1798 true,
1799 workspace,
1800 thread_store,
1801 window,
1802 cx,
1803 );
1804 assistant.start_assist(assist_id, window, cx);
1805 })?;
1806
1807 Ok(ProjectTransaction::default())
1808 })
1809 }
1810}
1811
1812fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
1813 ranges.sort_unstable_by(|a, b| {
1814 a.start
1815 .cmp(&b.start, buffer)
1816 .then_with(|| b.end.cmp(&a.end, buffer))
1817 });
1818
1819 let mut ix = 0;
1820 while ix + 1 < ranges.len() {
1821 let b = ranges[ix + 1].clone();
1822 let a = &mut ranges[ix];
1823 if a.end.cmp(&b.start, buffer).is_gt() {
1824 if a.end.cmp(&b.end, buffer).is_lt() {
1825 a.end = b.end;
1826 }
1827 ranges.remove(ix + 1);
1828 } else {
1829 ix += 1;
1830 }
1831 }
1832}