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