zeta1.rs

  1use std::{fmt::Write, ops::Range, path::Path, sync::Arc, time::Instant};
  2
  3use crate::{
  4    DebugEvent, EditPredictionFinishedDebugEvent, EditPredictionId, EditPredictionModelInput,
  5    EditPredictionStartedDebugEvent, EditPredictionStore, ZedUpdateRequiredError,
  6    cursor_excerpt::{editable_and_context_ranges_for_cursor_position, guess_token_count},
  7    prediction::EditPredictionResult,
  8};
  9use anyhow::Result;
 10use cloud_llm_client::{
 11    PredictEditsBody, PredictEditsGitInfo, PredictEditsRequestTrigger, PredictEditsResponse,
 12};
 13use edit_prediction_types::PredictedCursorPosition;
 14use gpui::{App, AppContext as _, AsyncApp, Context, Entity, SharedString, Task};
 15use language::{
 16    Anchor, Buffer, BufferSnapshot, OffsetRangeExt as _, Point, ToOffset, ToPoint as _, text_diff,
 17};
 18use project::{Project, ProjectPath};
 19use release_channel::AppVersion;
 20use text::Bias;
 21use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification};
 22use zeta_prompt::{
 23    Event, ZetaPromptInput,
 24    zeta1::{
 25        CURSOR_MARKER, EDITABLE_REGION_END_MARKER, EDITABLE_REGION_START_MARKER,
 26        START_OF_FILE_MARKER,
 27    },
 28};
 29
 30pub(crate) const MAX_CONTEXT_TOKENS: usize = 150;
 31pub(crate) const MAX_REWRITE_TOKENS: usize = 350;
 32pub(crate) const MAX_EVENT_TOKENS: usize = 500;
 33
 34pub(crate) fn request_prediction_with_zeta1(
 35    store: &mut EditPredictionStore,
 36    EditPredictionModelInput {
 37        project,
 38        buffer,
 39        snapshot,
 40        position,
 41        events,
 42        trigger,
 43        debug_tx,
 44        ..
 45    }: EditPredictionModelInput,
 46    cx: &mut Context<EditPredictionStore>,
 47) -> Task<Result<Option<EditPredictionResult>>> {
 48    let buffer_snapshotted_at = Instant::now();
 49    let client = store.client.clone();
 50    let llm_token = store.llm_token.clone();
 51    let app_version = AppVersion::global(cx);
 52
 53    let (git_info, can_collect_file) = if let Some(file) = snapshot.file() {
 54        let can_collect_file = store.can_collect_file(&project, file, cx);
 55        let git_info = if can_collect_file {
 56            git_info_for_file(&project, &ProjectPath::from_file(file.as_ref(), cx), cx)
 57        } else {
 58            None
 59        };
 60        (git_info, can_collect_file)
 61    } else {
 62        (None, false)
 63    };
 64
 65    let full_path: Arc<Path> = snapshot
 66        .file()
 67        .map(|f| Arc::from(f.full_path(cx).as_path()))
 68        .unwrap_or_else(|| Arc::from(Path::new("untitled")));
 69    let full_path_str = full_path.to_string_lossy().into_owned();
 70    let cursor_point = position.to_point(&snapshot);
 71    let prompt_for_events = {
 72        let events = events.clone();
 73        move || prompt_for_events_impl(&events, MAX_EVENT_TOKENS)
 74    };
 75    let gather_task = gather_context(
 76        full_path_str,
 77        &snapshot,
 78        cursor_point,
 79        prompt_for_events,
 80        trigger,
 81        cx,
 82    );
 83
 84    let uri = match client
 85        .http_client()
 86        .build_zed_llm_url("/predict_edits/v2", &[])
 87    {
 88        Ok(url) => Arc::from(url),
 89        Err(err) => return Task::ready(Err(err)),
 90    };
 91
 92    cx.spawn(async move |this, cx| {
 93        let GatherContextOutput {
 94            mut body,
 95            context_range,
 96            editable_range,
 97            included_events_count,
 98        } = gather_task.await?;
 99        let done_gathering_context_at = Instant::now();
100
101        let included_events = &events[events.len() - included_events_count..events.len()];
102        body.can_collect_data = can_collect_file
103            && this
104                .read_with(cx, |this, cx| this.can_collect_events(included_events, cx))
105                .unwrap_or(false);
106        if body.can_collect_data {
107            body.git_info = git_info;
108        }
109
110        log::debug!(
111            "Events:\n{}\nExcerpt:\n{:?}",
112            body.input_events,
113            body.input_excerpt
114        );
115
116        let response = EditPredictionStore::send_api_request::<PredictEditsResponse>(
117            |request| {
118                Ok(request
119                    .uri(uri.as_str())
120                    .body(serde_json::to_string(&body)?.into())?)
121            },
122            client,
123            llm_token,
124            app_version,
125            true,
126        )
127        .await;
128
129        let context_start_offset = context_range.start.to_offset(&snapshot);
130        let context_start_row = context_range.start.row;
131        let editable_offset_range = editable_range.to_offset(&snapshot);
132
133        let inputs = ZetaPromptInput {
134            events: included_events.into(),
135            related_files: vec![],
136            cursor_path: full_path,
137            cursor_excerpt: snapshot
138                .text_for_range(context_range)
139                .collect::<String>()
140                .into(),
141            editable_range_in_excerpt: (editable_range.start - context_start_offset)
142                ..(editable_offset_range.end - context_start_offset),
143            cursor_offset_in_excerpt: cursor_point.to_offset(&snapshot) - context_start_offset,
144            excerpt_start_row: Some(context_start_row),
145        };
146
147        if let Some(debug_tx) = &debug_tx {
148            debug_tx
149                .unbounded_send(DebugEvent::EditPredictionStarted(
150                    EditPredictionStartedDebugEvent {
151                        buffer: buffer.downgrade(),
152                        prompt: Some(serde_json::to_string(&inputs).unwrap()),
153                        position,
154                    },
155                ))
156                .ok();
157        }
158
159        let (response, usage) = match response {
160            Ok(response) => response,
161            Err(err) => {
162                if err.is::<ZedUpdateRequiredError>() {
163                    cx.update(|cx| {
164                        this.update(cx, |ep_store, _cx| {
165                            ep_store.update_required = true;
166                        })
167                        .ok();
168
169                        let error_message: SharedString = err.to_string().into();
170                        show_app_notification(
171                            NotificationId::unique::<ZedUpdateRequiredError>(),
172                            cx,
173                            move |cx| {
174                                cx.new(|cx| {
175                                    ErrorMessagePrompt::new(error_message.clone(), cx)
176                                        .with_link_button("Update Zed", "https://zed.dev/releases")
177                                })
178                            },
179                        );
180                    });
181                }
182
183                return Err(err);
184            }
185        };
186
187        let received_response_at = Instant::now();
188        log::debug!("completion response: {}", &response.output_excerpt);
189
190        if let Some(usage) = usage {
191            this.update(cx, |this, cx| {
192                this.user_store.update(cx, |user_store, cx| {
193                    user_store.update_edit_prediction_usage(usage, cx);
194                });
195            })
196            .ok();
197        }
198
199        if let Some(debug_tx) = &debug_tx {
200            debug_tx
201                .unbounded_send(DebugEvent::EditPredictionFinished(
202                    EditPredictionFinishedDebugEvent {
203                        buffer: buffer.downgrade(),
204                        model_output: Some(response.output_excerpt.clone()),
205                        position,
206                    },
207                ))
208                .ok();
209        }
210
211        let edit_prediction = process_completion_response(
212            response,
213            buffer,
214            &snapshot,
215            editable_range,
216            inputs,
217            buffer_snapshotted_at,
218            received_response_at,
219            cx,
220        )
221        .await;
222
223        let finished_at = Instant::now();
224
225        // record latency for ~1% of requests
226        if rand::random::<u8>() <= 2 {
227            telemetry::event!(
228                "Edit Prediction Request",
229                context_latency = done_gathering_context_at
230                    .duration_since(buffer_snapshotted_at)
231                    .as_millis(),
232                request_latency = received_response_at
233                    .duration_since(done_gathering_context_at)
234                    .as_millis(),
235                process_latency = finished_at.duration_since(received_response_at).as_millis()
236            );
237        }
238
239        edit_prediction.map(Some)
240    })
241}
242
243fn process_completion_response(
244    prediction_response: PredictEditsResponse,
245    buffer: Entity<Buffer>,
246    snapshot: &BufferSnapshot,
247    editable_range: Range<usize>,
248    inputs: ZetaPromptInput,
249    buffer_snapshotted_at: Instant,
250    received_response_at: Instant,
251    cx: &AsyncApp,
252) -> Task<Result<EditPredictionResult>> {
253    let snapshot = snapshot.clone();
254    let request_id = prediction_response.request_id;
255    let output_excerpt = prediction_response.output_excerpt;
256    cx.spawn(async move |cx| {
257        let output_excerpt: Arc<str> = output_excerpt.into();
258
259        let edits: Arc<[(Range<Anchor>, Arc<str>)]> = cx
260            .background_spawn({
261                let output_excerpt = output_excerpt.clone();
262                let editable_range = editable_range.clone();
263                let snapshot = snapshot.clone();
264                async move { parse_edits(output_excerpt.as_ref(), editable_range, &snapshot) }
265            })
266            .await?
267            .into();
268
269        let id = EditPredictionId(request_id.into());
270        Ok(EditPredictionResult::new(
271            id,
272            &buffer,
273            &snapshot,
274            edits,
275            None,
276            buffer_snapshotted_at,
277            received_response_at,
278            inputs,
279            cx,
280        )
281        .await)
282    })
283}
284
285pub(crate) fn parse_edits(
286    output_excerpt: &str,
287    editable_range: Range<usize>,
288    snapshot: &BufferSnapshot,
289) -> Result<Vec<(Range<Anchor>, Arc<str>)>> {
290    let content = output_excerpt.replace(CURSOR_MARKER, "");
291
292    let start_markers = content
293        .match_indices(EDITABLE_REGION_START_MARKER)
294        .collect::<Vec<_>>();
295    anyhow::ensure!(
296        start_markers.len() <= 1,
297        "expected at most one start marker, found {}",
298        start_markers.len()
299    );
300
301    let end_markers = content
302        .match_indices(EDITABLE_REGION_END_MARKER)
303        .collect::<Vec<_>>();
304    anyhow::ensure!(
305        end_markers.len() <= 1,
306        "expected at most one end marker, found {}",
307        end_markers.len()
308    );
309
310    let sof_markers = content
311        .match_indices(START_OF_FILE_MARKER)
312        .collect::<Vec<_>>();
313    anyhow::ensure!(
314        sof_markers.len() <= 1,
315        "expected at most one start-of-file marker, found {}",
316        sof_markers.len()
317    );
318
319    let content_start = start_markers
320        .first()
321        .map(|e| e.0 + EDITABLE_REGION_START_MARKER.len())
322        .map(|start| {
323            if content.len() > start
324                && content.is_char_boundary(start)
325                && content[start..].starts_with('\n')
326            {
327                start + 1
328            } else {
329                start
330            }
331        })
332        .unwrap_or(0);
333    let content_end = end_markers
334        .first()
335        .map(|e| {
336            if e.0 > 0 && content.is_char_boundary(e.0 - 1) && content[e.0 - 1..].starts_with('\n')
337            {
338                e.0 - 1
339            } else {
340                e.0
341            }
342        })
343        .unwrap_or(content.strip_suffix("\n").unwrap_or(&content).len());
344
345    // min to account for content_end and content_start both accounting for the same newline in the following case:
346    // <|editable_region_start|>\n<|editable_region_end|>
347    let new_text = &content[content_start.min(content_end)..content_end];
348
349    let old_text = snapshot
350        .text_for_range(editable_range.clone())
351        .collect::<String>();
352
353    Ok(compute_edits(
354        old_text,
355        new_text,
356        editable_range.start,
357        snapshot,
358    ))
359}
360
361pub fn compute_edits(
362    old_text: String,
363    new_text: &str,
364    offset: usize,
365    snapshot: &BufferSnapshot,
366) -> Vec<(Range<Anchor>, Arc<str>)> {
367    compute_edits_and_cursor_position(old_text, new_text, offset, None, snapshot).0
368}
369
370pub fn compute_edits_and_cursor_position(
371    old_text: String,
372    new_text: &str,
373    offset: usize,
374    cursor_offset_in_new_text: Option<usize>,
375    snapshot: &BufferSnapshot,
376) -> (
377    Vec<(Range<Anchor>, Arc<str>)>,
378    Option<PredictedCursorPosition>,
379) {
380    let diffs = text_diff(&old_text, new_text);
381
382    // Delta represents the cumulative change in byte count from all preceding edits.
383    // new_offset = old_offset + delta, so old_offset = new_offset - delta
384    let mut delta: isize = 0;
385    let mut cursor_position: Option<PredictedCursorPosition> = None;
386
387    let edits = diffs
388        .iter()
389        .map(|(raw_old_range, new_text)| {
390            // Compute cursor position if it falls within or before this edit.
391            if let (Some(cursor_offset), None) = (cursor_offset_in_new_text, cursor_position) {
392                let edit_start_in_new = (raw_old_range.start as isize + delta) as usize;
393                let edit_end_in_new = edit_start_in_new + new_text.len();
394
395                if cursor_offset < edit_start_in_new {
396                    let cursor_in_old = (cursor_offset as isize - delta) as usize;
397                    cursor_position = Some(PredictedCursorPosition::at_anchor(
398                        snapshot.anchor_after(offset + cursor_in_old),
399                    ));
400                } else if cursor_offset < edit_end_in_new {
401                    let offset_within_insertion = cursor_offset - edit_start_in_new;
402                    cursor_position = Some(PredictedCursorPosition::new(
403                        snapshot.anchor_before(offset + raw_old_range.start),
404                        offset_within_insertion,
405                    ));
406                }
407
408                delta += new_text.len() as isize - raw_old_range.len() as isize;
409            }
410
411            // Compute the edit with prefix/suffix trimming.
412            let mut old_range = raw_old_range.clone();
413            let old_slice = &old_text[old_range.clone()];
414
415            let prefix_len = common_prefix(old_slice.chars(), new_text.chars());
416            let suffix_len = common_prefix(
417                old_slice[prefix_len..].chars().rev(),
418                new_text[prefix_len..].chars().rev(),
419            );
420
421            old_range.start += offset;
422            old_range.end += offset;
423            old_range.start += prefix_len;
424            old_range.end -= suffix_len;
425
426            let new_text = new_text[prefix_len..new_text.len() - suffix_len].into();
427            let range = if old_range.is_empty() {
428                let anchor = snapshot.anchor_after(old_range.start);
429                anchor..anchor
430            } else {
431                snapshot.anchor_after(old_range.start)..snapshot.anchor_before(old_range.end)
432            };
433            (range, new_text)
434        })
435        .collect();
436
437    if let (Some(cursor_offset), None) = (cursor_offset_in_new_text, cursor_position) {
438        let cursor_in_old = (cursor_offset as isize - delta) as usize;
439        let buffer_offset = snapshot.clip_offset(offset + cursor_in_old, Bias::Right);
440        cursor_position = Some(PredictedCursorPosition::at_anchor(
441            snapshot.anchor_after(buffer_offset),
442        ));
443    }
444
445    (edits, cursor_position)
446}
447
448fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
449    a.zip(b)
450        .take_while(|(a, b)| a == b)
451        .map(|(a, _)| a.len_utf8())
452        .sum()
453}
454
455fn git_info_for_file(
456    project: &Entity<Project>,
457    project_path: &ProjectPath,
458    cx: &App,
459) -> Option<PredictEditsGitInfo> {
460    let git_store = project.read(cx).git_store().read(cx);
461    if let Some((repository, _repo_path)) =
462        git_store.repository_and_path_for_project_path(project_path, cx)
463    {
464        let repository = repository.read(cx);
465        let head_sha = repository
466            .head_commit
467            .as_ref()
468            .map(|head_commit| head_commit.sha.to_string());
469        let remote_origin_url = repository.remote_origin_url.clone();
470        let remote_upstream_url = repository.remote_upstream_url.clone();
471        if head_sha.is_none() && remote_origin_url.is_none() && remote_upstream_url.is_none() {
472            return None;
473        }
474        Some(PredictEditsGitInfo {
475            head_sha,
476            remote_origin_url,
477            remote_upstream_url,
478        })
479    } else {
480        None
481    }
482}
483
484pub struct GatherContextOutput {
485    pub body: PredictEditsBody,
486    pub context_range: Range<Point>,
487    pub editable_range: Range<usize>,
488    pub included_events_count: usize,
489}
490
491pub fn gather_context(
492    full_path_str: String,
493    snapshot: &BufferSnapshot,
494    cursor_point: language::Point,
495    prompt_for_events: impl FnOnce() -> (String, usize) + Send + 'static,
496    trigger: PredictEditsRequestTrigger,
497    cx: &App,
498) -> Task<Result<GatherContextOutput>> {
499    cx.background_spawn({
500        let snapshot = snapshot.clone();
501        async move {
502            let input_excerpt = excerpt_for_cursor_position(
503                cursor_point,
504                &full_path_str,
505                &snapshot,
506                MAX_REWRITE_TOKENS,
507                MAX_CONTEXT_TOKENS,
508            );
509            let (input_events, included_events_count) = prompt_for_events();
510            let editable_range = input_excerpt.editable_range.to_offset(&snapshot);
511
512            let body = PredictEditsBody {
513                input_events,
514                input_excerpt: input_excerpt.prompt,
515                can_collect_data: false,
516                diagnostic_groups: None,
517                git_info: None,
518                outline: None,
519                speculated_output: None,
520                trigger,
521            };
522
523            Ok(GatherContextOutput {
524                body,
525                context_range: input_excerpt.context_range,
526                editable_range,
527                included_events_count,
528            })
529        }
530    })
531}
532
533pub(crate) fn prompt_for_events(events: &[Arc<Event>], max_tokens: usize) -> String {
534    prompt_for_events_impl(events, max_tokens).0
535}
536
537fn prompt_for_events_impl(events: &[Arc<Event>], mut remaining_tokens: usize) -> (String, usize) {
538    let mut result = String::new();
539    for (ix, event) in events.iter().rev().enumerate() {
540        let event_string = format_event(event.as_ref());
541        let event_tokens = guess_token_count(event_string.len());
542        if event_tokens > remaining_tokens {
543            return (result, ix);
544        }
545
546        if !result.is_empty() {
547            result.insert_str(0, "\n\n");
548        }
549        result.insert_str(0, &event_string);
550        remaining_tokens -= event_tokens;
551    }
552    return (result, events.len());
553}
554
555pub fn format_event(event: &Event) -> String {
556    match event {
557        Event::BufferChange {
558            path,
559            old_path,
560            diff,
561            ..
562        } => {
563            let mut prompt = String::new();
564
565            if old_path != path {
566                writeln!(
567                    prompt,
568                    "User renamed {} to {}\n",
569                    old_path.display(),
570                    path.display()
571                )
572                .unwrap();
573            }
574
575            if !diff.is_empty() {
576                write!(
577                    prompt,
578                    "User edited {}:\n```diff\n{}\n```",
579                    path.display(),
580                    diff
581                )
582                .unwrap();
583            }
584
585            prompt
586        }
587    }
588}
589
590#[derive(Debug)]
591pub struct InputExcerpt {
592    pub context_range: Range<Point>,
593    pub editable_range: Range<Point>,
594    pub prompt: String,
595}
596
597pub fn excerpt_for_cursor_position(
598    position: Point,
599    path: &str,
600    snapshot: &BufferSnapshot,
601    editable_region_token_limit: usize,
602    context_token_limit: usize,
603) -> InputExcerpt {
604    let (editable_range, context_range) = editable_and_context_ranges_for_cursor_position(
605        position,
606        snapshot,
607        editable_region_token_limit,
608        context_token_limit,
609    );
610
611    let mut prompt = String::new();
612
613    writeln!(&mut prompt, "```{path}").unwrap();
614    if context_range.start == Point::zero() {
615        writeln!(&mut prompt, "{START_OF_FILE_MARKER}").unwrap();
616    }
617
618    for chunk in snapshot.chunks(context_range.start..editable_range.start, false) {
619        prompt.push_str(chunk.text);
620    }
621
622    push_editable_range(position, snapshot, editable_range.clone(), &mut prompt);
623
624    for chunk in snapshot.chunks(editable_range.end..context_range.end, false) {
625        prompt.push_str(chunk.text);
626    }
627    write!(prompt, "\n```").unwrap();
628
629    InputExcerpt {
630        context_range,
631        editable_range,
632        prompt,
633    }
634}
635
636fn push_editable_range(
637    cursor_position: Point,
638    snapshot: &BufferSnapshot,
639    editable_range: Range<Point>,
640    prompt: &mut String,
641) {
642    writeln!(prompt, "{EDITABLE_REGION_START_MARKER}").unwrap();
643    for chunk in snapshot.chunks(editable_range.start..cursor_position, false) {
644        prompt.push_str(chunk.text);
645    }
646    prompt.push_str(CURSOR_MARKER);
647    for chunk in snapshot.chunks(cursor_position..editable_range.end, false) {
648        prompt.push_str(chunk.text);
649    }
650    write!(prompt, "\n{EDITABLE_REGION_END_MARKER}").unwrap();
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656    use gpui::{App, AppContext};
657    use indoc::indoc;
658    use language::Buffer;
659
660    #[gpui::test]
661    fn test_excerpt_for_cursor_position(cx: &mut App) {
662        let text = indoc! {r#"
663            fn foo() {
664                let x = 42;
665                println!("Hello, world!");
666            }
667
668            fn bar() {
669                let x = 42;
670                let mut sum = 0;
671                for i in 0..x {
672                    sum += i;
673                }
674                println!("Sum: {}", sum);
675                return sum;
676            }
677
678            fn generate_random_numbers() -> Vec<i32> {
679                let mut rng = rand::thread_rng();
680                let mut numbers = Vec::new();
681                for _ in 0..5 {
682                    numbers.push(rng.random_range(1..101));
683                }
684                numbers
685            }
686        "#};
687        let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language::rust_lang(), cx));
688        let snapshot = buffer.read(cx).snapshot();
689
690        // The excerpt expands to syntax boundaries.
691        // With 50 token editable limit, we get a region that expands to syntax nodes.
692        let excerpt = excerpt_for_cursor_position(Point::new(12, 5), "main.rs", &snapshot, 50, 32);
693        assert_eq!(
694            excerpt.prompt,
695            indoc! {r#"
696            ```main.rs
697
698            fn bar() {
699                let x = 42;
700            <|editable_region_start|>
701                let mut sum = 0;
702                for i in 0..x {
703                    sum += i;
704                }
705                println!("Sum: {}", sum);
706                r<|user_cursor_is_here|>eturn sum;
707            }
708
709            fn generate_random_numbers() -> Vec<i32> {
710            <|editable_region_end|>
711                let mut rng = rand::thread_rng();
712                let mut numbers = Vec::new();
713            ```"#}
714        );
715
716        // With smaller budget, the region expands to syntax boundaries but is tighter.
717        let excerpt = excerpt_for_cursor_position(Point::new(12, 5), "main.rs", &snapshot, 40, 32);
718        assert_eq!(
719            excerpt.prompt,
720            indoc! {r#"
721            ```main.rs
722            fn bar() {
723                let x = 42;
724                let mut sum = 0;
725                for i in 0..x {
726            <|editable_region_start|>
727                    sum += i;
728                }
729                println!("Sum: {}", sum);
730                r<|user_cursor_is_here|>eturn sum;
731            }
732
733            fn generate_random_numbers() -> Vec<i32> {
734            <|editable_region_end|>
735                let mut rng = rand::thread_rng();
736            ```"#}
737        );
738    }
739
740    #[gpui::test]
741    fn test_parse_edits_empty_editable_region(cx: &mut App) {
742        let text = "fn foo() {\n    let x = 42;\n}\n";
743        let buffer = cx.new(|cx| Buffer::local(text, cx));
744        let snapshot = buffer.read(cx).snapshot();
745
746        let output = "<|editable_region_start|>\n<|editable_region_end|>";
747        let editable_range = 0..text.len();
748        let edits = parse_edits(output, editable_range, &snapshot).unwrap();
749        assert_eq!(edits.len(), 1);
750        let (range, new_text) = &edits[0];
751        assert_eq!(range.to_offset(&snapshot), 0..text.len(),);
752        assert_eq!(new_text.as_ref(), "");
753    }
754
755    #[gpui::test]
756    fn test_parse_edits_multibyte_char_before_end_marker(cx: &mut App) {
757        let text = "// café";
758        let buffer = cx.new(|cx| Buffer::local(text, cx));
759        let snapshot = buffer.read(cx).snapshot();
760
761        let output = "<|editable_region_start|>\n// café<|editable_region_end|>";
762        let editable_range = 0..text.len();
763
764        let edits = parse_edits(output, editable_range, &snapshot).unwrap();
765        assert_eq!(edits, vec![]);
766    }
767
768    #[gpui::test]
769    fn test_parse_edits_multibyte_char_after_start_marker(cx: &mut App) {
770        let text = "é is great";
771        let buffer = cx.new(|cx| Buffer::local(text, cx));
772        let snapshot = buffer.read(cx).snapshot();
773
774        let output = "<|editable_region_start|>é is great\n<|editable_region_end|>";
775        let editable_range = 0..text.len();
776
777        let edits = parse_edits(output, editable_range, &snapshot).unwrap();
778        assert!(edits.is_empty());
779    }
780}