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.asssistant_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());
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());
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 ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
875 ranges
876 .first()
877 .and_then(|(buffer, _, _)| buffer.read(cx).language())
878 .map(|language| language.name())
879 });
880 report_assistant_event(
881 AssistantEvent {
882 conversation_id: None,
883 kind: AssistantKind::Inline,
884 message_id,
885 phase: if undo {
886 AssistantPhase::Rejected
887 } else {
888 AssistantPhase::Accepted
889 },
890 model: model.telemetry_id(),
891 model_provider: model.provider_id().to_string(),
892 response_latency: None,
893 error_message: None,
894 language_name: language_name.map(|name| name.to_proto()),
895 },
896 Some(self.telemetry.clone()),
897 cx.http_client(),
898 model.api_key(cx),
899 cx.background_executor(),
900 );
901 }
902
903 if undo {
904 assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
905 } else {
906 self.confirmed_assists.insert(assist_id, active_alternative);
907 }
908 }
909 }
910
911 fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
912 let Some(assist) = self.assists.get_mut(&assist_id) else {
913 return false;
914 };
915 let Some(editor) = assist.editor.upgrade() else {
916 return false;
917 };
918 let Some(decorations) = assist.decorations.take() else {
919 return false;
920 };
921
922 editor.update(cx, |editor, cx| {
923 let mut to_remove = decorations.removed_line_block_ids;
924 to_remove.insert(decorations.prompt_block_id);
925 to_remove.insert(decorations.end_block_id);
926 editor.remove_blocks(to_remove, None, cx);
927 });
928
929 if decorations
930 .prompt_editor
931 .focus_handle(cx)
932 .contains_focused(cx)
933 {
934 self.focus_next_assist(assist_id, cx);
935 }
936
937 if let Some(editor_assists) = self.assists_by_editor.get_mut(&editor.downgrade()) {
938 if editor_assists
939 .scroll_lock
940 .as_ref()
941 .map_or(false, |lock| lock.assist_id == assist_id)
942 {
943 editor_assists.scroll_lock = None;
944 }
945 editor_assists.highlight_updates.send(()).ok();
946 }
947
948 true
949 }
950
951 fn focus_next_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
952 let Some(assist) = self.assists.get(&assist_id) else {
953 return;
954 };
955
956 let assist_group = &self.assist_groups[&assist.group_id];
957 let assist_ix = assist_group
958 .assist_ids
959 .iter()
960 .position(|id| *id == assist_id)
961 .unwrap();
962 let assist_ids = assist_group
963 .assist_ids
964 .iter()
965 .skip(assist_ix + 1)
966 .chain(assist_group.assist_ids.iter().take(assist_ix));
967
968 for assist_id in assist_ids {
969 let assist = &self.assists[assist_id];
970 if assist.decorations.is_some() {
971 self.focus_assist(*assist_id, cx);
972 return;
973 }
974 }
975
976 assist.editor.update(cx, |editor, cx| editor.focus(cx)).ok();
977 }
978
979 fn focus_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
980 let Some(assist) = self.assists.get(&assist_id) else {
981 return;
982 };
983
984 if let Some(decorations) = assist.decorations.as_ref() {
985 decorations.prompt_editor.update(cx, |prompt_editor, cx| {
986 prompt_editor.editor.update(cx, |editor, cx| {
987 editor.focus(cx);
988 editor.select_all(&SelectAll, cx);
989 })
990 });
991 }
992
993 self.scroll_to_assist(assist_id, cx);
994 }
995
996 pub fn scroll_to_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
997 let Some(assist) = self.assists.get(&assist_id) else {
998 return;
999 };
1000 let Some(editor) = assist.editor.upgrade() else {
1001 return;
1002 };
1003
1004 let position = assist.range.start;
1005 editor.update(cx, |editor, cx| {
1006 editor.change_selections(None, cx, |selections| {
1007 selections.select_anchor_ranges([position..position])
1008 });
1009
1010 let mut scroll_target_top;
1011 let mut scroll_target_bottom;
1012 if let Some(decorations) = assist.decorations.as_ref() {
1013 scroll_target_top = editor
1014 .row_for_block(decorations.prompt_block_id, cx)
1015 .unwrap()
1016 .0 as f32;
1017 scroll_target_bottom = editor
1018 .row_for_block(decorations.end_block_id, cx)
1019 .unwrap()
1020 .0 as f32;
1021 } else {
1022 let snapshot = editor.snapshot(cx);
1023 let start_row = assist
1024 .range
1025 .start
1026 .to_display_point(&snapshot.display_snapshot)
1027 .row();
1028 scroll_target_top = start_row.0 as f32;
1029 scroll_target_bottom = scroll_target_top + 1.;
1030 }
1031 scroll_target_top -= editor.vertical_scroll_margin() as f32;
1032 scroll_target_bottom += editor.vertical_scroll_margin() as f32;
1033
1034 let height_in_lines = editor.visible_line_count().unwrap_or(0.);
1035 let scroll_top = editor.scroll_position(cx).y;
1036 let scroll_bottom = scroll_top + height_in_lines;
1037
1038 if scroll_target_top < scroll_top {
1039 editor.set_scroll_position(point(0., scroll_target_top), cx);
1040 } else if scroll_target_bottom > scroll_bottom {
1041 if (scroll_target_bottom - scroll_target_top) <= height_in_lines {
1042 editor
1043 .set_scroll_position(point(0., scroll_target_bottom - height_in_lines), cx);
1044 } else {
1045 editor.set_scroll_position(point(0., scroll_target_top), cx);
1046 }
1047 }
1048 });
1049 }
1050
1051 fn unlink_assist_group(
1052 &mut self,
1053 assist_group_id: InlineAssistGroupId,
1054 cx: &mut WindowContext,
1055 ) -> Vec<InlineAssistId> {
1056 let assist_group = self.assist_groups.get_mut(&assist_group_id).unwrap();
1057 assist_group.linked = false;
1058 for assist_id in &assist_group.assist_ids {
1059 let assist = self.assists.get_mut(assist_id).unwrap();
1060 if let Some(editor_decorations) = assist.decorations.as_ref() {
1061 editor_decorations
1062 .prompt_editor
1063 .update(cx, |prompt_editor, cx| prompt_editor.unlink(cx));
1064 }
1065 }
1066 assist_group.assist_ids.clone()
1067 }
1068
1069 pub fn start_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
1070 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
1071 assist
1072 } else {
1073 return;
1074 };
1075
1076 let assist_group_id = assist.group_id;
1077 if self.assist_groups[&assist_group_id].linked {
1078 for assist_id in self.unlink_assist_group(assist_group_id, cx) {
1079 self.start_assist(assist_id, cx);
1080 }
1081 return;
1082 }
1083
1084 let Some(user_prompt) = assist.user_prompt(cx) else {
1085 return;
1086 };
1087
1088 self.prompt_history.retain(|prompt| *prompt != user_prompt);
1089 self.prompt_history.push_back(user_prompt.clone());
1090 if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
1091 self.prompt_history.pop_front();
1092 }
1093
1094 assist
1095 .codegen
1096 .update(cx, |codegen, cx| codegen.start(user_prompt, cx))
1097 .log_err();
1098 }
1099
1100 pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
1101 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
1102 assist
1103 } else {
1104 return;
1105 };
1106
1107 assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
1108 }
1109
1110 fn update_editor_highlights(&self, editor: &View<Editor>, cx: &mut WindowContext) {
1111 let mut gutter_pending_ranges = Vec::new();
1112 let mut gutter_transformed_ranges = Vec::new();
1113 let mut foreground_ranges = Vec::new();
1114 let mut inserted_row_ranges = Vec::new();
1115 let empty_assist_ids = Vec::new();
1116 let assist_ids = self
1117 .assists_by_editor
1118 .get(&editor.downgrade())
1119 .map_or(&empty_assist_ids, |editor_assists| {
1120 &editor_assists.assist_ids
1121 });
1122
1123 for assist_id in assist_ids {
1124 if let Some(assist) = self.assists.get(assist_id) {
1125 let codegen = assist.codegen.read(cx);
1126 let buffer = codegen.buffer(cx).read(cx).read(cx);
1127 foreground_ranges.extend(codegen.last_equal_ranges(cx).iter().cloned());
1128
1129 let pending_range =
1130 codegen.edit_position(cx).unwrap_or(assist.range.start)..assist.range.end;
1131 if pending_range.end.to_offset(&buffer) > pending_range.start.to_offset(&buffer) {
1132 gutter_pending_ranges.push(pending_range);
1133 }
1134
1135 if let Some(edit_position) = codegen.edit_position(cx) {
1136 let edited_range = assist.range.start..edit_position;
1137 if edited_range.end.to_offset(&buffer) > edited_range.start.to_offset(&buffer) {
1138 gutter_transformed_ranges.push(edited_range);
1139 }
1140 }
1141
1142 if assist.decorations.is_some() {
1143 inserted_row_ranges
1144 .extend(codegen.diff(cx).inserted_row_ranges.iter().cloned());
1145 }
1146 }
1147 }
1148
1149 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
1150 merge_ranges(&mut foreground_ranges, &snapshot);
1151 merge_ranges(&mut gutter_pending_ranges, &snapshot);
1152 merge_ranges(&mut gutter_transformed_ranges, &snapshot);
1153 editor.update(cx, |editor, cx| {
1154 enum GutterPendingRange {}
1155 if gutter_pending_ranges.is_empty() {
1156 editor.clear_gutter_highlights::<GutterPendingRange>(cx);
1157 } else {
1158 editor.highlight_gutter::<GutterPendingRange>(
1159 &gutter_pending_ranges,
1160 |cx| cx.theme().status().info_background,
1161 cx,
1162 )
1163 }
1164
1165 enum GutterTransformedRange {}
1166 if gutter_transformed_ranges.is_empty() {
1167 editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
1168 } else {
1169 editor.highlight_gutter::<GutterTransformedRange>(
1170 &gutter_transformed_ranges,
1171 |cx| cx.theme().status().info,
1172 cx,
1173 )
1174 }
1175
1176 if foreground_ranges.is_empty() {
1177 editor.clear_highlights::<InlineAssist>(cx);
1178 } else {
1179 editor.highlight_text::<InlineAssist>(
1180 foreground_ranges,
1181 HighlightStyle {
1182 fade_out: Some(0.6),
1183 ..Default::default()
1184 },
1185 cx,
1186 );
1187 }
1188
1189 editor.clear_row_highlights::<InlineAssist>();
1190 for row_range in inserted_row_ranges {
1191 editor.highlight_rows::<InlineAssist>(
1192 row_range,
1193 cx.theme().status().info_background,
1194 false,
1195 cx,
1196 );
1197 }
1198 });
1199 }
1200
1201 fn update_editor_blocks(
1202 &mut self,
1203 editor: &View<Editor>,
1204 assist_id: InlineAssistId,
1205 cx: &mut WindowContext,
1206 ) {
1207 let Some(assist) = self.assists.get_mut(&assist_id) else {
1208 return;
1209 };
1210 let Some(decorations) = assist.decorations.as_mut() else {
1211 return;
1212 };
1213
1214 let codegen = assist.codegen.read(cx);
1215 let old_snapshot = codegen.snapshot(cx);
1216 let old_buffer = codegen.old_buffer(cx);
1217 let deleted_row_ranges = codegen.diff(cx).deleted_row_ranges.clone();
1218
1219 editor.update(cx, |editor, cx| {
1220 let old_blocks = mem::take(&mut decorations.removed_line_block_ids);
1221 editor.remove_blocks(old_blocks, None, cx);
1222
1223 let mut new_blocks = Vec::new();
1224 for (new_row, old_row_range) in deleted_row_ranges {
1225 let (_, buffer_start) = old_snapshot
1226 .point_to_buffer_offset(Point::new(*old_row_range.start(), 0))
1227 .unwrap();
1228 let (_, buffer_end) = old_snapshot
1229 .point_to_buffer_offset(Point::new(
1230 *old_row_range.end(),
1231 old_snapshot.line_len(MultiBufferRow(*old_row_range.end())),
1232 ))
1233 .unwrap();
1234
1235 let deleted_lines_editor = cx.new_view(|cx| {
1236 let multi_buffer = cx.new_model(|_| {
1237 MultiBuffer::without_headers(language::Capability::ReadOnly)
1238 });
1239 multi_buffer.update(cx, |multi_buffer, cx| {
1240 multi_buffer.push_excerpts(
1241 old_buffer.clone(),
1242 Some(ExcerptRange {
1243 context: buffer_start..buffer_end,
1244 primary: None,
1245 }),
1246 cx,
1247 );
1248 });
1249
1250 enum DeletedLines {}
1251 let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
1252 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
1253 editor.set_show_wrap_guides(false, cx);
1254 editor.set_show_gutter(false, cx);
1255 editor.scroll_manager.set_forbid_vertical_scroll(true);
1256 editor.set_read_only(true);
1257 editor.set_show_inline_completions(Some(false), cx);
1258 editor.highlight_rows::<DeletedLines>(
1259 Anchor::min()..Anchor::max(),
1260 cx.theme().status().deleted_background,
1261 false,
1262 cx,
1263 );
1264 editor
1265 });
1266
1267 let height =
1268 deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
1269 new_blocks.push(BlockProperties {
1270 placement: BlockPlacement::Above(new_row),
1271 height,
1272 style: BlockStyle::Flex,
1273 render: Arc::new(move |cx| {
1274 div()
1275 .block_mouse_down()
1276 .bg(cx.theme().status().deleted_background)
1277 .size_full()
1278 .h(height as f32 * cx.line_height())
1279 .pl(cx.gutter_dimensions.full_width())
1280 .child(deleted_lines_editor.clone())
1281 .into_any_element()
1282 }),
1283 priority: 0,
1284 });
1285 }
1286
1287 decorations.removed_line_block_ids = editor
1288 .insert_blocks(new_blocks, None, cx)
1289 .into_iter()
1290 .collect();
1291 })
1292 }
1293
1294 fn resolve_inline_assist_target(
1295 workspace: &mut Workspace,
1296 cx: &mut WindowContext,
1297 ) -> Option<InlineAssistTarget> {
1298 if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
1299 if terminal_panel
1300 .read(cx)
1301 .focus_handle(cx)
1302 .contains_focused(cx)
1303 {
1304 if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
1305 pane.read(cx)
1306 .active_item()
1307 .and_then(|t| t.downcast::<TerminalView>())
1308 }) {
1309 return Some(InlineAssistTarget::Terminal(terminal_view));
1310 }
1311 }
1312 }
1313
1314 if let Some(workspace_editor) = workspace
1315 .active_item(cx)
1316 .and_then(|item| item.act_as::<Editor>(cx))
1317 {
1318 Some(InlineAssistTarget::Editor(workspace_editor))
1319 } else if let Some(terminal_view) = workspace
1320 .active_item(cx)
1321 .and_then(|item| item.act_as::<TerminalView>(cx))
1322 {
1323 Some(InlineAssistTarget::Terminal(terminal_view))
1324 } else {
1325 None
1326 }
1327 }
1328}
1329
1330struct EditorInlineAssists {
1331 assist_ids: Vec<InlineAssistId>,
1332 scroll_lock: Option<InlineAssistScrollLock>,
1333 highlight_updates: async_watch::Sender<()>,
1334 _update_highlights: Task<Result<()>>,
1335 _subscriptions: Vec<gpui::Subscription>,
1336}
1337
1338struct InlineAssistScrollLock {
1339 assist_id: InlineAssistId,
1340 distance_from_top: f32,
1341}
1342
1343impl EditorInlineAssists {
1344 #[allow(clippy::too_many_arguments)]
1345 fn new(editor: &View<Editor>, cx: &mut WindowContext) -> Self {
1346 let (highlight_updates_tx, mut highlight_updates_rx) = async_watch::channel(());
1347 Self {
1348 assist_ids: Vec::new(),
1349 scroll_lock: None,
1350 highlight_updates: highlight_updates_tx,
1351 _update_highlights: cx.spawn(|mut cx| {
1352 let editor = editor.downgrade();
1353 async move {
1354 while let Ok(()) = highlight_updates_rx.changed().await {
1355 let editor = editor.upgrade().context("editor was dropped")?;
1356 cx.update_global(|assistant: &mut InlineAssistant, cx| {
1357 assistant.update_editor_highlights(&editor, cx);
1358 })?;
1359 }
1360 Ok(())
1361 }
1362 }),
1363 _subscriptions: vec![
1364 cx.observe_release(editor, {
1365 let editor = editor.downgrade();
1366 |_, cx| {
1367 InlineAssistant::update_global(cx, |this, cx| {
1368 this.handle_editor_release(editor, cx);
1369 })
1370 }
1371 }),
1372 cx.observe(editor, move |editor, cx| {
1373 InlineAssistant::update_global(cx, |this, cx| {
1374 this.handle_editor_change(editor, cx)
1375 })
1376 }),
1377 cx.subscribe(editor, move |editor, event, cx| {
1378 InlineAssistant::update_global(cx, |this, cx| {
1379 this.handle_editor_event(editor, event, cx)
1380 })
1381 }),
1382 editor.update(cx, |editor, cx| {
1383 let editor_handle = cx.view().downgrade();
1384 editor.register_action(
1385 move |_: &editor::actions::Newline, cx: &mut WindowContext| {
1386 InlineAssistant::update_global(cx, |this, cx| {
1387 if let Some(editor) = editor_handle.upgrade() {
1388 this.handle_editor_newline(editor, cx)
1389 }
1390 })
1391 },
1392 )
1393 }),
1394 editor.update(cx, |editor, cx| {
1395 let editor_handle = cx.view().downgrade();
1396 editor.register_action(
1397 move |_: &editor::actions::Cancel, cx: &mut WindowContext| {
1398 InlineAssistant::update_global(cx, |this, cx| {
1399 if let Some(editor) = editor_handle.upgrade() {
1400 this.handle_editor_cancel(editor, cx)
1401 }
1402 })
1403 },
1404 )
1405 }),
1406 ],
1407 }
1408 }
1409}
1410
1411struct InlineAssistGroup {
1412 assist_ids: Vec<InlineAssistId>,
1413 linked: bool,
1414 active_assist_id: Option<InlineAssistId>,
1415}
1416
1417impl InlineAssistGroup {
1418 fn new() -> Self {
1419 Self {
1420 assist_ids: Vec::new(),
1421 linked: true,
1422 active_assist_id: None,
1423 }
1424 }
1425}
1426
1427fn build_assist_editor_renderer(editor: &View<PromptEditor<BufferCodegen>>) -> RenderBlock {
1428 let editor = editor.clone();
1429
1430 Arc::new(move |cx: &mut BlockContext| {
1431 let gutter_dimensions = editor.read(cx).gutter_dimensions();
1432
1433 *gutter_dimensions.lock() = *cx.gutter_dimensions;
1434 editor.clone().into_any_element()
1435 })
1436}
1437
1438#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1439struct InlineAssistGroupId(usize);
1440
1441impl InlineAssistGroupId {
1442 fn post_inc(&mut self) -> InlineAssistGroupId {
1443 let id = *self;
1444 self.0 += 1;
1445 id
1446 }
1447}
1448
1449pub struct InlineAssist {
1450 group_id: InlineAssistGroupId,
1451 range: Range<Anchor>,
1452 editor: WeakView<Editor>,
1453 decorations: Option<InlineAssistDecorations>,
1454 codegen: Model<BufferCodegen>,
1455 _subscriptions: Vec<Subscription>,
1456 workspace: WeakView<Workspace>,
1457}
1458
1459impl InlineAssist {
1460 #[allow(clippy::too_many_arguments)]
1461 fn new(
1462 assist_id: InlineAssistId,
1463 group_id: InlineAssistGroupId,
1464 editor: &View<Editor>,
1465 prompt_editor: &View<PromptEditor<BufferCodegen>>,
1466 prompt_block_id: CustomBlockId,
1467 end_block_id: CustomBlockId,
1468 range: Range<Anchor>,
1469 codegen: Model<BufferCodegen>,
1470 workspace: WeakView<Workspace>,
1471 cx: &mut WindowContext,
1472 ) -> Self {
1473 let prompt_editor_focus_handle = prompt_editor.focus_handle(cx);
1474 InlineAssist {
1475 group_id,
1476 editor: editor.downgrade(),
1477 decorations: Some(InlineAssistDecorations {
1478 prompt_block_id,
1479 prompt_editor: prompt_editor.clone(),
1480 removed_line_block_ids: HashSet::default(),
1481 end_block_id,
1482 }),
1483 range,
1484 codegen: codegen.clone(),
1485 workspace: workspace.clone(),
1486 _subscriptions: vec![
1487 cx.on_focus_in(&prompt_editor_focus_handle, move |cx| {
1488 InlineAssistant::update_global(cx, |this, cx| {
1489 this.handle_prompt_editor_focus_in(assist_id, cx)
1490 })
1491 }),
1492 cx.on_focus_out(&prompt_editor_focus_handle, move |_, cx| {
1493 InlineAssistant::update_global(cx, |this, cx| {
1494 this.handle_prompt_editor_focus_out(assist_id, cx)
1495 })
1496 }),
1497 cx.subscribe(prompt_editor, |prompt_editor, event, cx| {
1498 InlineAssistant::update_global(cx, |this, cx| {
1499 this.handle_prompt_editor_event(prompt_editor, event, cx)
1500 })
1501 }),
1502 cx.observe(&codegen, {
1503 let editor = editor.downgrade();
1504 move |_, cx| {
1505 if let Some(editor) = editor.upgrade() {
1506 InlineAssistant::update_global(cx, |this, cx| {
1507 if let Some(editor_assists) =
1508 this.assists_by_editor.get(&editor.downgrade())
1509 {
1510 editor_assists.highlight_updates.send(()).ok();
1511 }
1512
1513 this.update_editor_blocks(&editor, assist_id, cx);
1514 })
1515 }
1516 }
1517 }),
1518 cx.subscribe(&codegen, move |codegen, event, cx| {
1519 InlineAssistant::update_global(cx, |this, cx| match event {
1520 CodegenEvent::Undone => this.finish_assist(assist_id, false, cx),
1521 CodegenEvent::Finished => {
1522 let assist = if let Some(assist) = this.assists.get(&assist_id) {
1523 assist
1524 } else {
1525 return;
1526 };
1527
1528 if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) {
1529 if assist.decorations.is_none() {
1530 if let Some(workspace) = assist.workspace.upgrade() {
1531 let error = format!("Inline assistant error: {}", error);
1532 workspace.update(cx, |workspace, cx| {
1533 struct InlineAssistantError;
1534
1535 let id =
1536 NotificationId::composite::<InlineAssistantError>(
1537 assist_id.0,
1538 );
1539
1540 workspace.show_toast(Toast::new(id, error), cx);
1541 })
1542 }
1543 }
1544 }
1545
1546 if assist.decorations.is_none() {
1547 this.finish_assist(assist_id, false, cx);
1548 }
1549 }
1550 })
1551 }),
1552 ],
1553 }
1554 }
1555
1556 fn user_prompt(&self, cx: &AppContext) -> Option<String> {
1557 let decorations = self.decorations.as_ref()?;
1558 Some(decorations.prompt_editor.read(cx).prompt(cx))
1559 }
1560}
1561
1562struct InlineAssistDecorations {
1563 prompt_block_id: CustomBlockId,
1564 prompt_editor: View<PromptEditor<BufferCodegen>>,
1565 removed_line_block_ids: HashSet<CustomBlockId>,
1566 end_block_id: CustomBlockId,
1567}
1568
1569struct AssistantCodeActionProvider {
1570 editor: WeakView<Editor>,
1571 workspace: WeakView<Workspace>,
1572 thread_store: Option<WeakModel<ThreadStore>>,
1573}
1574
1575impl CodeActionProvider for AssistantCodeActionProvider {
1576 fn code_actions(
1577 &self,
1578 buffer: &Model<Buffer>,
1579 range: Range<text::Anchor>,
1580 cx: &mut WindowContext,
1581 ) -> Task<Result<Vec<CodeAction>>> {
1582 if !AssistantSettings::get_global(cx).enabled {
1583 return Task::ready(Ok(Vec::new()));
1584 }
1585
1586 let snapshot = buffer.read(cx).snapshot();
1587 let mut range = range.to_point(&snapshot);
1588
1589 // Expand the range to line boundaries.
1590 range.start.column = 0;
1591 range.end.column = snapshot.line_len(range.end.row);
1592
1593 let mut has_diagnostics = false;
1594 for diagnostic in snapshot.diagnostics_in_range::<_, Point>(range.clone(), false) {
1595 range.start = cmp::min(range.start, diagnostic.range.start);
1596 range.end = cmp::max(range.end, diagnostic.range.end);
1597 has_diagnostics = true;
1598 }
1599 if has_diagnostics {
1600 if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) {
1601 if let Some(symbol) = symbols_containing_start.last() {
1602 range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
1603 range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
1604 }
1605 }
1606
1607 if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None) {
1608 if let Some(symbol) = symbols_containing_end.last() {
1609 range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
1610 range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
1611 }
1612 }
1613
1614 Task::ready(Ok(vec![CodeAction {
1615 server_id: language::LanguageServerId(0),
1616 range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
1617 lsp_action: lsp::CodeAction {
1618 title: "Fix with Assistant".into(),
1619 ..Default::default()
1620 },
1621 }]))
1622 } else {
1623 Task::ready(Ok(Vec::new()))
1624 }
1625 }
1626
1627 fn apply_code_action(
1628 &self,
1629 buffer: Model<Buffer>,
1630 action: CodeAction,
1631 excerpt_id: ExcerptId,
1632 _push_to_history: bool,
1633 cx: &mut WindowContext,
1634 ) -> Task<Result<ProjectTransaction>> {
1635 let editor = self.editor.clone();
1636 let workspace = self.workspace.clone();
1637 let thread_store = self.thread_store.clone();
1638 cx.spawn(|mut cx| async move {
1639 let editor = editor.upgrade().context("editor was released")?;
1640 let range = editor
1641 .update(&mut cx, |editor, cx| {
1642 editor.buffer().update(cx, |multibuffer, cx| {
1643 let buffer = buffer.read(cx);
1644 let multibuffer_snapshot = multibuffer.read(cx);
1645
1646 let old_context_range =
1647 multibuffer_snapshot.context_range_for_excerpt(excerpt_id)?;
1648 let mut new_context_range = old_context_range.clone();
1649 if action
1650 .range
1651 .start
1652 .cmp(&old_context_range.start, buffer)
1653 .is_lt()
1654 {
1655 new_context_range.start = action.range.start;
1656 }
1657 if action.range.end.cmp(&old_context_range.end, buffer).is_gt() {
1658 new_context_range.end = action.range.end;
1659 }
1660 drop(multibuffer_snapshot);
1661
1662 if new_context_range != old_context_range {
1663 multibuffer.resize_excerpt(excerpt_id, new_context_range, cx);
1664 }
1665
1666 let multibuffer_snapshot = multibuffer.read(cx);
1667 Some(
1668 multibuffer_snapshot
1669 .anchor_in_excerpt(excerpt_id, action.range.start)?
1670 ..multibuffer_snapshot
1671 .anchor_in_excerpt(excerpt_id, action.range.end)?,
1672 )
1673 })
1674 })?
1675 .context("invalid range")?;
1676
1677 cx.update_global(|assistant: &mut InlineAssistant, cx| {
1678 let assist_id = assistant.suggest_assist(
1679 &editor,
1680 range,
1681 "Fix Diagnostics".into(),
1682 None,
1683 true,
1684 workspace,
1685 thread_store,
1686 cx,
1687 );
1688 assistant.start_assist(assist_id, cx);
1689 })?;
1690
1691 Ok(ProjectTransaction::default())
1692 })
1693 }
1694}
1695
1696fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
1697 ranges.sort_unstable_by(|a, b| {
1698 a.start
1699 .cmp(&b.start, buffer)
1700 .then_with(|| b.end.cmp(&a.end, buffer))
1701 });
1702
1703 let mut ix = 0;
1704 while ix + 1 < ranges.len() {
1705 let b = ranges[ix + 1].clone();
1706 let a = &mut ranges[ix];
1707 if a.end.cmp(&b.start, buffer).is_gt() {
1708 if a.end.cmp(&b.end, buffer).is_lt() {
1709 a.end = b.end;
1710 }
1711 ranges.remove(ix + 1);
1712 } else {
1713 ix += 1;
1714 }
1715 }
1716}