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