1use crate::{
2 prompts::generate_content_prompt, AssistantPanel, CompletionProvider, Hunk,
3 LanguageModelRequest, LanguageModelRequestMessage, Role, StreamingDiff,
4};
5use anyhow::Result;
6use client::telemetry::Telemetry;
7use collections::{hash_map, HashMap, HashSet, VecDeque};
8use editor::{
9 actions::{MoveDown, MoveUp},
10 display_map::{
11 BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
12 },
13 scroll::{Autoscroll, AutoscrollStrategy},
14 Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorStyle, ExcerptRange,
15 GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
16};
17use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
18use gpui::{
19 AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global,
20 HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View,
21 ViewContext, WeakView, WhiteSpace, WindowContext,
22};
23use language::{Buffer, Point, TransactionId};
24use multi_buffer::MultiBufferRow;
25use parking_lot::Mutex;
26use rope::Rope;
27use settings::Settings;
28use similar::TextDiff;
29use std::{
30 cmp, future, mem,
31 ops::{Range, RangeInclusive},
32 sync::Arc,
33 time::Instant,
34};
35use theme::ThemeSettings;
36use ui::{prelude::*, Tooltip};
37use util::RangeExt;
38use workspace::{notifications::NotificationId, Toast, Workspace};
39
40pub fn init(telemetry: Arc<Telemetry>, cx: &mut AppContext) {
41 cx.set_global(InlineAssistant::new(telemetry));
42}
43
44const PROMPT_HISTORY_MAX_LEN: usize = 20;
45
46pub struct InlineAssistant {
47 next_assist_id: InlineAssistId,
48 pending_assists: HashMap<InlineAssistId, PendingInlineAssist>,
49 pending_assist_ids_by_editor: HashMap<WeakView<Editor>, Vec<InlineAssistId>>,
50 prompt_history: VecDeque<String>,
51 telemetry: Option<Arc<Telemetry>>,
52}
53
54impl Global for InlineAssistant {}
55
56impl InlineAssistant {
57 pub fn new(telemetry: Arc<Telemetry>) -> Self {
58 Self {
59 next_assist_id: InlineAssistId::default(),
60 pending_assists: HashMap::default(),
61 pending_assist_ids_by_editor: HashMap::default(),
62 prompt_history: VecDeque::default(),
63 telemetry: Some(telemetry),
64 }
65 }
66
67 pub fn assist(
68 &mut self,
69 editor: &View<Editor>,
70 workspace: Option<WeakView<Workspace>>,
71 include_context: bool,
72 cx: &mut WindowContext,
73 ) {
74 let selection = editor.read(cx).selections.newest_anchor().clone();
75 if selection.start.excerpt_id != selection.end.excerpt_id {
76 return;
77 }
78 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
79
80 // Extend the selection to the start and the end of the line.
81 let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
82 if point_selection.end > point_selection.start {
83 point_selection.start.column = 0;
84 // If the selection ends at the start of the line, we don't want to include it.
85 if point_selection.end.column == 0 {
86 point_selection.end.row -= 1;
87 }
88 point_selection.end.column = snapshot.line_len(MultiBufferRow(point_selection.end.row));
89 }
90
91 let codegen_kind = if point_selection.start == point_selection.end {
92 CodegenKind::Generate {
93 position: snapshot.anchor_after(point_selection.start),
94 }
95 } else {
96 CodegenKind::Transform {
97 range: snapshot.anchor_before(point_selection.start)
98 ..snapshot.anchor_after(point_selection.end),
99 }
100 };
101
102 let assist_id = self.next_assist_id.post_inc();
103 let codegen = cx.new_model(|cx| {
104 Codegen::new(
105 editor.read(cx).buffer().clone(),
106 codegen_kind,
107 self.telemetry.clone(),
108 cx,
109 )
110 });
111
112 let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
113 let prompt_editor = cx.new_view(|cx| {
114 InlineAssistEditor::new(
115 assist_id,
116 gutter_dimensions.clone(),
117 self.prompt_history.clone(),
118 codegen.clone(),
119 workspace.clone(),
120 cx,
121 )
122 });
123 let (prompt_block_id, end_block_id) = editor.update(cx, |editor, cx| {
124 let start_anchor = snapshot.anchor_before(point_selection.start);
125 let end_anchor = snapshot.anchor_after(point_selection.end);
126 editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
127 selections.select_anchor_ranges([start_anchor..start_anchor])
128 });
129 let block_ids = editor.insert_blocks(
130 [
131 BlockProperties {
132 style: BlockStyle::Sticky,
133 position: start_anchor,
134 height: prompt_editor.read(cx).height_in_lines,
135 render: build_inline_assist_editor_renderer(
136 &prompt_editor,
137 gutter_dimensions,
138 ),
139 disposition: BlockDisposition::Above,
140 },
141 BlockProperties {
142 style: BlockStyle::Sticky,
143 position: end_anchor,
144 height: 1,
145 render: Box::new(|cx| {
146 v_flex()
147 .h_full()
148 .w_full()
149 .border_t_1()
150 .border_color(cx.theme().status().info_border)
151 .into_any_element()
152 }),
153 disposition: BlockDisposition::Below,
154 },
155 ],
156 Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)),
157 cx,
158 );
159 (block_ids[0], block_ids[1])
160 });
161
162 self.pending_assists.insert(
163 assist_id,
164 PendingInlineAssist {
165 include_context,
166 editor: editor.downgrade(),
167 editor_decorations: Some(PendingInlineAssistDecorations {
168 prompt_block_id,
169 prompt_editor: prompt_editor.clone(),
170 removed_line_block_ids: HashSet::default(),
171 end_block_id,
172 }),
173 codegen: codegen.clone(),
174 workspace,
175 _subscriptions: vec![
176 cx.subscribe(&prompt_editor, |inline_assist_editor, event, cx| {
177 InlineAssistant::update_global(cx, |this, cx| {
178 this.handle_inline_assistant_editor_event(
179 inline_assist_editor,
180 event,
181 cx,
182 )
183 })
184 }),
185 editor.update(cx, |editor, _cx| {
186 editor.register_action(
187 move |_: &editor::actions::Newline, cx: &mut WindowContext| {
188 InlineAssistant::update_global(cx, |this, cx| {
189 this.handle_editor_action(assist_id, false, cx)
190 })
191 },
192 )
193 }),
194 editor.update(cx, |editor, _cx| {
195 editor.register_action(
196 move |_: &editor::actions::Cancel, cx: &mut WindowContext| {
197 InlineAssistant::update_global(cx, |this, cx| {
198 this.handle_editor_action(assist_id, true, cx)
199 })
200 },
201 )
202 }),
203 cx.subscribe(editor, move |editor, event, cx| {
204 InlineAssistant::update_global(cx, |this, cx| {
205 this.handle_editor_event(assist_id, editor, event, cx)
206 })
207 }),
208 cx.observe(&codegen, {
209 let editor = editor.downgrade();
210 move |_, cx| {
211 if let Some(editor) = editor.upgrade() {
212 InlineAssistant::update_global(cx, |this, cx| {
213 this.update_editor_highlights(&editor, cx);
214 this.update_editor_blocks(&editor, assist_id, cx);
215 })
216 }
217 }
218 }),
219 cx.subscribe(&codegen, move |codegen, event, cx| {
220 InlineAssistant::update_global(cx, |this, cx| match event {
221 CodegenEvent::Undone => this.finish_inline_assist(assist_id, false, cx),
222 CodegenEvent::Finished => {
223 let pending_assist = if let Some(pending_assist) =
224 this.pending_assists.get(&assist_id)
225 {
226 pending_assist
227 } else {
228 return;
229 };
230
231 if let CodegenStatus::Error(error) = &codegen.read(cx).status {
232 if pending_assist.editor_decorations.is_none() {
233 if let Some(workspace) = pending_assist
234 .workspace
235 .as_ref()
236 .and_then(|workspace| workspace.upgrade())
237 {
238 let error =
239 format!("Inline assistant error: {}", error);
240 workspace.update(cx, |workspace, cx| {
241 struct InlineAssistantError;
242
243 let id = NotificationId::identified::<
244 InlineAssistantError,
245 >(
246 assist_id.0
247 );
248
249 workspace.show_toast(Toast::new(id, error), cx);
250 })
251 }
252 }
253 }
254
255 if pending_assist.editor_decorations.is_none() {
256 this.finish_inline_assist(assist_id, false, cx);
257 }
258 }
259 })
260 }),
261 ],
262 },
263 );
264
265 self.pending_assist_ids_by_editor
266 .entry(editor.downgrade())
267 .or_default()
268 .push(assist_id);
269 self.update_editor_highlights(editor, cx);
270 }
271
272 fn handle_inline_assistant_editor_event(
273 &mut self,
274 inline_assist_editor: View<InlineAssistEditor>,
275 event: &InlineAssistEditorEvent,
276 cx: &mut WindowContext,
277 ) {
278 let assist_id = inline_assist_editor.read(cx).id;
279 match event {
280 InlineAssistEditorEvent::StartRequested => {
281 self.start_inline_assist(assist_id, cx);
282 }
283 InlineAssistEditorEvent::StopRequested => {
284 self.stop_inline_assist(assist_id, cx);
285 }
286 InlineAssistEditorEvent::ConfirmRequested => {
287 self.finish_inline_assist(assist_id, false, cx);
288 }
289 InlineAssistEditorEvent::CancelRequested => {
290 self.finish_inline_assist(assist_id, true, cx);
291 }
292 InlineAssistEditorEvent::DismissRequested => {
293 self.dismiss_inline_assist(assist_id, cx);
294 }
295 InlineAssistEditorEvent::Resized { height_in_lines } => {
296 self.resize_inline_assist(assist_id, *height_in_lines, cx);
297 }
298 }
299 }
300
301 fn handle_editor_action(
302 &mut self,
303 assist_id: InlineAssistId,
304 undo: bool,
305 cx: &mut WindowContext,
306 ) {
307 let Some(assist) = self.pending_assists.get(&assist_id) else {
308 return;
309 };
310 let Some(editor) = assist.editor.upgrade() else {
311 return;
312 };
313
314 let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
315 let assist_range = assist.codegen.read(cx).range().to_offset(&buffer);
316 let editor = editor.read(cx);
317 if editor.selections.count() == 1 {
318 let selection = editor.selections.newest::<usize>(cx);
319 if assist_range.contains(&selection.start) && assist_range.contains(&selection.end) {
320 if undo {
321 self.finish_inline_assist(assist_id, true, cx);
322 } else if matches!(assist.codegen.read(cx).status, CodegenStatus::Pending) {
323 self.dismiss_inline_assist(assist_id, cx);
324 } else {
325 self.finish_inline_assist(assist_id, false, cx);
326 }
327
328 return;
329 }
330 }
331
332 cx.propagate();
333 }
334
335 fn handle_editor_event(
336 &mut self,
337 assist_id: InlineAssistId,
338 editor: View<Editor>,
339 event: &EditorEvent,
340 cx: &mut WindowContext,
341 ) {
342 let Some(assist) = self.pending_assists.get(&assist_id) else {
343 return;
344 };
345
346 match event {
347 EditorEvent::SelectionsChanged { local } if *local => {
348 if let CodegenStatus::Idle = &assist.codegen.read(cx).status {
349 self.finish_inline_assist(assist_id, true, cx);
350 }
351 }
352 EditorEvent::Saved => {
353 if let CodegenStatus::Done = &assist.codegen.read(cx).status {
354 self.finish_inline_assist(assist_id, false, cx)
355 }
356 }
357 EditorEvent::Edited { transaction_id }
358 if matches!(
359 assist.codegen.read(cx).status,
360 CodegenStatus::Error(_) | CodegenStatus::Done
361 ) =>
362 {
363 let buffer = editor.read(cx).buffer().read(cx);
364 let edited_ranges =
365 buffer.edited_ranges_for_transaction::<usize>(*transaction_id, cx);
366 let assist_range = assist.codegen.read(cx).range().to_offset(&buffer.read(cx));
367 if edited_ranges
368 .iter()
369 .any(|range| range.overlaps(&assist_range))
370 {
371 self.finish_inline_assist(assist_id, false, cx);
372 }
373 }
374 _ => {}
375 }
376 }
377
378 fn finish_inline_assist(
379 &mut self,
380 assist_id: InlineAssistId,
381 undo: bool,
382 cx: &mut WindowContext,
383 ) {
384 self.dismiss_inline_assist(assist_id, cx);
385
386 if let Some(pending_assist) = self.pending_assists.remove(&assist_id) {
387 if let hash_map::Entry::Occupied(mut entry) = self
388 .pending_assist_ids_by_editor
389 .entry(pending_assist.editor.clone())
390 {
391 entry.get_mut().retain(|id| *id != assist_id);
392 if entry.get().is_empty() {
393 entry.remove();
394 }
395 }
396
397 if let Some(editor) = pending_assist.editor.upgrade() {
398 self.update_editor_highlights(&editor, cx);
399
400 if undo {
401 pending_assist
402 .codegen
403 .update(cx, |codegen, cx| codegen.undo(cx));
404 }
405 }
406 }
407 }
408
409 fn dismiss_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
410 let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) else {
411 return false;
412 };
413 let Some(editor) = pending_assist.editor.upgrade() else {
414 return false;
415 };
416 let Some(decorations) = pending_assist.editor_decorations.take() else {
417 return false;
418 };
419
420 editor.update(cx, |editor, cx| {
421 let mut to_remove = decorations.removed_line_block_ids;
422 to_remove.insert(decorations.prompt_block_id);
423 to_remove.insert(decorations.end_block_id);
424 editor.remove_blocks(to_remove, None, cx);
425 if decorations
426 .prompt_editor
427 .focus_handle(cx)
428 .contains_focused(cx)
429 {
430 editor.focus(cx);
431 }
432 });
433
434 self.update_editor_highlights(&editor, cx);
435 true
436 }
437
438 fn resize_inline_assist(
439 &mut self,
440 assist_id: InlineAssistId,
441 height_in_lines: u8,
442 cx: &mut WindowContext,
443 ) {
444 if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) {
445 if let Some(editor) = pending_assist.editor.upgrade() {
446 if let Some(decorations) = pending_assist.editor_decorations.as_ref() {
447 let gutter_dimensions =
448 decorations.prompt_editor.read(cx).gutter_dimensions.clone();
449 let mut new_blocks = HashMap::default();
450 new_blocks.insert(
451 decorations.prompt_block_id,
452 (
453 Some(height_in_lines),
454 build_inline_assist_editor_renderer(
455 &decorations.prompt_editor,
456 gutter_dimensions,
457 ),
458 ),
459 );
460 editor.update(cx, |editor, cx| {
461 editor
462 .display_map
463 .update(cx, |map, cx| map.replace_blocks(new_blocks, cx))
464 });
465 }
466 }
467 }
468 }
469
470 fn start_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
471 let pending_assist = if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id)
472 {
473 pending_assist
474 } else {
475 return;
476 };
477
478 pending_assist
479 .codegen
480 .update(cx, |codegen, cx| codegen.undo(cx));
481
482 let Some(user_prompt) = pending_assist
483 .editor_decorations
484 .as_ref()
485 .map(|decorations| decorations.prompt_editor.read(cx).prompt(cx))
486 else {
487 return;
488 };
489
490 let context = if pending_assist.include_context {
491 pending_assist.workspace.as_ref().and_then(|workspace| {
492 let workspace = workspace.upgrade()?.read(cx);
493 let assistant_panel = workspace.panel::<AssistantPanel>(cx)?;
494 assistant_panel.read(cx).active_context(cx)
495 })
496 } else {
497 None
498 };
499
500 let editor = if let Some(editor) = pending_assist.editor.upgrade() {
501 editor
502 } else {
503 return;
504 };
505
506 let project_name = pending_assist.workspace.as_ref().and_then(|workspace| {
507 let workspace = workspace.upgrade()?;
508 Some(
509 workspace
510 .read(cx)
511 .project()
512 .read(cx)
513 .worktree_root_names(cx)
514 .collect::<Vec<&str>>()
515 .join("/"),
516 )
517 });
518
519 self.prompt_history.retain(|prompt| *prompt != user_prompt);
520 self.prompt_history.push_back(user_prompt.clone());
521 if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
522 self.prompt_history.pop_front();
523 }
524
525 let codegen = pending_assist.codegen.clone();
526 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
527 let range = codegen.read(cx).range();
528 let start = snapshot.point_to_buffer_offset(range.start);
529 let end = snapshot.point_to_buffer_offset(range.end);
530 let (buffer, range) = if let Some((start, end)) = start.zip(end) {
531 let (start_buffer, start_buffer_offset) = start;
532 let (end_buffer, end_buffer_offset) = end;
533 if start_buffer.remote_id() == end_buffer.remote_id() {
534 (start_buffer.clone(), start_buffer_offset..end_buffer_offset)
535 } else {
536 self.finish_inline_assist(assist_id, false, cx);
537 return;
538 }
539 } else {
540 self.finish_inline_assist(assist_id, false, cx);
541 return;
542 };
543
544 let language = buffer.language_at(range.start);
545 let language_name = if let Some(language) = language.as_ref() {
546 if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
547 None
548 } else {
549 Some(language.name())
550 }
551 } else {
552 None
553 };
554
555 // Higher Temperature increases the randomness of model outputs.
556 // If Markdown or No Language is Known, increase the randomness for more creative output
557 // If Code, decrease temperature to get more deterministic outputs
558 let temperature = if let Some(language) = language_name.clone() {
559 if language.as_ref() == "Markdown" {
560 1.0
561 } else {
562 0.5
563 }
564 } else {
565 1.0
566 };
567
568 let prompt = cx.background_executor().spawn(async move {
569 let language_name = language_name.as_deref();
570 generate_content_prompt(user_prompt, language_name, buffer, range, project_name)
571 });
572
573 let mut messages = Vec::new();
574 if let Some(context) = context {
575 let request = context.read(cx).to_completion_request(cx);
576 messages = request.messages;
577 }
578 let model = CompletionProvider::global(cx).model();
579
580 cx.spawn(|mut cx| async move {
581 let prompt = prompt.await?;
582
583 messages.push(LanguageModelRequestMessage {
584 role: Role::User,
585 content: prompt,
586 });
587
588 let request = LanguageModelRequest {
589 model,
590 messages,
591 stop: vec!["|END|>".to_string()],
592 temperature,
593 };
594
595 codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
596 anyhow::Ok(())
597 })
598 .detach_and_log_err(cx);
599 }
600
601 fn stop_inline_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
602 let pending_assist = if let Some(pending_assist) = self.pending_assists.get_mut(&assist_id)
603 {
604 pending_assist
605 } else {
606 return;
607 };
608
609 pending_assist
610 .codegen
611 .update(cx, |codegen, cx| codegen.stop(cx));
612 }
613
614 fn update_editor_highlights(&self, editor: &View<Editor>, cx: &mut WindowContext) {
615 let mut gutter_pending_ranges = Vec::new();
616 let mut gutter_transformed_ranges = Vec::new();
617 let mut foreground_ranges = Vec::new();
618 let mut inserted_row_ranges = Vec::new();
619 let empty_assist_ids = Vec::new();
620 let assist_ids = self
621 .pending_assist_ids_by_editor
622 .get(&editor.downgrade())
623 .unwrap_or(&empty_assist_ids);
624
625 for assist_id in assist_ids {
626 if let Some(pending_assist) = self.pending_assists.get(assist_id) {
627 let codegen = pending_assist.codegen.read(cx);
628 foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
629
630 if codegen.edit_position != codegen.range().end {
631 gutter_pending_ranges.push(codegen.edit_position..codegen.range().end);
632 }
633
634 if codegen.range().start != codegen.edit_position {
635 gutter_transformed_ranges.push(codegen.range().start..codegen.edit_position);
636 }
637
638 if pending_assist.editor_decorations.is_some() {
639 inserted_row_ranges.extend(codegen.diff.inserted_row_ranges.iter().cloned());
640 }
641 }
642 }
643
644 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
645 merge_ranges(&mut foreground_ranges, &snapshot);
646 merge_ranges(&mut gutter_pending_ranges, &snapshot);
647 merge_ranges(&mut gutter_transformed_ranges, &snapshot);
648 editor.update(cx, |editor, cx| {
649 enum GutterPendingRange {}
650 if gutter_pending_ranges.is_empty() {
651 editor.clear_gutter_highlights::<GutterPendingRange>(cx);
652 } else {
653 editor.highlight_gutter::<GutterPendingRange>(
654 &gutter_pending_ranges,
655 |cx| cx.theme().status().info_background,
656 cx,
657 )
658 }
659
660 enum GutterTransformedRange {}
661 if gutter_transformed_ranges.is_empty() {
662 editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
663 } else {
664 editor.highlight_gutter::<GutterTransformedRange>(
665 &gutter_transformed_ranges,
666 |cx| cx.theme().status().info,
667 cx,
668 )
669 }
670
671 if foreground_ranges.is_empty() {
672 editor.clear_highlights::<PendingInlineAssist>(cx);
673 } else {
674 editor.highlight_text::<PendingInlineAssist>(
675 foreground_ranges,
676 HighlightStyle {
677 fade_out: Some(0.6),
678 ..Default::default()
679 },
680 cx,
681 );
682 }
683
684 editor.clear_row_highlights::<PendingInlineAssist>();
685 for row_range in inserted_row_ranges {
686 editor.highlight_rows::<PendingInlineAssist>(
687 row_range,
688 Some(cx.theme().status().info_background),
689 false,
690 cx,
691 );
692 }
693 });
694 }
695
696 fn update_editor_blocks(
697 &mut self,
698 editor: &View<Editor>,
699 assist_id: InlineAssistId,
700 cx: &mut WindowContext,
701 ) {
702 let Some(pending_assist) = self.pending_assists.get_mut(&assist_id) else {
703 return;
704 };
705 let Some(decorations) = pending_assist.editor_decorations.as_mut() else {
706 return;
707 };
708
709 let codegen = pending_assist.codegen.read(cx);
710 let old_snapshot = codegen.snapshot.clone();
711 let old_buffer = codegen.old_buffer.clone();
712 let deleted_row_ranges = codegen.diff.deleted_row_ranges.clone();
713
714 editor.update(cx, |editor, cx| {
715 let old_blocks = mem::take(&mut decorations.removed_line_block_ids);
716 editor.remove_blocks(old_blocks, None, cx);
717
718 let mut new_blocks = Vec::new();
719 for (new_row, old_row_range) in deleted_row_ranges {
720 let (_, buffer_start) = old_snapshot
721 .point_to_buffer_offset(Point::new(*old_row_range.start(), 0))
722 .unwrap();
723 let (_, buffer_end) = old_snapshot
724 .point_to_buffer_offset(Point::new(
725 *old_row_range.end(),
726 old_snapshot.line_len(MultiBufferRow(*old_row_range.end())),
727 ))
728 .unwrap();
729
730 let deleted_lines_editor = cx.new_view(|cx| {
731 let multi_buffer = cx.new_model(|_| {
732 MultiBuffer::without_headers(0, language::Capability::ReadOnly)
733 });
734 multi_buffer.update(cx, |multi_buffer, cx| {
735 multi_buffer.push_excerpts(
736 old_buffer.clone(),
737 Some(ExcerptRange {
738 context: buffer_start..buffer_end,
739 primary: None,
740 }),
741 cx,
742 );
743 });
744
745 enum DeletedLines {}
746 let mut editor = Editor::for_multibuffer(multi_buffer, None, true, cx);
747 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
748 editor.set_show_wrap_guides(false, cx);
749 editor.set_show_gutter(false, cx);
750 editor.scroll_manager.set_forbid_vertical_scroll(true);
751 editor.set_read_only(true);
752 editor.highlight_rows::<DeletedLines>(
753 Anchor::min()..=Anchor::max(),
754 Some(cx.theme().status().deleted_background),
755 false,
756 cx,
757 );
758 editor
759 });
760
761 let height = deleted_lines_editor
762 .update(cx, |editor, cx| editor.max_point(cx).row().0 as u8 + 1);
763 new_blocks.push(BlockProperties {
764 position: new_row,
765 height,
766 style: BlockStyle::Flex,
767 render: Box::new(move |cx| {
768 div()
769 .bg(cx.theme().status().deleted_background)
770 .size_full()
771 .pl(cx.gutter_dimensions.full_width())
772 .child(deleted_lines_editor.clone())
773 .into_any_element()
774 }),
775 disposition: BlockDisposition::Above,
776 });
777 }
778
779 decorations.removed_line_block_ids = editor
780 .insert_blocks(new_blocks, None, cx)
781 .into_iter()
782 .collect();
783 })
784 }
785}
786
787fn build_inline_assist_editor_renderer(
788 editor: &View<InlineAssistEditor>,
789 gutter_dimensions: Arc<Mutex<GutterDimensions>>,
790) -> RenderBlock {
791 let editor = editor.clone();
792 Box::new(move |cx: &mut BlockContext| {
793 *gutter_dimensions.lock() = *cx.gutter_dimensions;
794 editor.clone().into_any_element()
795 })
796}
797
798#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
799struct InlineAssistId(usize);
800
801impl InlineAssistId {
802 fn post_inc(&mut self) -> InlineAssistId {
803 let id = *self;
804 self.0 += 1;
805 id
806 }
807}
808
809enum InlineAssistEditorEvent {
810 StartRequested,
811 StopRequested,
812 ConfirmRequested,
813 CancelRequested,
814 DismissRequested,
815 Resized { height_in_lines: u8 },
816}
817
818struct InlineAssistEditor {
819 id: InlineAssistId,
820 height_in_lines: u8,
821 prompt_editor: View<Editor>,
822 edited_since_done: bool,
823 gutter_dimensions: Arc<Mutex<GutterDimensions>>,
824 prompt_history: VecDeque<String>,
825 prompt_history_ix: Option<usize>,
826 pending_prompt: String,
827 codegen: Model<Codegen>,
828 workspace: Option<WeakView<Workspace>>,
829 _subscriptions: Vec<Subscription>,
830}
831
832impl EventEmitter<InlineAssistEditorEvent> for InlineAssistEditor {}
833
834impl Render for InlineAssistEditor {
835 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
836 let gutter_dimensions = *self.gutter_dimensions.lock();
837
838 let buttons = match &self.codegen.read(cx).status {
839 CodegenStatus::Idle => {
840 vec![
841 IconButton::new("start", IconName::Sparkle)
842 .icon_color(Color::Muted)
843 .size(ButtonSize::None)
844 .icon_size(IconSize::XSmall)
845 .tooltip(|cx| Tooltip::for_action("Transform", &menu::Confirm, cx))
846 .on_click(
847 cx.listener(|_, _, cx| {
848 cx.emit(InlineAssistEditorEvent::StartRequested)
849 }),
850 ),
851 IconButton::new("cancel", IconName::Close)
852 .icon_color(Color::Muted)
853 .size(ButtonSize::None)
854 .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
855 .on_click(cx.listener(|_, _, cx| {
856 cx.emit(InlineAssistEditorEvent::CancelRequested)
857 })),
858 ]
859 }
860 CodegenStatus::Pending => {
861 vec![
862 IconButton::new("stop", IconName::Stop)
863 .icon_color(Color::Error)
864 .size(ButtonSize::None)
865 .icon_size(IconSize::XSmall)
866 .tooltip(|cx| {
867 Tooltip::with_meta(
868 "Interrupt Transformation",
869 Some(&menu::Cancel),
870 "Changes won't be discarded",
871 cx,
872 )
873 })
874 .on_click(
875 cx.listener(|_, _, cx| cx.emit(InlineAssistEditorEvent::StopRequested)),
876 ),
877 IconButton::new("cancel", IconName::Close)
878 .icon_color(Color::Muted)
879 .size(ButtonSize::None)
880 .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
881 .on_click(cx.listener(|_, _, cx| {
882 cx.emit(InlineAssistEditorEvent::CancelRequested)
883 })),
884 ]
885 }
886 CodegenStatus::Error(_) | CodegenStatus::Done => {
887 vec![
888 if self.edited_since_done {
889 IconButton::new("restart", IconName::RotateCw)
890 .icon_color(Color::Info)
891 .icon_size(IconSize::XSmall)
892 .size(ButtonSize::None)
893 .tooltip(|cx| {
894 Tooltip::with_meta(
895 "Restart Transformation",
896 Some(&menu::Confirm),
897 "Changes will be discarded",
898 cx,
899 )
900 })
901 .on_click(cx.listener(|_, _, cx| {
902 cx.emit(InlineAssistEditorEvent::StartRequested);
903 }))
904 } else {
905 IconButton::new("confirm", IconName::Check)
906 .icon_color(Color::Info)
907 .size(ButtonSize::None)
908 .tooltip(|cx| Tooltip::for_action("Confirm Assist", &menu::Confirm, cx))
909 .on_click(cx.listener(|_, _, cx| {
910 cx.emit(InlineAssistEditorEvent::ConfirmRequested);
911 }))
912 },
913 IconButton::new("cancel", IconName::Close)
914 .icon_color(Color::Muted)
915 .size(ButtonSize::None)
916 .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
917 .on_click(cx.listener(|_, _, cx| {
918 cx.emit(InlineAssistEditorEvent::CancelRequested)
919 })),
920 ]
921 }
922 };
923
924 v_flex().h_full().w_full().justify_end().child(
925 h_flex()
926 .bg(cx.theme().colors().editor_background)
927 .border_y_1()
928 .border_color(cx.theme().status().info_border)
929 .py_1p5()
930 .w_full()
931 .on_action(cx.listener(Self::confirm))
932 .on_action(cx.listener(Self::cancel))
933 .on_action(cx.listener(Self::move_up))
934 .on_action(cx.listener(Self::move_down))
935 .child(
936 h_flex()
937 .w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
938 // .pr(gutter_dimensions.fold_area_width())
939 .justify_center()
940 .gap_2()
941 .children(self.workspace.clone().map(|workspace| {
942 IconButton::new("context", IconName::Context)
943 .size(ButtonSize::None)
944 .icon_size(IconSize::XSmall)
945 .icon_color(Color::Muted)
946 .on_click({
947 let workspace = workspace.clone();
948 cx.listener(move |_, _, cx| {
949 workspace
950 .update(cx, |workspace, cx| {
951 workspace.focus_panel::<AssistantPanel>(cx);
952 })
953 .ok();
954 })
955 })
956 .tooltip(move |cx| {
957 let token_count = workspace.upgrade().and_then(|workspace| {
958 let panel =
959 workspace.read(cx).panel::<AssistantPanel>(cx)?;
960 let context = panel.read(cx).active_context(cx)?;
961 context.read(cx).token_count()
962 });
963 if let Some(token_count) = token_count {
964 Tooltip::with_meta(
965 format!(
966 "{} Additional Context Tokens from Assistant",
967 token_count
968 ),
969 Some(&crate::ToggleFocus),
970 "Click to open…",
971 cx,
972 )
973 } else {
974 Tooltip::for_action(
975 "Toggle Assistant Panel",
976 &crate::ToggleFocus,
977 cx,
978 )
979 }
980 })
981 }))
982 .children(
983 if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
984 let error_message = SharedString::from(error.to_string());
985 Some(
986 div()
987 .id("error")
988 .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
989 .child(
990 Icon::new(IconName::XCircle)
991 .size(IconSize::Small)
992 .color(Color::Error),
993 ),
994 )
995 } else {
996 None
997 },
998 ),
999 )
1000 .child(div().flex_1().child(self.render_prompt_editor(cx)))
1001 .child(h_flex().gap_2().pr_4().children(buttons)),
1002 )
1003 }
1004}
1005
1006impl FocusableView for InlineAssistEditor {
1007 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1008 self.prompt_editor.focus_handle(cx)
1009 }
1010}
1011
1012impl InlineAssistEditor {
1013 const MAX_LINES: u8 = 8;
1014
1015 #[allow(clippy::too_many_arguments)]
1016 fn new(
1017 id: InlineAssistId,
1018 gutter_dimensions: Arc<Mutex<GutterDimensions>>,
1019 prompt_history: VecDeque<String>,
1020 codegen: Model<Codegen>,
1021 workspace: Option<WeakView<Workspace>>,
1022 cx: &mut ViewContext<Self>,
1023 ) -> Self {
1024 let prompt_editor = cx.new_view(|cx| {
1025 let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx);
1026 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1027 editor.set_placeholder_text("Add a prompt…", cx);
1028 editor
1029 });
1030 cx.focus_view(&prompt_editor);
1031
1032 let subscriptions = vec![
1033 cx.observe(&codegen, Self::handle_codegen_changed),
1034 cx.observe(&prompt_editor, Self::handle_prompt_editor_changed),
1035 cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
1036 ];
1037
1038 let mut this = Self {
1039 id,
1040 height_in_lines: 1,
1041 prompt_editor,
1042 edited_since_done: false,
1043 gutter_dimensions,
1044 prompt_history,
1045 prompt_history_ix: None,
1046 pending_prompt: String::new(),
1047 codegen,
1048 workspace,
1049 _subscriptions: subscriptions,
1050 };
1051 this.count_lines(cx);
1052 this
1053 }
1054
1055 fn prompt(&self, cx: &AppContext) -> String {
1056 self.prompt_editor.read(cx).text(cx)
1057 }
1058
1059 fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
1060 let height_in_lines = cmp::max(
1061 2, // Make the editor at least two lines tall, to account for padding and buttons.
1062 cmp::min(
1063 self.prompt_editor
1064 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1065 Self::MAX_LINES as u32,
1066 ),
1067 ) as u8;
1068
1069 if height_in_lines != self.height_in_lines {
1070 self.height_in_lines = height_in_lines;
1071 cx.emit(InlineAssistEditorEvent::Resized { height_in_lines });
1072 }
1073 }
1074
1075 fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
1076 self.count_lines(cx);
1077 }
1078
1079 fn handle_prompt_editor_events(
1080 &mut self,
1081 _: View<Editor>,
1082 event: &EditorEvent,
1083 cx: &mut ViewContext<Self>,
1084 ) {
1085 match event {
1086 EditorEvent::Edited { .. } => {
1087 let prompt = self.prompt_editor.read(cx).text(cx);
1088 if self
1089 .prompt_history_ix
1090 .map_or(true, |ix| self.prompt_history[ix] != prompt)
1091 {
1092 self.prompt_history_ix.take();
1093 self.pending_prompt = prompt;
1094 }
1095
1096 self.edited_since_done = true;
1097 cx.notify();
1098 }
1099 _ => {}
1100 }
1101 }
1102
1103 fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
1104 match &self.codegen.read(cx).status {
1105 CodegenStatus::Idle => {
1106 self.prompt_editor
1107 .update(cx, |editor, _| editor.set_read_only(false));
1108 }
1109 CodegenStatus::Pending => {
1110 self.prompt_editor
1111 .update(cx, |editor, _| editor.set_read_only(true));
1112 }
1113 CodegenStatus::Done | CodegenStatus::Error(_) => {
1114 self.edited_since_done = false;
1115 self.prompt_editor
1116 .update(cx, |editor, _| editor.set_read_only(false));
1117 }
1118 }
1119 }
1120
1121 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
1122 match &self.codegen.read(cx).status {
1123 CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
1124 cx.emit(InlineAssistEditorEvent::CancelRequested);
1125 }
1126 CodegenStatus::Pending => {
1127 cx.emit(InlineAssistEditorEvent::StopRequested);
1128 }
1129 }
1130 }
1131
1132 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
1133 match &self.codegen.read(cx).status {
1134 CodegenStatus::Idle => {
1135 cx.emit(InlineAssistEditorEvent::StartRequested);
1136 }
1137 CodegenStatus::Pending => {
1138 cx.emit(InlineAssistEditorEvent::DismissRequested);
1139 }
1140 CodegenStatus::Done | CodegenStatus::Error(_) => {
1141 if self.edited_since_done {
1142 cx.emit(InlineAssistEditorEvent::StartRequested);
1143 } else {
1144 cx.emit(InlineAssistEditorEvent::ConfirmRequested);
1145 }
1146 }
1147 }
1148 }
1149
1150 fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
1151 if let Some(ix) = self.prompt_history_ix {
1152 if ix > 0 {
1153 self.prompt_history_ix = Some(ix - 1);
1154 let prompt = self.prompt_history[ix - 1].as_str();
1155 self.prompt_editor.update(cx, |editor, cx| {
1156 editor.set_text(prompt, cx);
1157 editor.move_to_beginning(&Default::default(), cx);
1158 });
1159 }
1160 } else if !self.prompt_history.is_empty() {
1161 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
1162 let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
1163 self.prompt_editor.update(cx, |editor, cx| {
1164 editor.set_text(prompt, cx);
1165 editor.move_to_beginning(&Default::default(), cx);
1166 });
1167 }
1168 }
1169
1170 fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
1171 if let Some(ix) = self.prompt_history_ix {
1172 if ix < self.prompt_history.len() - 1 {
1173 self.prompt_history_ix = Some(ix + 1);
1174 let prompt = self.prompt_history[ix + 1].as_str();
1175 self.prompt_editor.update(cx, |editor, cx| {
1176 editor.set_text(prompt, cx);
1177 editor.move_to_end(&Default::default(), cx)
1178 });
1179 } else {
1180 self.prompt_history_ix = None;
1181 let prompt = self.pending_prompt.as_str();
1182 self.prompt_editor.update(cx, |editor, cx| {
1183 editor.set_text(prompt, cx);
1184 editor.move_to_end(&Default::default(), cx)
1185 });
1186 }
1187 }
1188 }
1189
1190 fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1191 let settings = ThemeSettings::get_global(cx);
1192 let text_style = TextStyle {
1193 color: if self.prompt_editor.read(cx).read_only(cx) {
1194 cx.theme().colors().text_disabled
1195 } else {
1196 cx.theme().colors().text
1197 },
1198 font_family: settings.ui_font.family.clone(),
1199 font_features: settings.ui_font.features.clone(),
1200 font_size: rems(0.875).into(),
1201 font_weight: FontWeight::NORMAL,
1202 font_style: FontStyle::Normal,
1203 line_height: relative(1.3),
1204 background_color: None,
1205 underline: None,
1206 strikethrough: None,
1207 white_space: WhiteSpace::Normal,
1208 };
1209 EditorElement::new(
1210 &self.prompt_editor,
1211 EditorStyle {
1212 background: cx.theme().colors().editor_background,
1213 local_player: cx.theme().players().local(),
1214 text: text_style,
1215 ..Default::default()
1216 },
1217 )
1218 }
1219}
1220
1221struct PendingInlineAssist {
1222 editor: WeakView<Editor>,
1223 editor_decorations: Option<PendingInlineAssistDecorations>,
1224 codegen: Model<Codegen>,
1225 _subscriptions: Vec<Subscription>,
1226 workspace: Option<WeakView<Workspace>>,
1227 include_context: bool,
1228}
1229
1230struct PendingInlineAssistDecorations {
1231 prompt_block_id: BlockId,
1232 prompt_editor: View<InlineAssistEditor>,
1233 removed_line_block_ids: HashSet<BlockId>,
1234 end_block_id: BlockId,
1235}
1236
1237#[derive(Debug)]
1238pub enum CodegenEvent {
1239 Finished,
1240 Undone,
1241}
1242
1243#[derive(Clone)]
1244pub enum CodegenKind {
1245 Transform { range: Range<Anchor> },
1246 Generate { position: Anchor },
1247}
1248
1249impl CodegenKind {
1250 fn range(&self, snapshot: &MultiBufferSnapshot) -> Range<Anchor> {
1251 match self {
1252 CodegenKind::Transform { range } => range.clone(),
1253 CodegenKind::Generate { position } => position.bias_left(snapshot)..*position,
1254 }
1255 }
1256}
1257
1258pub struct Codegen {
1259 buffer: Model<MultiBuffer>,
1260 old_buffer: Model<Buffer>,
1261 snapshot: MultiBufferSnapshot,
1262 kind: CodegenKind,
1263 edit_position: Anchor,
1264 last_equal_ranges: Vec<Range<Anchor>>,
1265 transaction_id: Option<TransactionId>,
1266 status: CodegenStatus,
1267 generation: Task<()>,
1268 diff: Diff,
1269 telemetry: Option<Arc<Telemetry>>,
1270 _subscription: gpui::Subscription,
1271}
1272
1273enum CodegenStatus {
1274 Idle,
1275 Pending,
1276 Done,
1277 Error(anyhow::Error),
1278}
1279
1280#[derive(Default)]
1281struct Diff {
1282 task: Option<Task<()>>,
1283 should_update: bool,
1284 deleted_row_ranges: Vec<(Anchor, RangeInclusive<u32>)>,
1285 inserted_row_ranges: Vec<RangeInclusive<Anchor>>,
1286}
1287
1288impl EventEmitter<CodegenEvent> for Codegen {}
1289
1290impl Codegen {
1291 pub fn new(
1292 buffer: Model<MultiBuffer>,
1293 kind: CodegenKind,
1294 telemetry: Option<Arc<Telemetry>>,
1295 cx: &mut ModelContext<Self>,
1296 ) -> Self {
1297 let snapshot = buffer.read(cx).snapshot(cx);
1298
1299 let (old_buffer, _, _) = buffer
1300 .read(cx)
1301 .range_to_buffer_ranges(kind.range(&snapshot), cx)
1302 .pop()
1303 .unwrap();
1304 let old_buffer = cx.new_model(|cx| {
1305 let old_buffer = old_buffer.read(cx);
1306 let text = old_buffer.as_rope().clone();
1307 let line_ending = old_buffer.line_ending();
1308 let language = old_buffer.language().cloned();
1309 let language_registry = old_buffer.language_registry();
1310
1311 let mut buffer = Buffer::local_normalized(text, line_ending, cx);
1312 buffer.set_language(language, cx);
1313 if let Some(language_registry) = language_registry {
1314 buffer.set_language_registry(language_registry)
1315 }
1316 buffer
1317 });
1318
1319 Self {
1320 buffer: buffer.clone(),
1321 old_buffer,
1322 edit_position: kind.range(&snapshot).start,
1323 snapshot,
1324 kind,
1325 last_equal_ranges: Default::default(),
1326 transaction_id: Default::default(),
1327 status: CodegenStatus::Idle,
1328 generation: Task::ready(()),
1329 diff: Diff::default(),
1330 telemetry,
1331 _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
1332 }
1333 }
1334
1335 fn handle_buffer_event(
1336 &mut self,
1337 _buffer: Model<MultiBuffer>,
1338 event: &multi_buffer::Event,
1339 cx: &mut ModelContext<Self>,
1340 ) {
1341 if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
1342 if self.transaction_id == Some(*transaction_id) {
1343 self.transaction_id = None;
1344 self.generation = Task::ready(());
1345 cx.emit(CodegenEvent::Undone);
1346 }
1347 }
1348 }
1349
1350 pub fn range(&self) -> Range<Anchor> {
1351 self.kind.range(&self.snapshot)
1352 }
1353
1354 pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
1355 &self.last_equal_ranges
1356 }
1357
1358 pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
1359 let range = self.range();
1360 let snapshot = self.snapshot.clone();
1361 let selected_text = snapshot
1362 .text_for_range(range.start..range.end)
1363 .collect::<Rope>();
1364
1365 let selection_start = range.start.to_point(&snapshot);
1366 let suggested_line_indent = snapshot
1367 .suggested_indents(selection_start.row..selection_start.row + 1, cx)
1368 .into_values()
1369 .next()
1370 .unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
1371
1372 let model_telemetry_id = prompt.model.telemetry_id();
1373 let response = CompletionProvider::global(cx).complete(prompt);
1374 let telemetry = self.telemetry.clone();
1375 self.edit_position = range.start;
1376 self.diff = Diff::default();
1377 self.status = CodegenStatus::Pending;
1378 self.generation = cx.spawn(|this, mut cx| {
1379 async move {
1380 let generate = async {
1381 let mut edit_start = range.start.to_offset(&snapshot);
1382
1383 let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
1384 let diff: Task<anyhow::Result<()>> =
1385 cx.background_executor().spawn(async move {
1386 let mut response_latency = None;
1387 let request_start = Instant::now();
1388 let diff = async {
1389 let chunks = strip_invalid_spans_from_codeblock(response.await?);
1390 futures::pin_mut!(chunks);
1391 let mut diff = StreamingDiff::new(selected_text.to_string());
1392
1393 let mut new_text = String::new();
1394 let mut base_indent = None;
1395 let mut line_indent = None;
1396 let mut first_line = true;
1397
1398 while let Some(chunk) = chunks.next().await {
1399 if response_latency.is_none() {
1400 response_latency = Some(request_start.elapsed());
1401 }
1402 let chunk = chunk?;
1403
1404 let mut lines = chunk.split('\n').peekable();
1405 while let Some(line) = lines.next() {
1406 new_text.push_str(line);
1407 if line_indent.is_none() {
1408 if let Some(non_whitespace_ch_ix) =
1409 new_text.find(|ch: char| !ch.is_whitespace())
1410 {
1411 line_indent = Some(non_whitespace_ch_ix);
1412 base_indent = base_indent.or(line_indent);
1413
1414 let line_indent = line_indent.unwrap();
1415 let base_indent = base_indent.unwrap();
1416 let indent_delta =
1417 line_indent as i32 - base_indent as i32;
1418 let mut corrected_indent_len = cmp::max(
1419 0,
1420 suggested_line_indent.len as i32 + indent_delta,
1421 )
1422 as usize;
1423 if first_line {
1424 corrected_indent_len = corrected_indent_len
1425 .saturating_sub(
1426 selection_start.column as usize,
1427 );
1428 }
1429
1430 let indent_char = suggested_line_indent.char();
1431 let mut indent_buffer = [0; 4];
1432 let indent_str =
1433 indent_char.encode_utf8(&mut indent_buffer);
1434 new_text.replace_range(
1435 ..line_indent,
1436 &indent_str.repeat(corrected_indent_len),
1437 );
1438 }
1439 }
1440
1441 if line_indent.is_some() {
1442 hunks_tx.send(diff.push_new(&new_text)).await?;
1443 new_text.clear();
1444 }
1445
1446 if lines.peek().is_some() {
1447 hunks_tx.send(diff.push_new("\n")).await?;
1448 line_indent = None;
1449 first_line = false;
1450 }
1451 }
1452 }
1453 hunks_tx.send(diff.push_new(&new_text)).await?;
1454 hunks_tx.send(diff.finish()).await?;
1455
1456 anyhow::Ok(())
1457 };
1458
1459 let result = diff.await;
1460
1461 let error_message =
1462 result.as_ref().err().map(|error| error.to_string());
1463 if let Some(telemetry) = telemetry {
1464 telemetry.report_assistant_event(
1465 None,
1466 telemetry_events::AssistantKind::Inline,
1467 model_telemetry_id,
1468 response_latency,
1469 error_message,
1470 );
1471 }
1472
1473 result?;
1474 Ok(())
1475 });
1476
1477 while let Some(hunks) = hunks_rx.next().await {
1478 this.update(&mut cx, |this, cx| {
1479 this.last_equal_ranges.clear();
1480
1481 let transaction = this.buffer.update(cx, |buffer, cx| {
1482 // Avoid grouping assistant edits with user edits.
1483 buffer.finalize_last_transaction(cx);
1484
1485 buffer.start_transaction(cx);
1486 buffer.edit(
1487 hunks.into_iter().filter_map(|hunk| match hunk {
1488 Hunk::Insert { text } => {
1489 let edit_start = snapshot.anchor_after(edit_start);
1490 Some((edit_start..edit_start, text))
1491 }
1492 Hunk::Remove { len } => {
1493 let edit_end = edit_start + len;
1494 let edit_range = snapshot.anchor_after(edit_start)
1495 ..snapshot.anchor_before(edit_end);
1496 edit_start = edit_end;
1497 Some((edit_range, String::new()))
1498 }
1499 Hunk::Keep { len } => {
1500 let edit_end = edit_start + len;
1501 let edit_range = snapshot.anchor_after(edit_start)
1502 ..snapshot.anchor_before(edit_end);
1503 edit_start = edit_end;
1504 this.last_equal_ranges.push(edit_range);
1505 None
1506 }
1507 }),
1508 None,
1509 cx,
1510 );
1511 this.edit_position = snapshot.anchor_after(edit_start);
1512
1513 buffer.end_transaction(cx)
1514 });
1515
1516 if let Some(transaction) = transaction {
1517 if let Some(first_transaction) = this.transaction_id {
1518 // Group all assistant edits into the first transaction.
1519 this.buffer.update(cx, |buffer, cx| {
1520 buffer.merge_transactions(
1521 transaction,
1522 first_transaction,
1523 cx,
1524 )
1525 });
1526 } else {
1527 this.transaction_id = Some(transaction);
1528 this.buffer.update(cx, |buffer, cx| {
1529 buffer.finalize_last_transaction(cx)
1530 });
1531 }
1532 }
1533
1534 this.update_diff(cx);
1535 cx.notify();
1536 })?;
1537 }
1538
1539 diff.await?;
1540
1541 anyhow::Ok(())
1542 };
1543
1544 let result = generate.await;
1545 this.update(&mut cx, |this, cx| {
1546 this.last_equal_ranges.clear();
1547 if let Err(error) = result {
1548 this.status = CodegenStatus::Error(error);
1549 } else {
1550 this.status = CodegenStatus::Done;
1551 }
1552 cx.emit(CodegenEvent::Finished);
1553 cx.notify();
1554 })
1555 .ok();
1556 }
1557 });
1558 cx.notify();
1559 }
1560
1561 pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
1562 self.last_equal_ranges.clear();
1563 self.status = CodegenStatus::Done;
1564 self.generation = Task::ready(());
1565 cx.emit(CodegenEvent::Finished);
1566 cx.notify();
1567 }
1568
1569 pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
1570 if let Some(transaction_id) = self.transaction_id.take() {
1571 self.buffer
1572 .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
1573 }
1574 }
1575
1576 fn update_diff(&mut self, cx: &mut ModelContext<Self>) {
1577 if self.diff.task.is_some() {
1578 self.diff.should_update = true;
1579 } else {
1580 self.diff.should_update = false;
1581
1582 let old_snapshot = self.snapshot.clone();
1583 let old_range = self.range().to_point(&old_snapshot);
1584 let new_snapshot = self.buffer.read(cx).snapshot(cx);
1585 let new_range = self.range().to_point(&new_snapshot);
1586
1587 self.diff.task = Some(cx.spawn(|this, mut cx| async move {
1588 let (deleted_row_ranges, inserted_row_ranges) = cx
1589 .background_executor()
1590 .spawn(async move {
1591 let old_text = old_snapshot
1592 .text_for_range(
1593 Point::new(old_range.start.row, 0)
1594 ..Point::new(
1595 old_range.end.row,
1596 old_snapshot.line_len(MultiBufferRow(old_range.end.row)),
1597 ),
1598 )
1599 .collect::<String>();
1600 let new_text = new_snapshot
1601 .text_for_range(
1602 Point::new(new_range.start.row, 0)
1603 ..Point::new(
1604 new_range.end.row,
1605 new_snapshot.line_len(MultiBufferRow(new_range.end.row)),
1606 ),
1607 )
1608 .collect::<String>();
1609
1610 let mut old_row = old_range.start.row;
1611 let mut new_row = new_range.start.row;
1612 let diff = TextDiff::from_lines(old_text.as_str(), new_text.as_str());
1613
1614 let mut deleted_row_ranges: Vec<(Anchor, RangeInclusive<u32>)> = Vec::new();
1615 let mut inserted_row_ranges = Vec::new();
1616 for change in diff.iter_all_changes() {
1617 let line_count = change.value().lines().count() as u32;
1618 match change.tag() {
1619 similar::ChangeTag::Equal => {
1620 old_row += line_count;
1621 new_row += line_count;
1622 }
1623 similar::ChangeTag::Delete => {
1624 let old_end_row = old_row + line_count - 1;
1625 let new_row =
1626 new_snapshot.anchor_before(Point::new(new_row, 0));
1627
1628 if let Some((_, last_deleted_row_range)) =
1629 deleted_row_ranges.last_mut()
1630 {
1631 if *last_deleted_row_range.end() + 1 == old_row {
1632 *last_deleted_row_range =
1633 *last_deleted_row_range.start()..=old_end_row;
1634 } else {
1635 deleted_row_ranges
1636 .push((new_row, old_row..=old_end_row));
1637 }
1638 } else {
1639 deleted_row_ranges.push((new_row, old_row..=old_end_row));
1640 }
1641
1642 old_row += line_count;
1643 }
1644 similar::ChangeTag::Insert => {
1645 let new_end_row = new_row + line_count - 1;
1646 let start = new_snapshot.anchor_before(Point::new(new_row, 0));
1647 let end = new_snapshot.anchor_before(Point::new(
1648 new_end_row,
1649 new_snapshot.line_len(MultiBufferRow(new_end_row)),
1650 ));
1651 inserted_row_ranges.push(start..=end);
1652 new_row += line_count;
1653 }
1654 }
1655 }
1656
1657 (deleted_row_ranges, inserted_row_ranges)
1658 })
1659 .await;
1660
1661 this.update(&mut cx, |this, cx| {
1662 this.diff.deleted_row_ranges = deleted_row_ranges;
1663 this.diff.inserted_row_ranges = inserted_row_ranges;
1664 this.diff.task = None;
1665 if this.diff.should_update {
1666 this.update_diff(cx);
1667 }
1668 cx.notify();
1669 })
1670 .ok();
1671 }));
1672 }
1673 }
1674}
1675
1676fn strip_invalid_spans_from_codeblock(
1677 stream: impl Stream<Item = Result<String>>,
1678) -> impl Stream<Item = Result<String>> {
1679 let mut first_line = true;
1680 let mut buffer = String::new();
1681 let mut starts_with_markdown_codeblock = false;
1682 let mut includes_start_or_end_span = false;
1683 stream.filter_map(move |chunk| {
1684 let chunk = match chunk {
1685 Ok(chunk) => chunk,
1686 Err(err) => return future::ready(Some(Err(err))),
1687 };
1688 buffer.push_str(&chunk);
1689
1690 if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
1691 includes_start_or_end_span = true;
1692
1693 buffer = buffer
1694 .strip_prefix("<|S|>")
1695 .or_else(|| buffer.strip_prefix("<|S|"))
1696 .unwrap_or(&buffer)
1697 .to_string();
1698 } else if buffer.ends_with("|E|>") {
1699 includes_start_or_end_span = true;
1700 } else if buffer.starts_with("<|")
1701 || buffer.starts_with("<|S")
1702 || buffer.starts_with("<|S|")
1703 || buffer.ends_with('|')
1704 || buffer.ends_with("|E")
1705 || buffer.ends_with("|E|")
1706 {
1707 return future::ready(None);
1708 }
1709
1710 if first_line {
1711 if buffer.is_empty() || buffer == "`" || buffer == "``" {
1712 return future::ready(None);
1713 } else if buffer.starts_with("```") {
1714 starts_with_markdown_codeblock = true;
1715 if let Some(newline_ix) = buffer.find('\n') {
1716 buffer.replace_range(..newline_ix + 1, "");
1717 first_line = false;
1718 } else {
1719 return future::ready(None);
1720 }
1721 }
1722 }
1723
1724 let mut text = buffer.to_string();
1725 if starts_with_markdown_codeblock {
1726 text = text
1727 .strip_suffix("\n```\n")
1728 .or_else(|| text.strip_suffix("\n```"))
1729 .or_else(|| text.strip_suffix("\n``"))
1730 .or_else(|| text.strip_suffix("\n`"))
1731 .or_else(|| text.strip_suffix('\n'))
1732 .unwrap_or(&text)
1733 .to_string();
1734 }
1735
1736 if includes_start_or_end_span {
1737 text = text
1738 .strip_suffix("|E|>")
1739 .or_else(|| text.strip_suffix("E|>"))
1740 .or_else(|| text.strip_prefix("|>"))
1741 .or_else(|| text.strip_prefix('>'))
1742 .unwrap_or(&text)
1743 .to_string();
1744 };
1745
1746 if text.contains('\n') {
1747 first_line = false;
1748 }
1749
1750 let remainder = buffer.split_off(text.len());
1751 let result = if buffer.is_empty() {
1752 None
1753 } else {
1754 Some(Ok(buffer.clone()))
1755 };
1756
1757 buffer = remainder;
1758 future::ready(result)
1759 })
1760}
1761
1762fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
1763 ranges.sort_unstable_by(|a, b| {
1764 a.start
1765 .cmp(&b.start, buffer)
1766 .then_with(|| b.end.cmp(&a.end, buffer))
1767 });
1768
1769 let mut ix = 0;
1770 while ix + 1 < ranges.len() {
1771 let b = ranges[ix + 1].clone();
1772 let a = &mut ranges[ix];
1773 if a.end.cmp(&b.start, buffer).is_gt() {
1774 if a.end.cmp(&b.end, buffer).is_lt() {
1775 a.end = b.end;
1776 }
1777 ranges.remove(ix + 1);
1778 } else {
1779 ix += 1;
1780 }
1781 }
1782}
1783
1784#[cfg(test)]
1785mod tests {
1786 use std::sync::Arc;
1787
1788 use crate::FakeCompletionProvider;
1789
1790 use super::*;
1791 use futures::stream::{self};
1792 use gpui::{Context, TestAppContext};
1793 use indoc::indoc;
1794 use language::{
1795 language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
1796 Point,
1797 };
1798 use rand::prelude::*;
1799 use serde::Serialize;
1800 use settings::SettingsStore;
1801
1802 #[derive(Serialize)]
1803 pub struct DummyCompletionRequest {
1804 pub name: String,
1805 }
1806
1807 #[gpui::test(iterations = 10)]
1808 async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
1809 let provider = FakeCompletionProvider::default();
1810 cx.set_global(cx.update(SettingsStore::test));
1811 cx.set_global(CompletionProvider::Fake(provider.clone()));
1812 cx.update(language_settings::init);
1813
1814 let text = indoc! {"
1815 fn main() {
1816 let x = 0;
1817 for _ in 0..10 {
1818 x += 1;
1819 }
1820 }
1821 "};
1822 let buffer =
1823 cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
1824 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
1825 let range = buffer.read_with(cx, |buffer, cx| {
1826 let snapshot = buffer.snapshot(cx);
1827 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
1828 });
1829 let codegen = cx.new_model(|cx| {
1830 Codegen::new(buffer.clone(), CodegenKind::Transform { range }, None, cx)
1831 });
1832
1833 let request = LanguageModelRequest::default();
1834 codegen.update(cx, |codegen, cx| codegen.start(request, cx));
1835
1836 let mut new_text = concat!(
1837 " let mut x = 0;\n",
1838 " while x < 10 {\n",
1839 " x += 1;\n",
1840 " }",
1841 );
1842 while !new_text.is_empty() {
1843 let max_len = cmp::min(new_text.len(), 10);
1844 let len = rng.gen_range(1..=max_len);
1845 let (chunk, suffix) = new_text.split_at(len);
1846 provider.send_completion(chunk.into());
1847 new_text = suffix;
1848 cx.background_executor.run_until_parked();
1849 }
1850 provider.finish_completion();
1851 cx.background_executor.run_until_parked();
1852
1853 assert_eq!(
1854 buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
1855 indoc! {"
1856 fn main() {
1857 let mut x = 0;
1858 while x < 10 {
1859 x += 1;
1860 }
1861 }
1862 "}
1863 );
1864 }
1865
1866 #[gpui::test(iterations = 10)]
1867 async fn test_autoindent_when_generating_past_indentation(
1868 cx: &mut TestAppContext,
1869 mut rng: StdRng,
1870 ) {
1871 let provider = FakeCompletionProvider::default();
1872 cx.set_global(CompletionProvider::Fake(provider.clone()));
1873 cx.set_global(cx.update(SettingsStore::test));
1874 cx.update(language_settings::init);
1875
1876 let text = indoc! {"
1877 fn main() {
1878 le
1879 }
1880 "};
1881 let buffer =
1882 cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
1883 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
1884 let position = buffer.read_with(cx, |buffer, cx| {
1885 let snapshot = buffer.snapshot(cx);
1886 snapshot.anchor_before(Point::new(1, 6))
1887 });
1888 let codegen = cx.new_model(|cx| {
1889 Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
1890 });
1891
1892 let request = LanguageModelRequest::default();
1893 codegen.update(cx, |codegen, cx| codegen.start(request, cx));
1894
1895 let mut new_text = concat!(
1896 "t mut x = 0;\n",
1897 "while x < 10 {\n",
1898 " x += 1;\n",
1899 "}", //
1900 );
1901 while !new_text.is_empty() {
1902 let max_len = cmp::min(new_text.len(), 10);
1903 let len = rng.gen_range(1..=max_len);
1904 let (chunk, suffix) = new_text.split_at(len);
1905 provider.send_completion(chunk.into());
1906 new_text = suffix;
1907 cx.background_executor.run_until_parked();
1908 }
1909 provider.finish_completion();
1910 cx.background_executor.run_until_parked();
1911
1912 assert_eq!(
1913 buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
1914 indoc! {"
1915 fn main() {
1916 let mut x = 0;
1917 while x < 10 {
1918 x += 1;
1919 }
1920 }
1921 "}
1922 );
1923 }
1924
1925 #[gpui::test(iterations = 10)]
1926 async fn test_autoindent_when_generating_before_indentation(
1927 cx: &mut TestAppContext,
1928 mut rng: StdRng,
1929 ) {
1930 let provider = FakeCompletionProvider::default();
1931 cx.set_global(CompletionProvider::Fake(provider.clone()));
1932 cx.set_global(cx.update(SettingsStore::test));
1933 cx.update(language_settings::init);
1934
1935 let text = concat!(
1936 "fn main() {\n",
1937 " \n",
1938 "}\n" //
1939 );
1940 let buffer =
1941 cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
1942 let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
1943 let position = buffer.read_with(cx, |buffer, cx| {
1944 let snapshot = buffer.snapshot(cx);
1945 snapshot.anchor_before(Point::new(1, 2))
1946 });
1947 let codegen = cx.new_model(|cx| {
1948 Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
1949 });
1950
1951 let request = LanguageModelRequest::default();
1952 codegen.update(cx, |codegen, cx| codegen.start(request, cx));
1953
1954 let mut new_text = concat!(
1955 "let mut x = 0;\n",
1956 "while x < 10 {\n",
1957 " x += 1;\n",
1958 "}", //
1959 );
1960 while !new_text.is_empty() {
1961 let max_len = cmp::min(new_text.len(), 10);
1962 let len = rng.gen_range(1..=max_len);
1963 let (chunk, suffix) = new_text.split_at(len);
1964 provider.send_completion(chunk.into());
1965 new_text = suffix;
1966 cx.background_executor.run_until_parked();
1967 }
1968 provider.finish_completion();
1969 cx.background_executor.run_until_parked();
1970
1971 assert_eq!(
1972 buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
1973 indoc! {"
1974 fn main() {
1975 let mut x = 0;
1976 while x < 10 {
1977 x += 1;
1978 }
1979 }
1980 "}
1981 );
1982 }
1983
1984 #[gpui::test]
1985 async fn test_strip_invalid_spans_from_codeblock() {
1986 assert_eq!(
1987 strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
1988 .map(|chunk| chunk.unwrap())
1989 .collect::<String>()
1990 .await,
1991 "Lorem ipsum dolor"
1992 );
1993 assert_eq!(
1994 strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
1995 .map(|chunk| chunk.unwrap())
1996 .collect::<String>()
1997 .await,
1998 "Lorem ipsum dolor"
1999 );
2000 assert_eq!(
2001 strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
2002 .map(|chunk| chunk.unwrap())
2003 .collect::<String>()
2004 .await,
2005 "Lorem ipsum dolor"
2006 );
2007 assert_eq!(
2008 strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
2009 .map(|chunk| chunk.unwrap())
2010 .collect::<String>()
2011 .await,
2012 "Lorem ipsum dolor"
2013 );
2014 assert_eq!(
2015 strip_invalid_spans_from_codeblock(chunks(
2016 "```html\n```js\nLorem ipsum dolor\n```\n```",
2017 2
2018 ))
2019 .map(|chunk| chunk.unwrap())
2020 .collect::<String>()
2021 .await,
2022 "```js\nLorem ipsum dolor\n```"
2023 );
2024 assert_eq!(
2025 strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
2026 .map(|chunk| chunk.unwrap())
2027 .collect::<String>()
2028 .await,
2029 "``\nLorem ipsum dolor\n```"
2030 );
2031 assert_eq!(
2032 strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
2033 .map(|chunk| chunk.unwrap())
2034 .collect::<String>()
2035 .await,
2036 "Lorem ipsum"
2037 );
2038
2039 assert_eq!(
2040 strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
2041 .map(|chunk| chunk.unwrap())
2042 .collect::<String>()
2043 .await,
2044 "Lorem ipsum"
2045 );
2046
2047 assert_eq!(
2048 strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
2049 .map(|chunk| chunk.unwrap())
2050 .collect::<String>()
2051 .await,
2052 "Lorem ipsum"
2053 );
2054 assert_eq!(
2055 strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
2056 .map(|chunk| chunk.unwrap())
2057 .collect::<String>()
2058 .await,
2059 "Lorem ipsum"
2060 );
2061 fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
2062 stream::iter(
2063 text.chars()
2064 .collect::<Vec<_>>()
2065 .chunks(size)
2066 .map(|chunk| Ok(chunk.iter().collect::<String>()))
2067 .collect::<Vec<_>>(),
2068 )
2069 }
2070 }
2071
2072 fn rust_lang() -> Language {
2073 Language::new(
2074 LanguageConfig {
2075 name: "Rust".into(),
2076 matcher: LanguageMatcher {
2077 path_suffixes: vec!["rs".to_string()],
2078 ..Default::default()
2079 },
2080 ..Default::default()
2081 },
2082 Some(tree_sitter_rust::language()),
2083 )
2084 .with_indents_query(
2085 r#"
2086 (call_expression) @indent
2087 (field_expression) @indent
2088 (_ "(" ")" @end) @indent
2089 (_ "{" "}" @end) @indent
2090 "#,
2091 )
2092 .unwrap()
2093 }
2094}