Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:06:23.244Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

zeta_prompt.rs

7142 lines · 243.4 KB · rust
1pub mod excerpt_ranges;
2pub mod hashed_regions;
3pub mod multi_region;
4pub mod udiff;
5
6use anyhow::{Result, anyhow};
7use serde::{Deserialize, Serialize};
8use std::fmt::Write;
9use std::ops::Range;
10use std::path::Path;
11use std::sync::Arc;
12use strum::{EnumIter, IntoEnumIterator as _, IntoStaticStr};
13
14pub use crate::excerpt_ranges::{
15    ExcerptRanges, compute_editable_and_context_ranges, compute_legacy_excerpt_ranges,
16};
17
18pub const CURSOR_MARKER: &str = "<|user_cursor|>";
19
20/// Use up to this amount of the editable region for prefill.
21/// Larger values may result in more robust generation, but
22/// this region becomes non-editable.
23pub const PREFILL_RATIO: f64 = 0.1; // 10%
24
25fn estimate_tokens(bytes: usize) -> usize {
26    bytes / 3
27}
28
29/// Leave some slack to avoid overflow.
30fn apply_prompt_budget_margin(max_tokens: usize) -> usize {
31    (max_tokens as f64 * 0.9).floor() as usize
32}
33
34/// Ensure text fits into the tokens budget; trim by line boundaries if needed.
35pub fn clamp_text_to_token_count(text: &str, max_tokens: usize) -> &str {
36    if estimate_tokens(text.len()) <= max_tokens {
37        return text;
38    }
39
40    let mut end_byte_offset = 0;
41
42    for line in text.split_inclusive('\n') {
43        if estimate_tokens(line.len() + end_byte_offset) > max_tokens {
44            break;
45        }
46
47        end_byte_offset += line.len();
48    }
49
50    &text[..end_byte_offset]
51}
52
53#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
54pub struct Zeta2PromptInput {
55    pub cursor_path: Arc<Path>,
56    pub cursor_excerpt: Arc<str>,
57    pub cursor_offset_in_excerpt: usize,
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub excerpt_start_row: Option<u32>,
60    pub events: Vec<Arc<Event>>,
61    #[serde(default)]
62    pub related_files: Option<Vec<RelatedFile>>,
63    #[serde(default, skip_serializing_if = "Vec::is_empty")]
64    pub active_buffer_diagnostics: Vec<ActiveBufferDiagnostic>,
65    /// These ranges let the server select model-appropriate subsets.
66    pub excerpt_ranges: ExcerptRanges,
67    /// Byte offset ranges within `cursor_excerpt` for all syntax nodes that
68    /// contain `cursor_offset_in_excerpt`, ordered from innermost to outermost.
69    /// When present, the server uses these to compute editable/context ranges
70    /// instead of `excerpt_ranges`.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub syntax_ranges: Option<Vec<Range<usize>>>,
73    #[serde(default)]
74    pub in_open_source_repo: bool,
75    #[serde(default)]
76    pub can_collect_data: bool,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub repo_url: Option<String>,
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
82pub struct FilePosition {
83    pub row: u32,
84    pub column: u32,
85}
86
87#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
88pub struct Zeta3PromptInput {
89    pub cursor_path: Arc<Path>,
90    pub cursor_position: FilePosition,
91    pub events: Vec<Arc<Event>>,
92    pub editable_context: Vec<RelatedFile>,
93    #[serde(default, skip_serializing_if = "Vec::is_empty")]
94    pub syntax_ranges: Vec<Range<usize>>,
95    #[serde(default, skip_serializing_if = "Vec::is_empty")]
96    pub active_buffer_diagnostics: Vec<ActiveBufferDiagnostic>,
97    #[serde(default)]
98    pub in_open_source_repo: bool,
99    #[serde(default)]
100    pub can_collect_data: bool,
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub repo_url: Option<String>,
103}
104
105#[derive(
106    Default,
107    Clone,
108    Copy,
109    Debug,
110    PartialEq,
111    Eq,
112    Hash,
113    EnumIter,
114    IntoStaticStr,
115    Serialize,
116    Deserialize,
117)]
118#[allow(non_camel_case_types)]
119pub enum ZetaFormat {
120    V0112MiddleAtEnd,
121    V0113Ordered,
122    V0114180EditableRegion,
123    V0120GitMergeMarkers,
124    #[default]
125    V0131GitMergeMarkersPrefix,
126    V0211Prefill,
127    #[serde(alias = "Zeta2")]
128    V0211SeedCoder,
129    V0331SeedCoderModelPy,
130    v0226Hashline,
131    V0304VariableEdit,
132    V0304SeedNoEdits,
133    /// Multi-block marker spans with NO_EDITS sentinel.
134    V0306SeedMultiRegions,
135    /// Byte-exact marker spans; all intermediate markers emitted; repeated marker means no-edit.
136    V0316SeedMultiRegions,
137    /// V0316, but marker numbers are relative to the cursor block (e.g. -1, -0, +1).
138    V0317SeedMultiRegions,
139    /// V0316 with larger block sizes.
140    #[serde(alias = "Zeta2.1")]
141    V0318SeedMultiRegions,
142    /// V0318-style markers over the full available current file excerpt with no related files.
143    V0327SingleFile,
144    /// V0318-style prompt with buffer diagnostics
145    V0420Diagnostics,
146    /// V0318-style multi-region format using Qwen FIM tokens and PSM ordering.
147    V0608QwenMultiRegions,
148
149    /// V0318-style marker-span output, but with content-hashed marker tags over rendered
150    /// related-file context so the model can target jump edits. There is no cursor-centered
151    /// editable region for this format.
152    V0615HashRegions,
153}
154
155impl std::fmt::Display for ZetaFormat {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        write!(f, "{}", <&'static str>::from(self))
158    }
159}
160
161impl ZetaFormat {
162    pub fn parse(format_name: &str) -> Result<Self> {
163        let lower = format_name.to_lowercase();
164
165        // Exact case-insensitive match takes priority, bypassing ambiguity checks.
166        for variant in ZetaFormat::iter() {
167            if <&'static str>::from(&variant).to_lowercase() == lower {
168                return Ok(variant);
169            }
170        }
171
172        let mut results = ZetaFormat::iter().filter(|version| {
173            <&'static str>::from(version)
174                .to_lowercase()
175                .contains(&lower)
176        });
177        let Some(result) = results.next() else {
178            anyhow::bail!(
179                "`{format_name}` did not match any of:\n{}",
180                Self::options_as_string()
181            );
182        };
183        if results.next().is_some() {
184            anyhow::bail!(
185                "`{format_name}` matched more than one of:\n{}",
186                Self::options_as_string()
187            );
188        }
189        Ok(result)
190    }
191
192    pub fn options_as_string() -> String {
193        ZetaFormat::iter()
194            .map(|format| format!("- {}\n", <&'static str>::from(format)))
195            .collect::<Vec<_>>()
196            .concat()
197    }
198}
199
200fn empty_range() -> Range<usize> {
201    0..0
202}
203
204#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
205#[serde(tag = "event")]
206pub enum Event {
207    BufferChange {
208        path: Arc<Path>,
209        old_path: Arc<Path>,
210        diff: String,
211        #[serde(default = "empty_range")]
212        old_range: Range<usize>,
213        #[serde(default = "empty_range")]
214        new_range: Range<usize>,
215        predicted: bool,
216        in_open_source_repo: bool,
217    },
218}
219
220impl Event {
221    pub fn in_open_source_repo(&self) -> bool {
222        match self {
223            Event::BufferChange {
224                in_open_source_repo,
225                ..
226            } => *in_open_source_repo,
227        }
228    }
229}
230
231pub fn write_event(prompt: &mut String, event: &Event) {
232    fn write_path_as_unix_str(prompt: &mut String, path: &Path) {
233        for component in path.components() {
234            prompt.push('/');
235            write!(prompt, "{}", component.as_os_str().display()).ok();
236        }
237    }
238    match event {
239        Event::BufferChange {
240            path,
241            old_path,
242            diff,
243            predicted,
244            ..
245        } => {
246            if *predicted {
247                prompt.push_str("// User accepted prediction:\n");
248            }
249            prompt.push_str("--- a");
250            write_path_as_unix_str(prompt, old_path.as_ref());
251            prompt.push_str("\n+++ b");
252            write_path_as_unix_str(prompt, path.as_ref());
253            prompt.push('\n');
254            prompt.push_str(diff);
255        }
256    }
257}
258
259#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
260pub struct ActiveBufferDiagnostic {
261    pub severity: Option<i32>,
262    pub message: String,
263    pub snippet: String,
264    pub snippet_buffer_row_range: Range<u32>,
265    pub diagnostic_range_in_snippet: Range<usize>,
266}
267
268#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
269pub struct RelatedFile {
270    pub path: Arc<Path>,
271    pub max_row: u32,
272    pub excerpts: Vec<RelatedExcerpt>,
273    #[serde(default)]
274    pub in_open_source_repo: bool,
275}
276
277#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
278pub struct RelatedExcerpt {
279    pub row_range: Range<u32>,
280    pub text: Arc<str>,
281    #[serde(default)]
282    pub order: usize,
283    #[serde(default)]
284    pub context_source: ContextSource,
285}
286
287#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
288#[serde(rename_all = "snake_case")]
289pub enum ContextSource {
290    #[default]
291    Lsp,
292    CursorExcerpt,
293    CurrentFile,
294    EditHistory,
295    EditHistoryFile,
296    GitLog,
297    Bm25,
298    OracleFile,
299    OracleSnippet,
300}
301
302pub fn prompt_input_contains_special_tokens(input: &Zeta2PromptInput, format: ZetaFormat) -> bool {
303    special_tokens_for_format(format).iter().any(|token| {
304        if let Some(line_token) = token.strip_suffix('\n') {
305            input.cursor_excerpt.lines().any(|line| line == line_token)
306        } else {
307            input.cursor_excerpt.contains(token)
308        }
309    })
310}
311
312pub fn format_zeta_prompt(input: &Zeta2PromptInput, format: ZetaFormat) -> Option<String> {
313    format_prompt_with_budget_for_format(input, format, max_prompt_tokens_for_format(format))
314}
315
316pub fn format_zeta3_prompt(input: &Zeta3PromptInput, format: ZetaFormat) -> Option<String> {
317    match format {
318        ZetaFormat::V0318SeedMultiRegions => {}
319        _ => return None,
320    }
321
322    let (current_excerpt, cursor_offset_in_excerpt) = zeta3_current_file_excerpt(input)?;
323    let (context, editable_range, context_range, cursor_offset) = resolve_zeta3_cursor_region(
324        current_excerpt.text.as_ref(),
325        cursor_offset_in_excerpt,
326        &input.syntax_ranges,
327        format,
328    );
329    let relative_row_range =
330        offset_range_to_row_range(current_excerpt.text.as_ref(), context_range);
331    let cursor_row_range = current_excerpt.row_range.start + relative_row_range.start
332        ..current_excerpt.row_range.start + relative_row_range.end;
333    let related_files = filter_redundant_excerpts(
334        zeta3_related_files(input, current_excerpt),
335        input.cursor_path.as_ref(),
336        cursor_row_range,
337    );
338
339    format_resolved_prompt_with_budget(
340        format,
341        input.cursor_path.as_ref(),
342        context,
343        &editable_range,
344        cursor_offset,
345        &input.events,
346        &related_files,
347        &input.active_buffer_diagnostics,
348        Some(input.cursor_position.row),
349        max_prompt_tokens_for_format(format),
350    )
351}
352
353fn max_prompt_tokens_for_format(format: ZetaFormat) -> usize {
354    match format {
355        ZetaFormat::V0112MiddleAtEnd
356        | ZetaFormat::V0113Ordered
357        | ZetaFormat::V0114180EditableRegion
358        | ZetaFormat::V0120GitMergeMarkers
359        | ZetaFormat::V0131GitMergeMarkersPrefix
360        | ZetaFormat::V0211Prefill
361        | ZetaFormat::V0211SeedCoder
362        | ZetaFormat::v0226Hashline
363        | ZetaFormat::V0304VariableEdit
364        | ZetaFormat::V0304SeedNoEdits
365        | ZetaFormat::V0306SeedMultiRegions
366        | ZetaFormat::V0316SeedMultiRegions
367        | ZetaFormat::V0317SeedMultiRegions
368        | ZetaFormat::V0331SeedCoderModelPy
369        | ZetaFormat::V0318SeedMultiRegions
370        | ZetaFormat::V0608QwenMultiRegions => 4096,
371        ZetaFormat::V0615HashRegions => 8000,
372        ZetaFormat::V0420Diagnostics => 8192,
373        ZetaFormat::V0327SingleFile => 16384,
374    }
375}
376
377fn zeta3_current_file_excerpt(input: &Zeta3PromptInput) -> Option<(&RelatedExcerpt, usize)> {
378    input
379        .editable_context
380        .iter()
381        .filter(|file| file.path == input.cursor_path)
382        .flat_map(|file| file.excerpts.iter())
383        .find_map(|excerpt| {
384            if excerpt.context_source != ContextSource::CurrentFile {
385                return None;
386            }
387            Some((
388                excerpt,
389                offset_for_position_in_excerpt(excerpt, input.cursor_position)?,
390            ))
391        })
392}
393
394fn offset_for_position_in_excerpt(
395    excerpt: &RelatedExcerpt,
396    position: FilePosition,
397) -> Option<usize> {
398    if position.row < excerpt.row_range.start {
399        return None;
400    }
401
402    let relative_row = (position.row - excerpt.row_range.start) as usize;
403    let text = excerpt.text.as_ref();
404    let mut row_start = 0;
405
406    for row in 0..=relative_row {
407        if row == relative_row {
408            let row_end = text[row_start..]
409                .find('\n')
410                .map_or(text.len(), |offset| row_start + offset);
411            let row_text = &text[row_start..row_end];
412            let column =
413                row_text.floor_char_boundary((position.column as usize).min(row_text.len()));
414            return Some(row_start + column);
415        }
416
417        row_start += text[row_start..].find('\n')? + 1;
418    }
419
420    None
421}
422
423fn zeta3_related_files(
424    input: &Zeta3PromptInput,
425    current_excerpt: &RelatedExcerpt,
426) -> Vec<RelatedFile> {
427    input
428        .editable_context
429        .iter()
430        .filter_map(|file| {
431            let mut file = file.clone();
432            if file.path == input.cursor_path {
433                file.excerpts.retain(|excerpt| excerpt != current_excerpt);
434            }
435            (!file.excerpts.is_empty()).then_some(file)
436        })
437        .collect()
438}
439
440fn resolve_zeta3_cursor_region<'a>(
441    cursor_excerpt: &'a str,
442    cursor_offset: usize,
443    syntax_ranges: &[Range<usize>],
444    format: ZetaFormat,
445) -> (&'a str, Range<usize>, Range<usize>, usize) {
446    let (editable_tokens, context_tokens) = token_limits_for_format(format);
447    let (editable_range, context_range) = compute_editable_and_context_ranges(
448        cursor_excerpt,
449        cursor_offset,
450        syntax_ranges,
451        editable_tokens,
452        context_tokens,
453    );
454
455    adjust_cursor_region(cursor_excerpt, cursor_offset, editable_range, context_range)
456}
457
458pub fn special_tokens_for_format(format: ZetaFormat) -> &'static [&'static str] {
459    match format {
460        ZetaFormat::V0112MiddleAtEnd => v0112_middle_at_end::special_tokens(),
461        ZetaFormat::V0113Ordered => v0113_ordered::special_tokens(),
462        ZetaFormat::V0114180EditableRegion => v0114180_editable_region::special_tokens(),
463        ZetaFormat::V0120GitMergeMarkers => v0120_git_merge_markers::special_tokens(),
464        ZetaFormat::V0131GitMergeMarkersPrefix => v0131_git_merge_markers_prefix::special_tokens(),
465        ZetaFormat::V0211Prefill => v0211_prefill::special_tokens(),
466        ZetaFormat::V0211SeedCoder | ZetaFormat::V0331SeedCoderModelPy => {
467            seed_coder::special_tokens()
468        }
469        ZetaFormat::v0226Hashline => hashline::special_tokens(),
470        ZetaFormat::V0304VariableEdit => v0304_variable_edit::special_tokens(),
471        ZetaFormat::V0304SeedNoEdits => seed_coder::special_tokens(),
472        ZetaFormat::V0316SeedMultiRegions => {
473            static TOKENS: &[&str] = &[
474                seed_coder::FIM_SUFFIX,
475                seed_coder::FIM_PREFIX,
476                seed_coder::FIM_MIDDLE,
477                seed_coder::FILE_MARKER,
478                multi_region::V0316_END_MARKER,
479                CURSOR_MARKER,
480                multi_region::MARKER_TAG_PREFIX,
481            ];
482            TOKENS
483        }
484        ZetaFormat::V0318SeedMultiRegions | ZetaFormat::V0420Diagnostics => {
485            static TOKENS: &[&str] = &[
486                seed_coder::FIM_SUFFIX,
487                seed_coder::FIM_PREFIX,
488                seed_coder::FIM_MIDDLE,
489                seed_coder::FILE_MARKER,
490                multi_region::V0318_END_MARKER,
491                CURSOR_MARKER,
492                multi_region::MARKER_TAG_PREFIX,
493            ];
494            TOKENS
495        }
496        ZetaFormat::V0608QwenMultiRegions => {
497            static TOKENS: &[&str] = &[
498                qwen::FIM_PREFIX,
499                qwen::FIM_SUFFIX,
500                qwen::FIM_MIDDLE,
501                qwen::FILE_MARKER,
502                qwen::END_MARKER,
503                CURSOR_MARKER,
504                multi_region::MARKER_TAG_PREFIX,
505            ];
506            TOKENS
507        }
508        ZetaFormat::V0317SeedMultiRegions => {
509            static TOKENS: &[&str] = &[
510                seed_coder::FIM_SUFFIX,
511                seed_coder::FIM_PREFIX,
512                seed_coder::FIM_MIDDLE,
513                seed_coder::FILE_MARKER,
514                multi_region::V0317_END_MARKER,
515                CURSOR_MARKER,
516                multi_region::RELATIVE_MARKER_TAG_PREFIX,
517            ];
518            TOKENS
519        }
520        ZetaFormat::V0615HashRegions => {
521            static TOKENS: &[&str] = &[
522                seed_coder::FIM_SUFFIX,
523                seed_coder::FIM_PREFIX,
524                seed_coder::FIM_MIDDLE,
525                seed_coder::FILE_MARKER,
526                hashed_regions::V0615_END_MARKER,
527                CURSOR_MARKER,
528                hashed_regions::MARKER_TAG_PREFIX,
529            ];
530            TOKENS
531        }
532        ZetaFormat::V0327SingleFile => {
533            static TOKENS: &[&str] = &[
534                seed_coder::FIM_SUFFIX,
535                seed_coder::FIM_PREFIX,
536                seed_coder::FIM_MIDDLE,
537                seed_coder::FILE_MARKER,
538                multi_region::V0327_END_MARKER,
539                CURSOR_MARKER,
540                multi_region::MARKER_TAG_PREFIX,
541            ];
542            TOKENS
543        }
544        ZetaFormat::V0306SeedMultiRegions => {
545            static TOKENS: &[&str] = &[
546                seed_coder::FIM_SUFFIX,
547                seed_coder::FIM_PREFIX,
548                seed_coder::FIM_MIDDLE,
549                seed_coder::FILE_MARKER,
550                seed_coder::START_MARKER,
551                seed_coder::SEPARATOR,
552                seed_coder::END_MARKER,
553                CURSOR_MARKER,
554                multi_region::MARKER_TAG_PREFIX,
555            ];
556            TOKENS
557        }
558    }
559}
560
561/// Returns the (editable_token_limit, context_token_limit) for a given format.
562pub fn token_limits_for_format(format: ZetaFormat) -> (usize, usize) {
563    match format {
564        ZetaFormat::V0112MiddleAtEnd | ZetaFormat::V0113Ordered => (150, 350),
565        ZetaFormat::V0114180EditableRegion => (180, 350),
566        ZetaFormat::V0120GitMergeMarkers
567        | ZetaFormat::V0131GitMergeMarkersPrefix
568        | ZetaFormat::V0211Prefill
569        | ZetaFormat::V0211SeedCoder
570        | ZetaFormat::V0331SeedCoderModelPy
571        | ZetaFormat::v0226Hashline
572        | ZetaFormat::V0306SeedMultiRegions
573        | ZetaFormat::V0316SeedMultiRegions
574        | ZetaFormat::V0318SeedMultiRegions
575        | ZetaFormat::V0420Diagnostics
576        | ZetaFormat::V0608QwenMultiRegions
577        | ZetaFormat::V0317SeedMultiRegions
578        | ZetaFormat::V0327SingleFile
579        | ZetaFormat::V0304SeedNoEdits => (350, 150),
580        ZetaFormat::V0615HashRegions => (8000, 0),
581
582        ZetaFormat::V0304VariableEdit => (1024, 0),
583    }
584}
585
586pub fn stop_tokens_for_format(format: ZetaFormat) -> &'static [&'static str] {
587    match format {
588        ZetaFormat::v0226Hashline => &[hashline::NO_EDITS_COMMAND_MARKER],
589        ZetaFormat::V0112MiddleAtEnd
590        | ZetaFormat::V0113Ordered
591        | ZetaFormat::V0114180EditableRegion
592        | ZetaFormat::V0120GitMergeMarkers
593        | ZetaFormat::V0131GitMergeMarkersPrefix
594        | ZetaFormat::V0211Prefill
595        | ZetaFormat::V0211SeedCoder
596        | ZetaFormat::V0331SeedCoderModelPy
597        | ZetaFormat::V0304VariableEdit
598        | ZetaFormat::V0306SeedMultiRegions
599        | ZetaFormat::V0304SeedNoEdits => &[],
600        ZetaFormat::V0316SeedMultiRegions => &[multi_region::V0316_END_MARKER],
601        ZetaFormat::V0318SeedMultiRegions | ZetaFormat::V0420Diagnostics => {
602            &[multi_region::V0318_END_MARKER]
603        }
604        ZetaFormat::V0608QwenMultiRegions => &[qwen::END_MARKER],
605        ZetaFormat::V0317SeedMultiRegions => &[multi_region::V0317_END_MARKER],
606        ZetaFormat::V0327SingleFile => &[multi_region::V0327_END_MARKER],
607        ZetaFormat::V0615HashRegions => &[hashed_regions::V0615_END_MARKER],
608    }
609}
610
611/// Delimiters used by response-only SFT (e.g. Unsloth `train_on_responses_only`)
612/// to mask the prompt and train only on the model's completion.
613///
614/// Both strings must appear verbatim in the prompt produced by
615/// [`format_zeta_prompt`] for the same format: `instruction_part` marks the
616/// start of an example, and `response_part` is the final marker before the
617/// completion begins.
618pub struct TrainingDelimiters {
619    pub instruction_part: &'static str,
620    pub response_part: &'static str,
621}
622
623/// Return the response-only training delimiters for a format.
624///
625/// This match is intentionally exhaustive with no wildcard arm so that adding a
626/// new [`ZetaFormat`] fails to compile until its delimiters are specified.
627pub fn training_delimiters_for_format(format: ZetaFormat) -> TrainingDelimiters {
628    match format {
629        ZetaFormat::V0211SeedCoder
630        | ZetaFormat::V0331SeedCoderModelPy
631        | ZetaFormat::V0304SeedNoEdits
632        | ZetaFormat::V0306SeedMultiRegions
633        | ZetaFormat::V0316SeedMultiRegions
634        | ZetaFormat::V0317SeedMultiRegions
635        | ZetaFormat::V0318SeedMultiRegions
636        | ZetaFormat::V0327SingleFile
637        | ZetaFormat::V0420Diagnostics
638        | ZetaFormat::V0615HashRegions => TrainingDelimiters {
639            instruction_part: seed_coder::FIM_SUFFIX,
640            response_part: seed_coder::FIM_MIDDLE,
641        },
642        ZetaFormat::V0608QwenMultiRegions => TrainingDelimiters {
643            instruction_part: qwen::FIM_PREFIX,
644            response_part: qwen::FIM_MIDDLE,
645        },
646        ZetaFormat::V0112MiddleAtEnd
647        | ZetaFormat::V0113Ordered
648        | ZetaFormat::V0114180EditableRegion => TrainingDelimiters {
649            instruction_part: "<|file_sep|>",
650            response_part: "<|fim_middle|>updated\n",
651        },
652        ZetaFormat::V0120GitMergeMarkers => TrainingDelimiters {
653            instruction_part: "<|file_sep|>",
654            response_part: v0120_git_merge_markers::SEPARATOR,
655        },
656        ZetaFormat::V0131GitMergeMarkersPrefix | ZetaFormat::V0211Prefill => TrainingDelimiters {
657            instruction_part: "<|file_sep|>",
658            response_part: "<|fim_middle|>",
659        },
660        ZetaFormat::v0226Hashline => TrainingDelimiters {
661            instruction_part: "<|file_sep|>",
662            response_part: hashline::END_MARKER,
663        },
664        ZetaFormat::V0304VariableEdit => TrainingDelimiters {
665            instruction_part: "<|file_sep|>",
666            response_part: "<|fim_prefix|>",
667        },
668    }
669}
670
671/// Return (editable_range, context_range) for the prompt format
672pub fn excerpt_ranges_for_format(
673    format: ZetaFormat,
674    ranges: &ExcerptRanges,
675) -> (Range<usize>, Range<usize>) {
676    match format {
677        ZetaFormat::V0112MiddleAtEnd | ZetaFormat::V0113Ordered => (
678            ranges.editable_150.clone(),
679            ranges.editable_150_context_350.clone(),
680        ),
681        ZetaFormat::V0114180EditableRegion => (
682            ranges.editable_180.clone(),
683            ranges.editable_180_context_350.clone(),
684        ),
685        ZetaFormat::V0120GitMergeMarkers
686        | ZetaFormat::V0131GitMergeMarkersPrefix
687        | ZetaFormat::V0211Prefill
688        | ZetaFormat::V0211SeedCoder
689        | ZetaFormat::V0331SeedCoderModelPy
690        | ZetaFormat::v0226Hashline
691        | ZetaFormat::V0304SeedNoEdits
692        | ZetaFormat::V0306SeedMultiRegions
693        | ZetaFormat::V0316SeedMultiRegions
694        | ZetaFormat::V0318SeedMultiRegions
695        | ZetaFormat::V0317SeedMultiRegions
696        | ZetaFormat::V0420Diagnostics
697        | ZetaFormat::V0608QwenMultiRegions => (
698            ranges.editable_350.clone(),
699            ranges.editable_350_context_150.clone(),
700        ),
701        ZetaFormat::V0327SingleFile => (
702            ranges.editable_350_context_150.clone(),
703            ranges.context_8192.clone().unwrap_or(
704                // shouldn't be used, only for compat with old data/clients
705                ranges.editable_350_context_150.clone(),
706            ),
707        ),
708        ZetaFormat::V0615HashRegions => (
709            ranges
710                .context_8192
711                .clone()
712                .unwrap_or_else(|| ranges.editable_350_context_150.clone()),
713            ranges
714                .context_8192
715                .clone()
716                .unwrap_or_else(|| ranges.editable_350_context_150.clone()),
717        ),
718
719        ZetaFormat::V0304VariableEdit => {
720            let context = ranges
721                .editable_350_context_1024
722                .clone()
723                .or(ranges.editable_350_context_512.clone())
724                .unwrap_or_else(|| ranges.editable_350_context_150.clone());
725            (context.clone(), context)
726        }
727    }
728}
729
730pub fn write_cursor_excerpt_section_for_format(
731    format: ZetaFormat,
732    prompt: &mut String,
733    path: &Path,
734    context: &str,
735    editable_range: &Range<usize>,
736    cursor_offset: usize,
737) {
738    match format {
739        ZetaFormat::V0112MiddleAtEnd => v0112_middle_at_end::write_cursor_excerpt_section(
740            prompt,
741            path,
742            context,
743            editable_range,
744            cursor_offset,
745        ),
746        ZetaFormat::V0113Ordered | ZetaFormat::V0114180EditableRegion => {
747            v0113_ordered::write_cursor_excerpt_section(
748                prompt,
749                path,
750                context,
751                editable_range,
752                cursor_offset,
753            )
754        }
755        ZetaFormat::V0120GitMergeMarkers => v0120_git_merge_markers::write_cursor_excerpt_section(
756            prompt,
757            path,
758            context,
759            editable_range,
760            cursor_offset,
761        ),
762        ZetaFormat::V0131GitMergeMarkersPrefix | ZetaFormat::V0211Prefill => {
763            v0131_git_merge_markers_prefix::write_cursor_excerpt_section(
764                prompt,
765                path,
766                context,
767                editable_range,
768                cursor_offset,
769            )
770        }
771        ZetaFormat::V0211SeedCoder
772        | ZetaFormat::V0331SeedCoderModelPy
773        | ZetaFormat::V0304SeedNoEdits => seed_coder::write_cursor_excerpt_section(
774            prompt,
775            path,
776            context,
777            editable_range,
778            cursor_offset,
779        ),
780        ZetaFormat::v0226Hashline => hashline::write_cursor_excerpt_section(
781            prompt,
782            path,
783            context,
784            editable_range,
785            cursor_offset,
786        ),
787        ZetaFormat::V0304VariableEdit => {
788            v0304_variable_edit::write_cursor_excerpt_section(prompt, path, context, cursor_offset)
789        }
790        ZetaFormat::V0306SeedMultiRegions => {
791            prompt.push_str(&build_v0306_cursor_prefix(
792                path,
793                context,
794                editable_range,
795                cursor_offset,
796            ));
797        }
798        ZetaFormat::V0316SeedMultiRegions => {
799            prompt.push_str(&build_v0316_cursor_prefix(
800                path,
801                context,
802                editable_range,
803                cursor_offset,
804            ));
805        }
806        ZetaFormat::V0318SeedMultiRegions | ZetaFormat::V0420Diagnostics => {
807            prompt.push_str(&build_v0318_cursor_prefix(
808                path,
809                context,
810                editable_range,
811                cursor_offset,
812                seed_coder::FILE_MARKER,
813            ));
814        }
815        ZetaFormat::V0608QwenMultiRegions => {
816            prompt.push_str(&build_v0318_cursor_prefix(
817                path,
818                context,
819                editable_range,
820                cursor_offset,
821                qwen::FILE_MARKER,
822            ));
823        }
824        ZetaFormat::V0317SeedMultiRegions => {
825            prompt.push_str(&build_v0317_cursor_prefix(
826                path,
827                context,
828                editable_range,
829                cursor_offset,
830            ));
831        }
832        ZetaFormat::V0327SingleFile => {
833            prompt.push_str(&build_v0318_cursor_prefix(
834                path,
835                context,
836                editable_range,
837                cursor_offset,
838                seed_coder::FILE_MARKER,
839            ));
840        }
841        ZetaFormat::V0615HashRegions => {
842            prompt.push_str(&build_v0615_cursor_prefix(
843                path,
844                context,
845                editable_range,
846                cursor_offset,
847            ));
848        }
849    }
850}
851
852fn build_v0306_cursor_prefix(
853    path: &Path,
854    context: &str,
855    editable_range: &Range<usize>,
856    cursor_offset: usize,
857) -> String {
858    let mut section = String::new();
859    let path_str = path.to_string_lossy();
860    write!(section, "{}{}\n", seed_coder::FILE_MARKER, path_str).ok();
861
862    section.push_str(&context[..editable_range.start]);
863    section.push_str(seed_coder::START_MARKER);
864
865    let editable_text = &context[editable_range.clone()];
866    let cursor_in_editable = cursor_offset - editable_range.start;
867    multi_region::write_editable_with_markers(
868        &mut section,
869        editable_text,
870        cursor_in_editable,
871        CURSOR_MARKER,
872    );
873
874    if !section.ends_with('\n') {
875        section.push('\n');
876    }
877    section.push_str(seed_coder::SEPARATOR);
878    section
879}
880
881fn build_v0316_cursor_prefix(
882    path: &Path,
883    context: &str,
884    editable_range: &Range<usize>,
885    cursor_offset: usize,
886) -> String {
887    let mut section = String::new();
888    let path_str = path.to_string_lossy();
889    write!(section, "{}{}\n", seed_coder::FILE_MARKER, path_str).ok();
890
891    section.push_str(&context[..editable_range.start]);
892
893    let editable_text = &context[editable_range.clone()];
894    let cursor_in_editable = cursor_offset - editable_range.start;
895    multi_region::write_editable_with_markers_v0316(
896        &mut section,
897        editable_text,
898        cursor_in_editable,
899        CURSOR_MARKER,
900    );
901
902    if !section.ends_with('\n') {
903        section.push('\n');
904    }
905    section
906}
907
908fn build_v0318_cursor_prefix(
909    path: &Path,
910    context: &str,
911    editable_range: &Range<usize>,
912    cursor_offset: usize,
913    file_marker: &str,
914) -> String {
915    let mut section = String::new();
916    let path_str = path.to_string_lossy();
917    write!(section, "{}{}\n", file_marker, path_str).ok();
918
919    section.push_str(&context[..editable_range.start]);
920
921    let editable_text = &context[editable_range.clone()];
922    let cursor_in_editable = cursor_offset - editable_range.start;
923    multi_region::write_editable_with_markers_v0318(
924        &mut section,
925        editable_text,
926        cursor_in_editable,
927        CURSOR_MARKER,
928    );
929
930    if !section.ends_with('\n') {
931        section.push('\n');
932    }
933    section
934}
935
936fn build_v0615_cursor_prefix(
937    path: &Path,
938    context: &str,
939    editable_range: &Range<usize>,
940    cursor_offset: usize,
941) -> String {
942    let mut section = String::new();
943    let path_str = path.to_string_lossy();
944    write!(section, "{}{}\n", seed_coder::FILE_MARKER, path_str).ok();
945
946    section.push_str(&context[..editable_range.start]);
947
948    let editable_text = &context[editable_range.clone()];
949    let cursor_in_editable = cursor_offset - editable_range.start;
950    let markers = hashed_regions::markers_for_text(editable_text);
951    hashed_regions::write_snippet_with_markers(
952        &mut section,
953        editable_text,
954        &markers,
955        Some((cursor_in_editable, CURSOR_MARKER)),
956    );
957
958    if !section.ends_with('\n') {
959        section.push('\n');
960    }
961    section
962}
963
964fn build_v0317_cursor_prefix(
965    path: &Path,
966    context: &str,
967    editable_range: &Range<usize>,
968    cursor_offset: usize,
969) -> String {
970    let mut section = String::new();
971    let path_str = path.to_string_lossy();
972    write!(section, "{}{}\n", seed_coder::FILE_MARKER, path_str).ok();
973
974    section.push_str(&context[..editable_range.start]);
975
976    let editable_text = &context[editable_range.clone()];
977    let cursor_in_editable = cursor_offset - editable_range.start;
978    multi_region::write_editable_with_markers_v0317(
979        &mut section,
980        editable_text,
981        cursor_in_editable,
982        CURSOR_MARKER,
983    );
984
985    if !section.ends_with('\n') {
986        section.push('\n');
987    }
988    section
989}
990
991fn offset_range_to_row_range(text: &str, range: Range<usize>) -> Range<u32> {
992    let start_row = text[0..range.start].matches('\n').count() as u32;
993    let mut end_row = start_row + text[range.clone()].matches('\n').count() as u32;
994    if !text[..range.end].ends_with('\n') {
995        end_row += 1;
996    }
997    return start_row..end_row;
998}
999
1000fn assemble_single_file_fim_prompt(
1001    context: &str,
1002    editable_range: &Range<usize>,
1003    cursor_prefix_section: &str,
1004    events: &[Arc<Event>],
1005    max_tokens: usize,
1006) -> String {
1007    let suffix_section = seed_coder::build_suffix_section(context, editable_range);
1008
1009    let suffix_tokens = estimate_tokens(suffix_section.len() + seed_coder::FIM_PREFIX.len());
1010    let cursor_prefix_tokens =
1011        estimate_tokens(cursor_prefix_section.len() + seed_coder::FIM_MIDDLE.len());
1012    let budget_after_cursor = max_tokens.saturating_sub(suffix_tokens + cursor_prefix_tokens);
1013
1014    let edit_history_section = format_edit_history_within_budget(
1015        events,
1016        seed_coder::FILE_MARKER,
1017        "edit_history",
1018        budget_after_cursor,
1019        max_edit_event_count_for_format(&ZetaFormat::V0327SingleFile),
1020    );
1021
1022    let mut prompt = String::new();
1023    prompt.push_str(&suffix_section);
1024    prompt.push_str(seed_coder::FIM_PREFIX);
1025    prompt.push_str(&edit_history_section);
1026    if !edit_history_section.is_empty() {
1027        prompt.push('\n');
1028    }
1029    prompt.push_str(cursor_prefix_section);
1030    prompt.push_str(seed_coder::FIM_MIDDLE);
1031    prompt
1032}
1033
1034fn format_hash_region_related_files_within_budget(
1035    input: &Zeta2PromptInput,
1036    marker_table: &[hashed_regions::SnippetMarkers],
1037    cursor: &hashed_regions::RelatedFileCursor,
1038    max_tokens: usize,
1039) -> Option<String> {
1040    let related_files = input.related_files.as_deref()?;
1041
1042    struct RenderedExcerpt {
1043        file_ix: usize,
1044        excerpt_ix: usize,
1045        order: usize,
1046        rendered: String,
1047    }
1048
1049    let mut candidates = Vec::new();
1050    let mut required_candidate_ix = None;
1051    for (file_ix, file) in related_files.iter().enumerate() {
1052        for (excerpt_ix, excerpt) in file.excerpts.iter().enumerate() {
1053            let markers =
1054                hashed_regions::marker_table_for_excerpt(marker_table, file_ix, excerpt_ix);
1055            let mut rendered = String::new();
1056            if let Some(markers) = markers {
1057                let cursor_in_excerpt = (file_ix == cursor.file_ix
1058                    && excerpt_ix == cursor.excerpt_ix)
1059                    .then_some((cursor.offset_in_excerpt, CURSOR_MARKER));
1060                hashed_regions::write_snippet_with_markers(
1061                    &mut rendered,
1062                    &excerpt.text,
1063                    markers,
1064                    cursor_in_excerpt,
1065                );
1066            } else {
1067                rendered.push_str(&excerpt.text);
1068            }
1069            if !rendered.ends_with('\n') {
1070                rendered.push('\n');
1071            }
1072
1073            if file_ix == cursor.file_ix && excerpt_ix == cursor.excerpt_ix {
1074                required_candidate_ix = Some(candidates.len());
1075            }
1076
1077            candidates.push(RenderedExcerpt {
1078                file_ix,
1079                excerpt_ix,
1080                order: excerpt.order,
1081                rendered,
1082            });
1083        }
1084    }
1085
1086    let required_candidate_ix = required_candidate_ix?;
1087    let file_headers: Vec<String> = related_files
1088        .iter()
1089        .map(|file| {
1090            let path = hashed_regions::related_file_patch_path(&input.cursor_path, &file.path)
1091                .iter()
1092                .map(|component| component.to_string_lossy())
1093                .collect::<Vec<_>>()
1094                .join("/");
1095            format!("{}{path}\n", seed_coder::FILE_MARKER)
1096        })
1097        .collect();
1098
1099    let mut total_tokens = 0;
1100    let mut included = vec![false; candidates.len()];
1101    let mut file_included = vec![false; related_files.len()];
1102
1103    let required = &candidates[required_candidate_ix];
1104    let required_cost =
1105        estimate_tokens(file_headers[required.file_ix].len() + required.rendered.len());
1106    if required_cost > max_tokens {
1107        return None;
1108    }
1109    total_tokens += required_cost;
1110    included[required_candidate_ix] = true;
1111    file_included[required.file_ix] = true;
1112
1113    let mut selection_order: Vec<usize> = (0..candidates.len()).collect();
1114    selection_order.sort_by_key(|&candidate_ix| {
1115        let candidate = &candidates[candidate_ix];
1116        (candidate.order, candidate.file_ix, candidate.excerpt_ix)
1117    });
1118
1119    for candidate_ix in selection_order {
1120        if included[candidate_ix] {
1121            continue;
1122        }
1123        let candidate = &candidates[candidate_ix];
1124        let header_cost = if file_included[candidate.file_ix] {
1125            0
1126        } else {
1127            estimate_tokens(file_headers[candidate.file_ix].len())
1128        };
1129        let excerpt_cost = estimate_tokens(candidate.rendered.len());
1130        if total_tokens + header_cost + excerpt_cost > max_tokens {
1131            continue;
1132        }
1133        total_tokens += header_cost + excerpt_cost;
1134        included[candidate_ix] = true;
1135        file_included[candidate.file_ix] = true;
1136    }
1137
1138    let mut result = String::new();
1139    let mut last_file_ix = None;
1140    for (candidate_ix, candidate) in candidates.iter().enumerate() {
1141        if !included[candidate_ix] {
1142            continue;
1143        }
1144        if last_file_ix != Some(candidate.file_ix) {
1145            result.push_str(&file_headers[candidate.file_ix]);
1146            last_file_ix = Some(candidate.file_ix);
1147        }
1148        result.push_str(&candidate.rendered);
1149
1150        let file = &related_files[candidate.file_ix];
1151        let excerpt = &file.excerpts[candidate.excerpt_ix];
1152        let next_excerpt_start = candidates
1153            .iter()
1154            .enumerate()
1155            .skip(candidate_ix + 1)
1156            .find(|(next_ix, next)| included[*next_ix] && next.file_ix == candidate.file_ix)
1157            .map(|(_, next)| file.excerpts[next.excerpt_ix].row_range.start);
1158        if rows_omitted_after_excerpt(excerpt, next_excerpt_start, file.max_row) {
1159            result.push_str("...\n");
1160        }
1161    }
1162
1163    Some(result)
1164}
1165
1166fn format_hash_regions_prompt_with_budget(
1167    input: &Zeta2PromptInput,
1168    max_tokens: usize,
1169) -> Option<String> {
1170    let marker_table = hashed_regions::build_marker_table(input);
1171    let cursor = hashed_regions::locate_cursor_in_related_files(input)?;
1172    hashed_regions::marker_table_for_excerpt(&marker_table, cursor.file_ix, cursor.excerpt_ix)?;
1173
1174    let fixed_tokens = estimate_tokens(
1175        seed_coder::FIM_SUFFIX.len()
1176            + "\n".len()
1177            + seed_coder::FIM_PREFIX.len()
1178            + seed_coder::FIM_MIDDLE.len(),
1179    );
1180    let related_files_budget = max_tokens.saturating_sub(fixed_tokens);
1181    let related_files_section = format_hash_region_related_files_within_budget(
1182        input,
1183        &marker_table,
1184        &cursor,
1185        related_files_budget,
1186    )?;
1187
1188    let mut prompt = String::new();
1189    prompt.push_str(seed_coder::FIM_SUFFIX);
1190    prompt.push('\n');
1191    prompt.push_str(seed_coder::FIM_PREFIX);
1192    prompt.push_str(&related_files_section);
1193    if !prompt.ends_with('\n') {
1194        prompt.push('\n');
1195    }
1196    prompt.push_str(seed_coder::FIM_MIDDLE);
1197    Some(prompt)
1198}
1199
1200pub fn format_prompt_with_budget_for_format(
1201    input: &Zeta2PromptInput,
1202    format: ZetaFormat,
1203    max_tokens: usize,
1204) -> Option<String> {
1205    if format == ZetaFormat::V0615HashRegions {
1206        return format_hash_regions_prompt_with_budget(
1207            input,
1208            apply_prompt_budget_margin(max_tokens),
1209        );
1210    }
1211
1212    let (context, editable_range, context_range, cursor_offset) =
1213        resolve_cursor_region(input, format);
1214    let empty_files = Vec::new();
1215    let input_related_files = input.related_files.as_deref().unwrap_or(&empty_files);
1216    let filtered_related_files = if format == ZetaFormat::V0615HashRegions {
1217        input_related_files.to_vec()
1218    } else if let Some(cursor_excerpt_start_row) = input.excerpt_start_row {
1219        let relative_row_range =
1220            offset_range_to_row_range(&input.cursor_excerpt, context_range.clone());
1221        let row_range = relative_row_range.start + cursor_excerpt_start_row
1222            ..relative_row_range.end + cursor_excerpt_start_row;
1223        filter_redundant_excerpts(
1224            input_related_files.to_vec(),
1225            input.cursor_path.as_ref(),
1226            row_range,
1227        )
1228    } else {
1229        input_related_files.to_vec()
1230    };
1231    let cursor_buffer_row = input.excerpt_start_row.map(|excerpt_start_row| {
1232        excerpt_start_row
1233            + input.cursor_excerpt[..context_range.start + cursor_offset]
1234                .bytes()
1235                .filter(|byte| *byte == b'\n')
1236                .count() as u32
1237    });
1238
1239    format_resolved_prompt_with_budget(
1240        format,
1241        input.cursor_path.as_ref(),
1242        context,
1243        &editable_range,
1244        cursor_offset,
1245        &input.events,
1246        &filtered_related_files,
1247        &input.active_buffer_diagnostics,
1248        cursor_buffer_row,
1249        max_tokens,
1250    )
1251}
1252
1253fn format_resolved_prompt_with_budget(
1254    format: ZetaFormat,
1255    path: &Path,
1256    context: &str,
1257    editable_range: &Range<usize>,
1258    cursor_offset: usize,
1259    events: &[Arc<Event>],
1260    related_files: &[RelatedFile],
1261    active_buffer_diagnostics: &[ActiveBufferDiagnostic],
1262    cursor_buffer_row: Option<u32>,
1263    max_tokens: usize,
1264) -> Option<String> {
1265    let prompt = match format {
1266        ZetaFormat::V0211SeedCoder
1267        | ZetaFormat::V0331SeedCoderModelPy
1268        | ZetaFormat::V0304SeedNoEdits
1269        | ZetaFormat::V0306SeedMultiRegions
1270        | ZetaFormat::V0316SeedMultiRegions
1271        | ZetaFormat::V0318SeedMultiRegions
1272        | ZetaFormat::V0317SeedMultiRegions
1273        | ZetaFormat::V0420Diagnostics => {
1274            let mut cursor_section = String::new();
1275
1276            write_cursor_excerpt_section_for_format(
1277                format,
1278                &mut cursor_section,
1279                path,
1280                context,
1281                editable_range,
1282                cursor_offset,
1283            );
1284
1285            let budget_with_margin = apply_prompt_budget_margin(max_tokens);
1286            seed_coder::assemble_fim_prompt(
1287                context,
1288                editable_range,
1289                &cursor_section,
1290                events,
1291                related_files,
1292                if format == ZetaFormat::V0420Diagnostics {
1293                    active_buffer_diagnostics
1294                } else {
1295                    &[]
1296                },
1297                cursor_buffer_row,
1298                budget_with_margin,
1299            )
1300        }
1301        ZetaFormat::V0608QwenMultiRegions => {
1302            let mut cursor_section = String::new();
1303
1304            write_cursor_excerpt_section_for_format(
1305                format,
1306                &mut cursor_section,
1307                path,
1308                context,
1309                editable_range,
1310                cursor_offset,
1311            );
1312
1313            qwen::assemble_fim_prompt(
1314                context,
1315                editable_range,
1316                &cursor_section,
1317                events,
1318                related_files,
1319                apply_prompt_budget_margin(max_tokens),
1320            )
1321        }
1322        ZetaFormat::V0327SingleFile => {
1323            let mut cursor_section = String::new();
1324            write_cursor_excerpt_section_for_format(
1325                format,
1326                &mut cursor_section,
1327                path,
1328                context,
1329                editable_range,
1330                cursor_offset,
1331            );
1332
1333            assemble_single_file_fim_prompt(
1334                context,
1335                editable_range,
1336                &cursor_section,
1337                events,
1338                apply_prompt_budget_margin(max_tokens),
1339            )
1340        }
1341        _ => {
1342            let mut cursor_section = String::new();
1343            write_cursor_excerpt_section_for_format(
1344                format,
1345                &mut cursor_section,
1346                path,
1347                context,
1348                editable_range,
1349                cursor_offset,
1350            );
1351
1352            let mut remaining_budget = apply_prompt_budget_margin(max_tokens);
1353            let cursor_tokens = estimate_tokens(cursor_section.len());
1354            remaining_budget = remaining_budget.saturating_sub(cursor_tokens);
1355
1356            let edit_history_section = format_edit_history_within_budget(
1357                events,
1358                "<|file_sep|>",
1359                "edit history",
1360                remaining_budget,
1361                max_edit_event_count_for_format(&format),
1362            );
1363            let edit_history_tokens = estimate_tokens(edit_history_section.len());
1364            remaining_budget = remaining_budget.saturating_sub(edit_history_tokens);
1365
1366            let related_files_section = format_related_files_within_budget(
1367                related_files,
1368                "<|file_sep|>",
1369                "",
1370                remaining_budget,
1371            );
1372
1373            let mut prompt = String::new();
1374            prompt.push_str(&related_files_section);
1375            prompt.push_str(&edit_history_section);
1376            prompt.push_str(&cursor_section);
1377            prompt
1378        }
1379    };
1380    let prompt_tokens = estimate_tokens(prompt.len());
1381    if prompt_tokens > max_tokens {
1382        return None;
1383    }
1384    return Some(prompt);
1385}
1386
1387pub fn format_active_buffer_diagnostics_with_budget(
1388    diagnostics: &[ActiveBufferDiagnostic],
1389    cursor_buffer_row: Option<u32>,
1390    budget: usize,
1391) -> String {
1392    if diagnostics.is_empty() || budget == 0 {
1393        return String::new();
1394    }
1395
1396    const MAX_DIAGNOSTICS: usize = 10;
1397
1398    let mut diagnostic_indices = (0..diagnostics.len()).collect::<Vec<_>>();
1399    if let Some(cursor_buffer_row) = cursor_buffer_row {
1400        let distance = |index: &usize| {
1401            let range = &diagnostics[*index].snippet_buffer_row_range;
1402            u32::abs_diff(cursor_buffer_row, range.start)
1403                + u32::abs_diff(cursor_buffer_row, range.end)
1404        };
1405        // Only the closest `MAX_DIAGNOSTICS` are rendered below, so select that
1406        // prefix instead of fully sorting every diagnostic.
1407        if diagnostic_indices.len() > MAX_DIAGNOSTICS {
1408            diagnostic_indices.select_nth_unstable_by_key(MAX_DIAGNOSTICS, &distance);
1409            diagnostic_indices.truncate(MAX_DIAGNOSTICS);
1410        }
1411        diagnostic_indices.sort_unstable_by_key(&distance);
1412    }
1413
1414    let mut output = format!("{}diagnostics\n", seed_coder::FILE_MARKER);
1415    let header_tokens = estimate_tokens(output.len());
1416    if header_tokens > budget {
1417        return String::new();
1418    }
1419
1420    let mut used_tokens = header_tokens;
1421    let mut included_diagnostics = 0;
1422    for diagnostic_index in diagnostic_indices.into_iter().take(MAX_DIAGNOSTICS) {
1423        let diagnostic = &diagnostics[diagnostic_index];
1424        let snippet = clamp_text_to_token_count(&diagnostic.snippet, 256);
1425
1426        let diagnostic_section = if snippet.is_empty() {
1427            format!("*{}*\n", diagnostic.message)
1428        } else {
1429            format!(
1430                "*{}*:\n```\n{}{}\n```\n",
1431                diagnostic.message,
1432                snippet,
1433                if snippet.len() < diagnostic.snippet.len() {
1434                    "..."
1435                } else {
1436                    ""
1437                }
1438            )
1439        };
1440        let diagnostic_tokens = estimate_tokens(diagnostic_section.len());
1441        if used_tokens + diagnostic_tokens > budget {
1442            break;
1443        }
1444        output.push_str(&diagnostic_section);
1445        used_tokens += diagnostic_tokens;
1446        included_diagnostics += 1;
1447    }
1448
1449    if included_diagnostics == 0 {
1450        String::new()
1451    } else {
1452        output
1453    }
1454}
1455
1456pub fn filter_redundant_excerpts(
1457    mut related_files: Vec<RelatedFile>,
1458    cursor_path: &Path,
1459    cursor_row_range: Range<u32>,
1460) -> Vec<RelatedFile> {
1461    for file in &mut related_files {
1462        if file.path.as_ref() == cursor_path {
1463            file.excerpts.retain(|excerpt| {
1464                excerpt.row_range.start < cursor_row_range.start
1465                    || excerpt.row_range.end > cursor_row_range.end
1466            });
1467        }
1468    }
1469    related_files.retain(|file| !file.excerpts.is_empty());
1470    related_files
1471}
1472
1473pub fn max_edit_event_count_for_format(format: &ZetaFormat) -> usize {
1474    match format {
1475        ZetaFormat::V0112MiddleAtEnd
1476        | ZetaFormat::V0113Ordered
1477        | ZetaFormat::V0114180EditableRegion
1478        | ZetaFormat::V0120GitMergeMarkers
1479        | ZetaFormat::V0131GitMergeMarkersPrefix
1480        | ZetaFormat::V0211Prefill
1481        | ZetaFormat::V0211SeedCoder
1482        | ZetaFormat::V0331SeedCoderModelPy
1483        | ZetaFormat::v0226Hashline
1484        | ZetaFormat::V0304SeedNoEdits
1485        | ZetaFormat::V0304VariableEdit
1486        | ZetaFormat::V0306SeedMultiRegions
1487        | ZetaFormat::V0316SeedMultiRegions
1488        | ZetaFormat::V0318SeedMultiRegions
1489        | ZetaFormat::V0317SeedMultiRegions
1490        | ZetaFormat::V0420Diagnostics
1491        | ZetaFormat::V0608QwenMultiRegions
1492        | ZetaFormat::V0327SingleFile
1493        | ZetaFormat::V0615HashRegions => 6,
1494    }
1495}
1496
1497pub fn get_prefill_for_format(
1498    format: ZetaFormat,
1499    context: &str,
1500    editable_range: &Range<usize>,
1501) -> String {
1502    match format {
1503        ZetaFormat::V0211Prefill => v0211_prefill::get_prefill(context, editable_range),
1504        ZetaFormat::V0112MiddleAtEnd
1505        | ZetaFormat::V0113Ordered
1506        | ZetaFormat::V0114180EditableRegion
1507        | ZetaFormat::V0120GitMergeMarkers
1508        | ZetaFormat::V0131GitMergeMarkersPrefix
1509        | ZetaFormat::V0211SeedCoder
1510        | ZetaFormat::V0331SeedCoderModelPy
1511        | ZetaFormat::v0226Hashline
1512        | ZetaFormat::V0304VariableEdit => String::new(),
1513        ZetaFormat::V0304SeedNoEdits
1514        | ZetaFormat::V0306SeedMultiRegions
1515        | ZetaFormat::V0316SeedMultiRegions
1516        | ZetaFormat::V0318SeedMultiRegions
1517        | ZetaFormat::V0317SeedMultiRegions
1518        | ZetaFormat::V0420Diagnostics
1519        | ZetaFormat::V0608QwenMultiRegions
1520        | ZetaFormat::V0327SingleFile
1521        | ZetaFormat::V0615HashRegions => String::new(),
1522    }
1523}
1524
1525pub fn output_end_marker_for_format(format: ZetaFormat) -> Option<&'static str> {
1526    match format {
1527        ZetaFormat::V0120GitMergeMarkers => Some(v0120_git_merge_markers::END_MARKER),
1528        ZetaFormat::V0131GitMergeMarkersPrefix => Some(v0131_git_merge_markers_prefix::END_MARKER),
1529        ZetaFormat::V0211Prefill => Some(v0131_git_merge_markers_prefix::END_MARKER),
1530        ZetaFormat::V0211SeedCoder
1531        | ZetaFormat::V0331SeedCoderModelPy
1532        | ZetaFormat::V0304SeedNoEdits
1533        | ZetaFormat::V0306SeedMultiRegions => Some(seed_coder::END_MARKER),
1534        ZetaFormat::V0316SeedMultiRegions => Some(multi_region::V0316_END_MARKER),
1535        ZetaFormat::V0318SeedMultiRegions => Some(multi_region::V0318_END_MARKER),
1536        ZetaFormat::V0420Diagnostics => Some(multi_region::V0318_END_MARKER),
1537        ZetaFormat::V0608QwenMultiRegions => Some(qwen::END_MARKER),
1538        ZetaFormat::V0317SeedMultiRegions => Some(multi_region::V0317_END_MARKER),
1539        ZetaFormat::V0327SingleFile => Some(multi_region::V0327_END_MARKER),
1540        ZetaFormat::V0615HashRegions => Some(hashed_regions::V0615_END_MARKER),
1541
1542        ZetaFormat::V0112MiddleAtEnd
1543        | ZetaFormat::V0113Ordered
1544        | ZetaFormat::V0114180EditableRegion
1545        | ZetaFormat::v0226Hashline
1546        | ZetaFormat::V0304VariableEdit => None,
1547    }
1548}
1549
1550pub fn encode_patch_as_output_for_format(
1551    format: ZetaFormat,
1552    old_editable_region: &str,
1553    patch: &str,
1554    cursor_offset: Option<usize>,
1555) -> Result<Option<String>> {
1556    match format {
1557        ZetaFormat::v0226Hashline => {
1558            hashline::patch_to_edit_commands(old_editable_region, patch, cursor_offset).map(Some)
1559        }
1560        ZetaFormat::V0304VariableEdit => v0304_variable_edit::patch_to_variable_edit_output(
1561            old_editable_region,
1562            patch,
1563            cursor_offset,
1564        )
1565        .map(Some),
1566        ZetaFormat::V0304SeedNoEdits | ZetaFormat::V0306SeedMultiRegions => {
1567            Ok(seed_coder::no_edits(patch))
1568        }
1569        ZetaFormat::V0316SeedMultiRegions => {
1570            let empty_patch = patch.lines().count() <= 3;
1571            if empty_patch {
1572                let marker_offsets = multi_region::compute_marker_offsets(old_editable_region);
1573                let marker_num =
1574                    multi_region::nearest_marker_number(cursor_offset, &marker_offsets);
1575                let tag = multi_region::marker_tag(marker_num);
1576                Ok(Some(format!(
1577                    "{tag}{tag}{}",
1578                    multi_region::V0316_END_MARKER
1579                )))
1580            } else {
1581                Ok(None)
1582            }
1583        }
1584        ZetaFormat::V0318SeedMultiRegions | ZetaFormat::V0420Diagnostics => {
1585            let empty_patch = patch.lines().count() <= 3;
1586            if empty_patch {
1587                let marker_offsets =
1588                    multi_region::compute_marker_offsets_v0318(old_editable_region);
1589                let marker_num =
1590                    multi_region::nearest_marker_number(cursor_offset, &marker_offsets);
1591                let tag = multi_region::marker_tag(marker_num);
1592                Ok(Some(format!(
1593                    "{tag}{tag}{}",
1594                    multi_region::V0318_END_MARKER
1595                )))
1596            } else {
1597                Ok(None)
1598            }
1599        }
1600        ZetaFormat::V0608QwenMultiRegions => {
1601            let empty_patch = patch.lines().count() <= 3;
1602            if empty_patch {
1603                let marker_offsets =
1604                    multi_region::compute_marker_offsets_v0318(old_editable_region);
1605                let marker_num =
1606                    multi_region::nearest_marker_number(cursor_offset, &marker_offsets);
1607                let tag = multi_region::marker_tag(marker_num);
1608                Ok(Some(format!("{tag}{tag}{}", qwen::END_MARKER)))
1609            } else {
1610                Ok(None)
1611            }
1612        }
1613        ZetaFormat::V0317SeedMultiRegions => {
1614            let empty_patch = patch.lines().count() <= 3;
1615            if empty_patch {
1616                let tag = multi_region::marker_tag_relative(0);
1617                Ok(Some(format!(
1618                    "{tag}{tag}{}",
1619                    multi_region::V0317_END_MARKER
1620                )))
1621            } else {
1622                Ok(None)
1623            }
1624        }
1625        ZetaFormat::V0327SingleFile => {
1626            let empty_patch = patch.lines().count() <= 3;
1627            if empty_patch {
1628                let marker_offsets =
1629                    multi_region::compute_marker_offsets_v0318(old_editable_region);
1630                let marker_num =
1631                    multi_region::nearest_marker_number(cursor_offset, &marker_offsets);
1632                let tag = multi_region::marker_tag(marker_num);
1633                Ok(Some(format!(
1634                    "{tag}{tag}{}",
1635                    multi_region::V0327_END_MARKER
1636                )))
1637            } else {
1638                Ok(None)
1639            }
1640        }
1641        ZetaFormat::V0615HashRegions => Ok(None),
1642        _ => Ok(None),
1643    }
1644}
1645
1646/// Given a `Zeta2PromptInput`, a format, and a patch (with cursor already
1647/// extracted), produce the expected model output string for training.
1648pub fn format_expected_output(
1649    input: &Zeta2PromptInput,
1650    format: ZetaFormat,
1651    patch: &str,
1652    cursor_offset: Option<usize>,
1653) -> Result<String> {
1654    if format == ZetaFormat::V0615HashRegions {
1655        return hashed_regions::encode_patch_as_output(input, patch, cursor_offset, CURSOR_MARKER);
1656    }
1657
1658    let (context, editable_range, _, _) = resolve_cursor_region(input, format);
1659    let mut old_editable = context[editable_range].to_string();
1660    if !old_editable.is_empty() && !old_editable.ends_with('\n') {
1661        old_editable.push('\n');
1662    }
1663
1664    // Formats with their own output encoding (hashline, variable-edit,
1665    // multi-region empty patches) are handled here.
1666    if let Some(output) =
1667        encode_patch_as_output_for_format(format, &old_editable, patch, cursor_offset)?
1668    {
1669        return Ok(output);
1670    }
1671
1672    let empty_patch = patch.lines().count() <= 3;
1673
1674    match format {
1675        // Multi-region formats: non-empty patches need diff application
1676        // then marker-span encoding.
1677        ZetaFormat::V0316SeedMultiRegions => {
1678            let (new_editable, first_hunk_offset) =
1679                udiff::apply_diff_to_string_with_hunk_offset(patch, &old_editable)?;
1680            let cursor_in_new = cursor_in_new_text(cursor_offset, first_hunk_offset, &new_editable);
1681            multi_region::encode_from_old_and_new_v0316(
1682                &old_editable,
1683                &new_editable,
1684                cursor_in_new,
1685                CURSOR_MARKER,
1686                multi_region::V0316_END_MARKER,
1687            )
1688        }
1689        ZetaFormat::V0318SeedMultiRegions | ZetaFormat::V0420Diagnostics => {
1690            let (new_editable, first_hunk_offset) =
1691                udiff::apply_diff_to_string_with_hunk_offset(patch, &old_editable)?;
1692            let cursor_in_new = cursor_in_new_text(cursor_offset, first_hunk_offset, &new_editable);
1693            multi_region::encode_from_old_and_new_v0318(
1694                &old_editable,
1695                &new_editable,
1696                cursor_in_new,
1697                CURSOR_MARKER,
1698                multi_region::V0318_END_MARKER,
1699            )
1700        }
1701        ZetaFormat::V0608QwenMultiRegions => {
1702            let (new_editable, first_hunk_offset) =
1703                udiff::apply_diff_to_string_with_hunk_offset(patch, &old_editable)?;
1704            let cursor_in_new = cursor_in_new_text(cursor_offset, first_hunk_offset, &new_editable);
1705            multi_region::encode_from_old_and_new_v0318(
1706                &old_editable,
1707                &new_editable,
1708                cursor_in_new,
1709                CURSOR_MARKER,
1710                qwen::END_MARKER,
1711            )
1712        }
1713        ZetaFormat::V0327SingleFile => {
1714            let (new_editable, first_hunk_offset) =
1715                udiff::apply_diff_to_string_with_hunk_offset(patch, &old_editable)?;
1716            let cursor_in_new = cursor_in_new_text(cursor_offset, first_hunk_offset, &new_editable);
1717            multi_region::encode_from_old_and_new_v0318(
1718                &old_editable,
1719                &new_editable,
1720                cursor_in_new,
1721                CURSOR_MARKER,
1722                multi_region::V0327_END_MARKER,
1723            )
1724        }
1725        ZetaFormat::V0317SeedMultiRegions => {
1726            let (new_editable, first_hunk_offset) =
1727                udiff::apply_diff_to_string_with_hunk_offset(patch, &old_editable)?;
1728            let cursor_in_new = cursor_in_new_text(cursor_offset, first_hunk_offset, &new_editable);
1729            multi_region::encode_from_old_and_new_v0317(
1730                &old_editable,
1731                &new_editable,
1732                cursor_in_new,
1733                CURSOR_MARKER,
1734                multi_region::V0317_END_MARKER,
1735            )
1736        }
1737        // V0131-style formats and fallback: produce new editable text with
1738        // cursor marker inserted, followed by the end marker.
1739        ZetaFormat::V0112MiddleAtEnd
1740        | ZetaFormat::V0113Ordered
1741        | ZetaFormat::V0114180EditableRegion
1742        | ZetaFormat::V0120GitMergeMarkers
1743        | ZetaFormat::V0131GitMergeMarkersPrefix
1744        | ZetaFormat::V0211Prefill
1745        | ZetaFormat::V0211SeedCoder
1746        | ZetaFormat::v0226Hashline
1747        | ZetaFormat::V0304VariableEdit
1748        | ZetaFormat::V0304SeedNoEdits
1749        | ZetaFormat::V0331SeedCoderModelPy
1750        | ZetaFormat::V0306SeedMultiRegions
1751        | ZetaFormat::V0615HashRegions => {
1752            let (mut result, first_hunk_offset) = if empty_patch {
1753                (old_editable.clone(), None)
1754            } else {
1755                udiff::apply_diff_to_string_with_hunk_offset(patch, &old_editable)?
1756            };
1757
1758            if let Some(cursor) = cursor_offset {
1759                let hunk_start = if !empty_patch {
1760                    first_hunk_offset.unwrap_or(0)
1761                } else {
1762                    0
1763                };
1764                let offset = (hunk_start + cursor).min(result.len());
1765                result.insert_str(offset, CURSOR_MARKER);
1766            }
1767
1768            if !result.is_empty() && !result.ends_with('\n') {
1769                result.push('\n');
1770            }
1771
1772            if let Some(end_marker) = output_end_marker_for_format(format) {
1773                result.push_str(end_marker);
1774            }
1775
1776            Ok(result)
1777        }
1778    }
1779}
1780
1781/// Compute the cursor position within the new text after diff application.
1782fn cursor_in_new_text(
1783    cursor_offset: Option<usize>,
1784    first_hunk_offset: Option<usize>,
1785    new_text: &str,
1786) -> Option<usize> {
1787    cursor_offset.map(|cursor| {
1788        let hunk_start = first_hunk_offset.unwrap_or(0);
1789        (hunk_start + cursor).min(new_text.len())
1790    })
1791}
1792
1793#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1794pub struct ParsedOutput {
1795    /// Text that should replace the editable region
1796    pub new_editable_region: String,
1797    /// The byte range within `cursor_excerpt` that this replacement applies to
1798    pub range_in_excerpt: Range<usize>,
1799    /// Byte offset of the cursor marker within `new_editable_region`, if present
1800    pub cursor_offset_in_new_editable_region: Option<usize>,
1801}
1802
1803#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1804pub struct CursorPosition {
1805    pub path: String,
1806    pub row: usize,
1807    pub column: usize,
1808    pub offset: usize,
1809    pub editable_region_offset: usize,
1810}
1811
1812pub fn parsed_output_from_editable_region(
1813    range_in_excerpt: Range<usize>,
1814    mut new_editable_region: String,
1815) -> ParsedOutput {
1816    let cursor_offset_in_new_editable_region = new_editable_region.find(CURSOR_MARKER);
1817    if let Some(offset) = cursor_offset_in_new_editable_region {
1818        new_editable_region.replace_range(offset..offset + CURSOR_MARKER.len(), "");
1819    }
1820
1821    ParsedOutput {
1822        new_editable_region,
1823        range_in_excerpt,
1824        cursor_offset_in_new_editable_region,
1825    }
1826}
1827
1828/// Parse model output for the given zeta format
1829pub fn parse_zeta2_model_output(
1830    output: &str,
1831    format: ZetaFormat,
1832    prompt_inputs: &Zeta2PromptInput,
1833) -> Result<ParsedOutput> {
1834    let output = match output_end_marker_for_format(format) {
1835        Some(marker) => output.strip_suffix(marker).unwrap_or(output),
1836        None => output,
1837    };
1838
1839    let (context, editable_range_in_context, context_range, cursor_offset) =
1840        resolve_cursor_region(prompt_inputs, format);
1841    let context_start = context_range.start;
1842    let old_editable_region = &context[editable_range_in_context.clone()];
1843    let cursor_offset_in_editable = cursor_offset.saturating_sub(editable_range_in_context.start);
1844
1845    let (range_in_context, output) = match format {
1846        ZetaFormat::v0226Hashline => (
1847            editable_range_in_context,
1848            if hashline::output_has_edit_commands(output) {
1849                hashline::apply_edit_commands(old_editable_region, output)
1850            } else {
1851                output.to_string()
1852            },
1853        ),
1854        ZetaFormat::V0304VariableEdit => v0304_variable_edit::apply_variable_edit(context, output)?,
1855        ZetaFormat::V0304SeedNoEdits => (
1856            editable_range_in_context,
1857            if output.starts_with(seed_coder::NO_EDITS) {
1858                old_editable_region.to_string()
1859            } else {
1860                output.to_string()
1861            },
1862        ),
1863        ZetaFormat::V0306SeedMultiRegions => (
1864            editable_range_in_context,
1865            if output.starts_with(seed_coder::NO_EDITS) {
1866                old_editable_region.to_string()
1867            } else {
1868                multi_region::apply_marker_span(old_editable_region, output)?
1869            },
1870        ),
1871        ZetaFormat::V0316SeedMultiRegions => (
1872            editable_range_in_context,
1873            multi_region::apply_marker_span_v0316(old_editable_region, output)?,
1874        ),
1875        ZetaFormat::V0318SeedMultiRegions
1876        | ZetaFormat::V0420Diagnostics
1877        | ZetaFormat::V0608QwenMultiRegions => (
1878            editable_range_in_context,
1879            multi_region::apply_marker_span_v0318(old_editable_region, output)?,
1880        ),
1881        ZetaFormat::V0317SeedMultiRegions => (
1882            editable_range_in_context,
1883            multi_region::apply_marker_span_v0317(
1884                old_editable_region,
1885                output,
1886                Some(cursor_offset_in_editable),
1887            )?,
1888        ),
1889        ZetaFormat::V0327SingleFile => (
1890            editable_range_in_context,
1891            multi_region::apply_marker_span_v0318(old_editable_region, output)?,
1892        ),
1893        ZetaFormat::V0615HashRegions => {
1894            anyhow::bail!(
1895                "V0615HashRegions output addresses related-file context; use parse_zeta2_model_output_as_patch"
1896            )
1897        }
1898        _ => (editable_range_in_context, output.to_string()),
1899    };
1900
1901    let range_in_excerpt =
1902        range_in_context.start + context_start..range_in_context.end + context_start;
1903
1904    Ok(parsed_output_from_editable_region(range_in_excerpt, output))
1905}
1906
1907pub fn parse_zeta2_model_output_as_patch(
1908    output: &str,
1909    format: ZetaFormat,
1910    prompt_inputs: &Zeta2PromptInput,
1911) -> Result<String> {
1912    if format == ZetaFormat::V0615HashRegions {
1913        return hashed_regions::parse_output_as_patch(prompt_inputs, output, CURSOR_MARKER);
1914    }
1915
1916    let parsed = parse_zeta2_model_output(output, format, prompt_inputs)?;
1917    parsed_output_to_patch(prompt_inputs, parsed)
1918}
1919
1920pub fn parse_zeta3_model_output_as_patch(
1921    output: &str,
1922    format: ZetaFormat,
1923    input: &Zeta3PromptInput,
1924) -> Result<String> {
1925    match format {
1926        ZetaFormat::V0318SeedMultiRegions => {}
1927        _ => anyhow::bail!("unsupported Zeta3 output format: {format}"),
1928    }
1929
1930    let output = match output_end_marker_for_format(format) {
1931        Some(marker) => output.strip_suffix(marker).unwrap_or(output),
1932        None => output,
1933    };
1934    let (current_excerpt, cursor_offset_in_excerpt) = zeta3_current_file_excerpt(input)
1935        .ok_or_else(|| anyhow!("Zeta3 input is missing current-file editable context at cursor"))?;
1936    let (context, editable_range_in_context, context_range, _) = resolve_zeta3_cursor_region(
1937        current_excerpt.text.as_ref(),
1938        cursor_offset_in_excerpt,
1939        &input.syntax_ranges,
1940        format,
1941    );
1942    let old_editable_region = &context[editable_range_in_context.clone()];
1943    let range_in_excerpt = editable_range_in_context.start + context_range.start
1944        ..editable_range_in_context.end + context_range.start;
1945    let parsed = parsed_output_from_editable_region(
1946        range_in_excerpt,
1947        multi_region::apply_marker_span_v0318(old_editable_region, output)?,
1948    );
1949
1950    parsed_output_to_patch_for_excerpt(
1951        input.cursor_path.as_ref(),
1952        current_excerpt.text.as_ref(),
1953        current_excerpt.row_range.start,
1954        parsed,
1955    )
1956}
1957
1958pub fn cursor_position_from_parsed_output(
1959    prompt_inputs: &Zeta2PromptInput,
1960    parsed: &ParsedOutput,
1961) -> Option<CursorPosition> {
1962    let cursor_offset = parsed.cursor_offset_in_new_editable_region?;
1963    let editable_region_offset = parsed.range_in_excerpt.start;
1964    let excerpt = prompt_inputs.cursor_excerpt.as_ref();
1965
1966    let editable_region_start_line = excerpt[..editable_region_offset].matches('\n').count();
1967
1968    let new_editable_region = &parsed.new_editable_region;
1969    let prefix_end = cursor_offset.min(new_editable_region.len());
1970    let new_region_prefix = &new_editable_region[..prefix_end];
1971
1972    let row = editable_region_start_line + new_region_prefix.matches('\n').count();
1973
1974    let column = match new_region_prefix.rfind('\n') {
1975        Some(last_newline) => cursor_offset - last_newline - 1,
1976        None => {
1977            let content_prefix = &excerpt[..editable_region_offset];
1978            let content_column = match content_prefix.rfind('\n') {
1979                Some(last_newline) => editable_region_offset - last_newline - 1,
1980                None => editable_region_offset,
1981            };
1982            content_column + cursor_offset
1983        }
1984    };
1985
1986    Some(CursorPosition {
1987        path: prompt_inputs.cursor_path.to_string_lossy().into_owned(),
1988        row,
1989        column,
1990        offset: editable_region_offset + cursor_offset,
1991        editable_region_offset: cursor_offset,
1992    })
1993}
1994
1995pub fn parsed_output_to_patch(
1996    prompt_inputs: &Zeta2PromptInput,
1997    parsed: ParsedOutput,
1998) -> Result<String> {
1999    parsed_output_to_patch_for_excerpt(
2000        prompt_inputs.cursor_path.as_ref(),
2001        prompt_inputs.cursor_excerpt.as_ref(),
2002        0,
2003        parsed,
2004    )
2005}
2006
2007fn parsed_output_to_patch_for_excerpt(
2008    path: &Path,
2009    excerpt: &str,
2010    excerpt_start_row: u32,
2011    parsed: ParsedOutput,
2012) -> Result<String> {
2013    let range_in_excerpt = parsed.range_in_excerpt;
2014    let old_text = excerpt[range_in_excerpt.clone()].to_string();
2015    let mut new_text = parsed.new_editable_region;
2016
2017    let mut old_text_normalized = old_text;
2018    if !new_text.is_empty() && !new_text.ends_with('\n') {
2019        new_text.push('\n');
2020    }
2021    if !old_text_normalized.is_empty() && !old_text_normalized.ends_with('\n') {
2022        old_text_normalized.push('\n');
2023    }
2024
2025    let editable_region_offset = range_in_excerpt.start;
2026    let editable_region_start_line =
2027        excerpt_start_row + excerpt[..editable_region_offset].matches('\n').count() as u32;
2028    let editable_region_lines = old_text_normalized.lines().count() as u32;
2029
2030    let diff = udiff::unified_diff_with_context(
2031        &old_text_normalized,
2032        &new_text,
2033        editable_region_start_line,
2034        editable_region_start_line,
2035        editable_region_lines,
2036    );
2037
2038    let path = path.to_string_lossy().trim_start_matches('/').to_string();
2039    let formatted_diff = format!("--- a/{path}\n+++ b/{path}\n{diff}");
2040
2041    Ok(udiff::encode_cursor_in_patch(
2042        &formatted_diff,
2043        parsed.cursor_offset_in_new_editable_region,
2044    ))
2045}
2046
2047pub fn excerpt_range_for_format(
2048    format: ZetaFormat,
2049    ranges: &ExcerptRanges,
2050) -> (Range<usize>, Range<usize>) {
2051    excerpt_ranges_for_format(format, ranges)
2052}
2053
2054pub fn resolve_cursor_region(
2055    input: &Zeta2PromptInput,
2056    format: ZetaFormat,
2057) -> (&str, Range<usize>, Range<usize>, usize) {
2058    let (editable_range, context_range) = if format == ZetaFormat::V0327SingleFile {
2059        let (editable_tokens, _) = token_limits_for_format(format);
2060        let context_range = 0..input.cursor_excerpt.len();
2061        let editable_range = multi_region::compute_v0327_editable_range(
2062            &input.cursor_excerpt,
2063            input.cursor_offset_in_excerpt,
2064            editable_tokens,
2065        );
2066        (editable_range, context_range)
2067    } else if let Some(syntax_ranges) = &input.syntax_ranges {
2068        let (editable_tokens, context_tokens) = token_limits_for_format(format);
2069        compute_editable_and_context_ranges(
2070            &input.cursor_excerpt,
2071            input.cursor_offset_in_excerpt,
2072            syntax_ranges,
2073            editable_tokens,
2074            context_tokens,
2075        )
2076    } else {
2077        excerpt_range_for_format(format, &input.excerpt_ranges)
2078    };
2079
2080    adjust_cursor_region(
2081        &input.cursor_excerpt,
2082        input.cursor_offset_in_excerpt,
2083        editable_range,
2084        context_range,
2085    )
2086}
2087
2088fn adjust_cursor_region(
2089    cursor_excerpt: &str,
2090    cursor_offset: usize,
2091    editable_range: Range<usize>,
2092    context_range: Range<usize>,
2093) -> (&str, Range<usize>, Range<usize>, usize) {
2094    let context_start = context_range.start;
2095    let context_text = &cursor_excerpt[context_range.clone()];
2096    let adjusted_editable =
2097        (editable_range.start - context_start)..(editable_range.end - context_start);
2098    let adjusted_cursor = cursor_offset - context_start;
2099
2100    (
2101        context_text,
2102        adjusted_editable,
2103        context_range,
2104        adjusted_cursor,
2105    )
2106}
2107
2108pub fn get_prefill(input: &Zeta2PromptInput, format: ZetaFormat) -> String {
2109    let (context, editable_range, _, _) = resolve_cursor_region(input, format);
2110    get_prefill_for_format(format, context, &editable_range)
2111}
2112
2113pub fn format_edit_history_within_budget(
2114    events: &[Arc<Event>],
2115    file_marker: &str,
2116    edit_history_name: &str,
2117    max_tokens: usize,
2118    max_edit_event_count: usize,
2119) -> String {
2120    let header = format!("{}{}\n", file_marker, edit_history_name);
2121    let header_tokens = estimate_tokens(header.len());
2122    if header_tokens >= max_tokens {
2123        return String::new();
2124    }
2125
2126    let mut event_strings: Vec<String> = Vec::new();
2127    let mut total_tokens = header_tokens;
2128
2129    for event in events.iter().rev().take(max_edit_event_count) {
2130        let mut event_str = String::new();
2131        write_event(&mut event_str, event);
2132        let event_tokens = estimate_tokens(event_str.len());
2133
2134        if total_tokens + event_tokens > max_tokens {
2135            break;
2136        }
2137        total_tokens += event_tokens;
2138        event_strings.push(event_str);
2139    }
2140
2141    if event_strings.is_empty() {
2142        return String::new();
2143    }
2144
2145    let mut result = header;
2146    for event_str in event_strings.iter().rev() {
2147        result.push_str(event_str);
2148    }
2149    result
2150}
2151
2152fn excerpt_rendered_tokens(excerpt: &RelatedExcerpt, file_max_row: u32) -> usize {
2153    let needs_newline = !excerpt.text.ends_with('\n');
2154    let needs_ellipsis = excerpt.row_range.end < file_max_row;
2155    let len = excerpt.text.len()
2156        + if needs_newline { "\n".len() } else { 0 }
2157        + if needs_ellipsis { "...\n".len() } else { 0 };
2158    estimate_tokens(len)
2159}
2160
2161pub fn format_related_files_within_budget(
2162    related_files: &[RelatedFile],
2163    file_prefix: &str,
2164    file_suffix: &str,
2165    max_tokens: usize,
2166) -> String {
2167    struct ExcerptCandidate {
2168        file_ix: usize,
2169        excerpt_ix: usize,
2170        order: usize,
2171    }
2172
2173    let mut excerpt_candidates: Vec<ExcerptCandidate> = related_files
2174        .iter()
2175        .enumerate()
2176        .flat_map(|(file_ix, file)| {
2177            file.excerpts
2178                .iter()
2179                .enumerate()
2180                .map(move |(excerpt_ix, e)| ExcerptCandidate {
2181                    file_ix,
2182                    excerpt_ix,
2183                    order: e.order,
2184                })
2185        })
2186        .collect();
2187
2188    // Pre-compute file header strings and their token costs.
2189    let file_headers: Vec<String> = related_files
2190        .iter()
2191        .map(|file| {
2192            let path_str = file.path.to_string_lossy();
2193            format!("{}{}\n", file_prefix, path_str)
2194        })
2195        .collect();
2196
2197    // Sort the excerpts by their order and determine how many fit within the budget.
2198    let mut total_tokens = 0;
2199    let mut included_excerpt_count = 0_usize;
2200    let mut included_file_indices = vec![false; related_files.len()];
2201    excerpt_candidates.sort_by_key(|e| (e.order, e.file_ix, e.excerpt_ix));
2202    for candidate in &excerpt_candidates {
2203        let file = &related_files[candidate.file_ix];
2204        let excerpt = &file.excerpts[candidate.excerpt_ix];
2205        let file_already_included = included_file_indices[candidate.file_ix];
2206        let header_cost = if file_already_included {
2207            0
2208        } else {
2209            estimate_tokens(file_headers[candidate.file_ix].len() + file_suffix.len())
2210        };
2211        let excerpt_cost = excerpt_rendered_tokens(excerpt, file.max_row);
2212        if total_tokens + header_cost + excerpt_cost > max_tokens {
2213            break;
2214        }
2215        total_tokens += header_cost + excerpt_cost;
2216        if !file_already_included {
2217            included_file_indices[candidate.file_ix] = true;
2218        }
2219        included_excerpt_count += 1;
2220    }
2221
2222    excerpt_candidates.truncate(included_excerpt_count);
2223    excerpt_candidates.sort_unstable_by_key(|c| (c.file_ix, c.excerpt_ix));
2224
2225    // Render all of the files that fit within the token budget, in the original order.
2226    let mut result = String::new();
2227    let mut last_file_ix = None;
2228    for (candidate_ix, candidate) in excerpt_candidates.iter().enumerate() {
2229        if last_file_ix != Some(candidate.file_ix) {
2230            if last_file_ix.is_some() {
2231                result.push_str(file_suffix);
2232            }
2233            result.push_str(&file_headers[candidate.file_ix]);
2234            last_file_ix = Some(candidate.file_ix);
2235        }
2236        let file = &related_files[candidate.file_ix];
2237        let excerpt = &file.excerpts[candidate.excerpt_ix];
2238        result.push_str(&excerpt.text);
2239        if !result.ends_with('\n') {
2240            result.push('\n');
2241        }
2242        let next_excerpt_start = excerpt_candidates
2243            .get(candidate_ix + 1)
2244            .filter(|next| next.file_ix == candidate.file_ix)
2245            .map(|next| file.excerpts[next.excerpt_ix].row_range.start);
2246        if rows_omitted_after_excerpt(excerpt, next_excerpt_start, file.max_row) {
2247            result.push_str("...\n");
2248        }
2249    }
2250
2251    result
2252}
2253
2254/// Whether rows are omitted between this excerpt and the next rendered
2255/// excerpt of the same file (or the end of the file), in which case an
2256/// ellipsis line should be rendered.
2257pub fn rows_omitted_after_excerpt(
2258    excerpt: &RelatedExcerpt,
2259    next_excerpt_start: Option<u32>,
2260    file_max_row: u32,
2261) -> bool {
2262    match next_excerpt_start {
2263        Some(next_start) => excerpt.row_range.end < next_start,
2264        None => excerpt.row_range.end < file_max_row,
2265    }
2266}
2267
2268pub fn write_related_files(
2269    prompt: &mut String,
2270    related_files: &[RelatedFile],
2271) -> Vec<Range<usize>> {
2272    let mut ranges = Vec::new();
2273    for file in related_files {
2274        let start = prompt.len();
2275        let path_str = file.path.to_string_lossy();
2276        write!(prompt, "<|file_sep|>{}\n", path_str).ok();
2277        for (excerpt_ix, excerpt) in file.excerpts.iter().enumerate() {
2278            prompt.push_str(&excerpt.text);
2279            if !prompt.ends_with('\n') {
2280                prompt.push('\n');
2281            }
2282            let next_excerpt_start = file
2283                .excerpts
2284                .get(excerpt_ix + 1)
2285                .map(|next| next.row_range.start);
2286            if rows_omitted_after_excerpt(excerpt, next_excerpt_start, file.max_row) {
2287                prompt.push_str("...\n");
2288            }
2289        }
2290        let end = prompt.len();
2291        ranges.push(start..end);
2292    }
2293    ranges
2294}
2295
2296mod v0112_middle_at_end {
2297    use super::*;
2298
2299    pub fn special_tokens() -> &'static [&'static str] {
2300        &[
2301            "<|fim_prefix|>",
2302            "<|fim_suffix|>",
2303            "<|fim_middle|>",
2304            "<|file_sep|>",
2305            CURSOR_MARKER,
2306        ]
2307    }
2308
2309    pub fn write_cursor_excerpt_section(
2310        prompt: &mut String,
2311        path: &Path,
2312        context: &str,
2313        editable_range: &Range<usize>,
2314        cursor_offset: usize,
2315    ) {
2316        let path_str = path.to_string_lossy();
2317        write!(prompt, "<|file_sep|>{}\n", path_str).ok();
2318
2319        prompt.push_str("<|fim_prefix|>\n");
2320        prompt.push_str(&context[..editable_range.start]);
2321
2322        prompt.push_str("<|fim_suffix|>\n");
2323        prompt.push_str(&context[editable_range.end..]);
2324        if !prompt.ends_with('\n') {
2325            prompt.push('\n');
2326        }
2327
2328        prompt.push_str("<|fim_middle|>current\n");
2329        prompt.push_str(&context[editable_range.start..cursor_offset]);
2330        prompt.push_str(CURSOR_MARKER);
2331        prompt.push_str(&context[cursor_offset..editable_range.end]);
2332        if !prompt.ends_with('\n') {
2333            prompt.push('\n');
2334        }
2335
2336        prompt.push_str("<|fim_middle|>updated\n");
2337    }
2338}
2339
2340mod v0113_ordered {
2341    use super::*;
2342
2343    pub fn special_tokens() -> &'static [&'static str] {
2344        &[
2345            "<|fim_prefix|>",
2346            "<|fim_suffix|>",
2347            "<|fim_middle|>",
2348            "<|file_sep|>",
2349            CURSOR_MARKER,
2350        ]
2351    }
2352
2353    pub fn write_cursor_excerpt_section(
2354        prompt: &mut String,
2355        path: &Path,
2356        context: &str,
2357        editable_range: &Range<usize>,
2358        cursor_offset: usize,
2359    ) {
2360        let path_str = path.to_string_lossy();
2361        write!(prompt, "<|file_sep|>{}\n", path_str).ok();
2362
2363        prompt.push_str("<|fim_prefix|>\n");
2364        prompt.push_str(&context[..editable_range.start]);
2365        if !prompt.ends_with('\n') {
2366            prompt.push('\n');
2367        }
2368
2369        prompt.push_str("<|fim_middle|>current\n");
2370        prompt.push_str(&context[editable_range.start..cursor_offset]);
2371        prompt.push_str(CURSOR_MARKER);
2372        prompt.push_str(&context[cursor_offset..editable_range.end]);
2373        if !prompt.ends_with('\n') {
2374            prompt.push('\n');
2375        }
2376
2377        prompt.push_str("<|fim_suffix|>\n");
2378        prompt.push_str(&context[editable_range.end..]);
2379        if !prompt.ends_with('\n') {
2380            prompt.push('\n');
2381        }
2382
2383        prompt.push_str("<|fim_middle|>updated\n");
2384    }
2385}
2386
2387mod v0114180_editable_region {
2388    use super::*;
2389
2390    pub fn special_tokens() -> &'static [&'static str] {
2391        v0113_ordered::special_tokens()
2392    }
2393}
2394
2395pub mod v0120_git_merge_markers {
2396    //! A prompt that uses git-style merge conflict markers to represent the editable region.
2397    //!
2398    //! Example prompt:
2399    //!
2400    //! <|file_sep|>path/to/target_file.py
2401    //! <|fim_prefix|>
2402    //! code before editable region
2403    //! <|fim_suffix|>
2404    //! code after editable region
2405    //! <|fim_middle|>
2406    //! <<<<<<< CURRENT
2407    //! code that
2408    //! needs to<|user_cursor|>
2409    //! be rewritten
2410    //! =======
2411    //!
2412    //! Expected output (should be generated by the model):
2413    //!
2414    //! updated
2415    //! code with
2416    //! changes applied
2417    //! >>>>>>> UPDATED
2418
2419    use super::*;
2420
2421    pub const START_MARKER: &str = "<<<<<<< CURRENT\n";
2422    pub const SEPARATOR: &str = "=======\n";
2423    pub const END_MARKER: &str = ">>>>>>> UPDATED\n";
2424
2425    pub fn special_tokens() -> &'static [&'static str] {
2426        &[
2427            "<|fim_prefix|>",
2428            "<|fim_suffix|>",
2429            "<|fim_middle|>",
2430            "<|file_sep|>",
2431            START_MARKER,
2432            SEPARATOR,
2433            END_MARKER,
2434            CURSOR_MARKER,
2435        ]
2436    }
2437
2438    pub fn write_cursor_excerpt_section(
2439        prompt: &mut String,
2440        path: &Path,
2441        context: &str,
2442        editable_range: &Range<usize>,
2443        cursor_offset: usize,
2444    ) {
2445        let path_str = path.to_string_lossy();
2446        write!(prompt, "<|file_sep|>{}\n", path_str).ok();
2447
2448        prompt.push_str("<|fim_prefix|>");
2449        prompt.push_str(&context[..editable_range.start]);
2450
2451        prompt.push_str("<|fim_suffix|>");
2452        prompt.push_str(&context[editable_range.end..]);
2453        if !prompt.ends_with('\n') {
2454            prompt.push('\n');
2455        }
2456
2457        prompt.push_str("<|fim_middle|>");
2458        prompt.push_str(START_MARKER);
2459        prompt.push_str(&context[editable_range.start..cursor_offset]);
2460        prompt.push_str(CURSOR_MARKER);
2461        prompt.push_str(&context[cursor_offset..editable_range.end]);
2462        if !prompt.ends_with('\n') {
2463            prompt.push('\n');
2464        }
2465        prompt.push_str(SEPARATOR);
2466    }
2467}
2468
2469pub mod v0131_git_merge_markers_prefix {
2470    //! A prompt that uses git-style merge conflict markers to represent the editable region.
2471    //!
2472    //! Example prompt:
2473    //!
2474    //! <|file_sep|>path/to/target_file.py
2475    //! <|fim_prefix|>
2476    //! code before editable region
2477    //! <<<<<<< CURRENT
2478    //! code that
2479    //! needs to<|user_cursor|>
2480    //! be rewritten
2481    //! =======
2482    //! <|fim_suffix|>
2483    //! code after editable region
2484    //! <|fim_middle|>
2485    //!
2486    //! Expected output (should be generated by the model):
2487    //!
2488    //! updated
2489    //! code with
2490    //! changes applied
2491    //! >>>>>>> UPDATED
2492
2493    use super::*;
2494
2495    pub const START_MARKER: &str = "<<<<<<< CURRENT\n";
2496    pub const SEPARATOR: &str = "=======\n";
2497    pub const END_MARKER: &str = ">>>>>>> UPDATED\n";
2498
2499    pub fn special_tokens() -> &'static [&'static str] {
2500        &[
2501            "<|fim_prefix|>",
2502            "<|fim_suffix|>",
2503            "<|fim_middle|>",
2504            "<|file_sep|>",
2505            START_MARKER,
2506            SEPARATOR,
2507            END_MARKER,
2508            CURSOR_MARKER,
2509        ]
2510    }
2511
2512    pub fn write_cursor_excerpt_section(
2513        prompt: &mut String,
2514        path: &Path,
2515        context: &str,
2516        editable_range: &Range<usize>,
2517        cursor_offset: usize,
2518    ) {
2519        let path_str = path.to_string_lossy();
2520        write!(prompt, "<|file_sep|>{}\n", path_str).ok();
2521
2522        prompt.push_str("<|fim_prefix|>");
2523        prompt.push_str(&context[..editable_range.start]);
2524        prompt.push_str(START_MARKER);
2525        prompt.push_str(&context[editable_range.start..cursor_offset]);
2526        prompt.push_str(CURSOR_MARKER);
2527        prompt.push_str(&context[cursor_offset..editable_range.end]);
2528        if !prompt.ends_with('\n') {
2529            prompt.push('\n');
2530        }
2531        prompt.push_str(SEPARATOR);
2532
2533        prompt.push_str("<|fim_suffix|>");
2534        prompt.push_str(&context[editable_range.end..]);
2535        if !prompt.ends_with('\n') {
2536            prompt.push('\n');
2537        }
2538
2539        prompt.push_str("<|fim_middle|>");
2540    }
2541}
2542
2543pub mod v0211_prefill {
2544    use super::*;
2545
2546    pub fn special_tokens() -> &'static [&'static str] {
2547        v0131_git_merge_markers_prefix::special_tokens()
2548    }
2549
2550    pub fn get_prefill(context: &str, editable_range: &Range<usize>) -> String {
2551        let editable_region = &context[editable_range.start..editable_range.end];
2552
2553        let prefill_len = (editable_region.len() as f64 * PREFILL_RATIO) as usize;
2554        let prefill_len = editable_region.floor_char_boundary(prefill_len);
2555
2556        // Find a token boundary to avoid splitting tokens in the prefill.
2557        // In Qwen2.5-Coder, \n is always the END of a token (e.g. `;\n`,
2558        // ` {\n`), and \n\n / \n\n\n are single tokens, so we must include
2559        // the \n and consume any consecutive \n characters after it.
2560        let prefill = &editable_region[..prefill_len];
2561        match prefill.rfind('\n') {
2562            Some(pos) => {
2563                let mut end = pos + 1;
2564                while end < editable_region.len()
2565                    && editable_region.as_bytes().get(end) == Some(&b'\n')
2566                {
2567                    end += 1;
2568                }
2569                editable_region[..end].to_string()
2570            }
2571            // No newline found. Fall back to splitting before the last space
2572            // (word-level boundary)
2573            None => match prefill.rfind(' ') {
2574                Some(pos) => prefill[..pos].to_string(),
2575                None => prefill.to_string(),
2576            },
2577        }
2578    }
2579}
2580
2581pub mod hashline {
2582
2583    use std::fmt::Display;
2584
2585    pub const END_MARKER: &str = "<|fim_middle|>updated";
2586    pub const START_MARKER: &str = "<|fim_middle|>current";
2587
2588    use super::*;
2589
2590    const SET_COMMAND_MARKER: &str = "<|set|>";
2591    const INSERT_COMMAND_MARKER: &str = "<|insert|>";
2592    pub const NO_EDITS_COMMAND_MARKER: &str = "<|no_edits|>";
2593
2594    pub fn special_tokens() -> &'static [&'static str] {
2595        return &[
2596            SET_COMMAND_MARKER,
2597            "<|set_range|>",
2598            INSERT_COMMAND_MARKER,
2599            NO_EDITS_COMMAND_MARKER,
2600            CURSOR_MARKER,
2601            "<|file_sep|>",
2602            "<|fim_prefix|>",
2603            "<|fim_suffix|>",
2604            "<|fim_middle|>",
2605        ];
2606    }
2607
2608    /// A parsed line reference like `3:c3` (line index 3 with hash 0xc3).
2609    #[derive(Debug, Clone, PartialEq, Eq)]
2610    struct LineRef {
2611        index: usize,
2612        hash: u8,
2613    }
2614
2615    impl Display for LineRef {
2616        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2617            write!(f, "{}:{:02x}", self.index, self.hash)
2618        }
2619    }
2620
2621    pub fn hash_line(line: &[u8]) -> u8 {
2622        let mut h: u8 = 0;
2623        for &byte in line {
2624            h = h.wrapping_add(byte);
2625        }
2626        return h;
2627    }
2628
2629    /// Write the hashline-encoded editable region into `out`. Each line of
2630    /// `editable_text` is prefixed with `{line_index}:{hash}|` and the cursor
2631    /// marker is inserted at `cursor_offset_in_editable` (byte offset relative
2632    /// to the start of `editable_text`).
2633    pub fn write_hashline_editable_region(
2634        out: &mut String,
2635        editable_text: &str,
2636        cursor_offset_in_editable: usize,
2637    ) {
2638        let mut offset = 0;
2639        for (i, line) in editable_text.lines().enumerate() {
2640            let (head, cursor, tail) = if cursor_offset_in_editable > offset
2641                && cursor_offset_in_editable < offset + line.len()
2642            {
2643                (
2644                    &line[..cursor_offset_in_editable - offset],
2645                    CURSOR_MARKER,
2646                    &line[cursor_offset_in_editable - offset..],
2647                )
2648            } else {
2649                (line, "", "")
2650            };
2651            write!(
2652                out,
2653                "\n{}|{head}{cursor}{tail}",
2654                LineRef {
2655                    index: i,
2656                    hash: hash_line(line.as_bytes())
2657                }
2658            )
2659            .unwrap();
2660            offset += line.len() + 1;
2661        }
2662    }
2663
2664    pub fn write_cursor_excerpt_section(
2665        prompt: &mut String,
2666        path: &Path,
2667        context: &str,
2668        editable_range: &Range<usize>,
2669        cursor_offset: usize,
2670    ) {
2671        let path_str = path.to_string_lossy();
2672        write!(prompt, "<|file_sep|>{}\n", path_str).ok();
2673
2674        prompt.push_str("<|fim_prefix|>\n");
2675        prompt.push_str(&context[..editable_range.start]);
2676        prompt.push_str(START_MARKER);
2677
2678        let cursor_offset_in_editable = cursor_offset.saturating_sub(editable_range.start);
2679        let editable_region = &context[editable_range.clone()];
2680        write_hashline_editable_region(prompt, editable_region, cursor_offset_in_editable);
2681
2682        if !prompt.ends_with('\n') {
2683            prompt.push('\n');
2684        }
2685
2686        prompt.push_str("<|fim_suffix|>\n");
2687        prompt.push_str(&context[editable_range.end..]);
2688        if !prompt.ends_with('\n') {
2689            prompt.push('\n');
2690        }
2691
2692        prompt.push_str(END_MARKER);
2693        prompt.push('\n');
2694    }
2695
2696    /// A single edit command parsed from the model output.
2697    #[derive(Debug)]
2698    enum EditCommand<'a> {
2699        /// Replace a range of lines (inclusive on both ends). Single-line set is
2700        /// represented by `start == end`.
2701        Set {
2702            start: LineRef,
2703            end: LineRef,
2704            content: &'a str,
2705        },
2706        /// Insert new lines after the given line, or before the first line if
2707        /// `after` is `None`.
2708        Insert {
2709            after: Option<LineRef>,
2710            content: &'a str,
2711        },
2712    }
2713
2714    /// Parse a line reference like `3:c3` into a `LineRef`.
2715    fn parse_line_ref(s: &str) -> Option<LineRef> {
2716        let (idx_str, hash_str) = s.split_once(':')?;
2717        let index = idx_str.parse::<usize>().ok()?;
2718        let hash = u8::from_str_radix(hash_str, 16).ok()?;
2719        Some(LineRef { index, hash })
2720    }
2721
2722    /// Parse the model output into a list of `EditCommand`s.
2723    fn parse_edit_commands(model_output: &str) -> Vec<EditCommand<'_>> {
2724        let mut commands = Vec::new();
2725        let mut offset = 0usize;
2726
2727        while offset < model_output.len() {
2728            let next_nl = model_output[offset..]
2729                .find('\n')
2730                .map(|i| offset + i)
2731                .unwrap_or(model_output.len());
2732            let line = &model_output[offset..next_nl];
2733            let line_end = if next_nl < model_output.len() {
2734                next_nl + 1
2735            } else {
2736                next_nl
2737            };
2738
2739            let trimmed = line.trim();
2740            let (is_set, specifier) = if let Some(spec) = trimmed.strip_prefix(SET_COMMAND_MARKER) {
2741                (true, spec)
2742            } else if let Some(spec) = trimmed.strip_prefix(INSERT_COMMAND_MARKER) {
2743                (false, spec)
2744            } else {
2745                offset = line_end;
2746                continue;
2747            };
2748
2749            let mut content_end = line_end;
2750            let mut scan = line_end;
2751
2752            while scan < model_output.len() {
2753                let body_nl = model_output[scan..]
2754                    .find('\n')
2755                    .map(|i| scan + i)
2756                    .unwrap_or(model_output.len());
2757                let body_line = &model_output[scan..body_nl];
2758                if body_line.trim().starts_with(SET_COMMAND_MARKER)
2759                    || body_line.trim().starts_with(INSERT_COMMAND_MARKER)
2760                {
2761                    break;
2762                }
2763                scan = if body_nl < model_output.len() {
2764                    body_nl + 1
2765                } else {
2766                    body_nl
2767                };
2768                content_end = scan;
2769            }
2770
2771            let content = &model_output[line_end..content_end];
2772
2773            if is_set {
2774                if let Some((start_str, end_str)) = specifier.split_once('-') {
2775                    if let (Some(start), Some(end)) =
2776                        (parse_line_ref(start_str), parse_line_ref(end_str))
2777                    {
2778                        commands.push(EditCommand::Set {
2779                            start,
2780                            end,
2781                            content,
2782                        });
2783                    }
2784                } else if let Some(target) = parse_line_ref(specifier) {
2785                    commands.push(EditCommand::Set {
2786                        start: target.clone(),
2787                        end: target,
2788                        content,
2789                    });
2790                }
2791            } else {
2792                let after = parse_line_ref(specifier);
2793                commands.push(EditCommand::Insert { after, content });
2794            }
2795
2796            offset = scan;
2797        }
2798
2799        commands
2800    }
2801
2802    /// Returns `true` if the model output contains `<|set|>` or `<|insert|>` commands
2803    /// (as opposed to being a plain full-replacement output).
2804    /// Strip the `{line_num}:{hash}|` prefixes from each line of a hashline-encoded
2805    /// editable region, returning the plain text content.
2806    pub fn strip_hashline_prefixes(region: &str) -> String {
2807        let mut decoded: String = region
2808            .lines()
2809            .map(|line| line.find('|').map_or(line, |pos| &line[pos + 1..]))
2810            .collect::<Vec<_>>()
2811            .join("\n");
2812        if region.ends_with('\n') {
2813            decoded.push('\n');
2814        }
2815        decoded
2816    }
2817
2818    pub fn output_has_edit_commands(model_output: &str) -> bool {
2819        model_output.contains(SET_COMMAND_MARKER)
2820            || model_output.contains(INSERT_COMMAND_MARKER)
2821            || model_output.contains(NO_EDITS_COMMAND_MARKER)
2822    }
2823
2824    /// Apply `<|set|>` and `<|insert|>` edit commands from the model output to the
2825    /// original editable region text.
2826    ///
2827    /// `editable_region` is the original text of the editable region (without hash
2828    /// prefixes). `model_output` is the raw model response containing edit commands.
2829    ///
2830    /// Returns the full replacement text for the editable region.
2831    pub fn apply_edit_commands(editable_region: &str, model_output: &str) -> String {
2832        if model_output
2833            .trim_start()
2834            .starts_with(NO_EDITS_COMMAND_MARKER)
2835        {
2836            return editable_region.to_string();
2837        }
2838
2839        let original_lines: Vec<&str> = editable_region.lines().collect();
2840        let old_hashes: Vec<u8> = original_lines
2841            .iter()
2842            .map(|line| hash_line(line.as_bytes()))
2843            .collect();
2844
2845        let commands = parse_edit_commands(model_output);
2846
2847        // For set operations: indexed by start line → Some((end line index, content))
2848        // For insert operations: indexed by line index → vec of content to insert after
2849        // Insert-before-first is tracked separately.
2850        let mut set_ops: Vec<Option<(usize, &str)>> = vec![None; original_lines.len()];
2851        let mut insert_before_first: Vec<&str> = Vec::new();
2852        let mut insert_after: Vec<Vec<&str>> = vec![Vec::new(); original_lines.len()];
2853
2854        for command in &commands {
2855            match command {
2856                EditCommand::Set {
2857                    start,
2858                    end,
2859                    content,
2860                } => {
2861                    if start.index < old_hashes.len()
2862                        && end.index < old_hashes.len()
2863                        && start.index <= end.index
2864                        && old_hashes[start.index] == start.hash
2865                        && old_hashes[end.index] == end.hash
2866                    {
2867                        set_ops[start.index] = Some((end.index, *content));
2868                    }
2869                }
2870                EditCommand::Insert { after, content } => match after {
2871                    None => insert_before_first.push(*content),
2872                    Some(line_ref) => {
2873                        if line_ref.index < old_hashes.len()
2874                            && old_hashes[line_ref.index] == line_ref.hash
2875                        {
2876                            insert_after[line_ref.index].push(*content);
2877                        }
2878                    }
2879                },
2880            }
2881        }
2882
2883        let mut result = String::new();
2884
2885        // Emit any insertions before the first line
2886        for content in &insert_before_first {
2887            result.push_str(content);
2888            if !content.ends_with('\n') {
2889                result.push('\n');
2890            }
2891        }
2892
2893        let mut i = 0;
2894        while i < original_lines.len() {
2895            if let Some((end_index, replacement)) = set_ops[i].as_ref() {
2896                // Replace lines i..=end_index with the replacement content
2897                result.push_str(replacement);
2898                if !replacement.is_empty() && !replacement.ends_with('\n') {
2899                    result.push('\n');
2900                }
2901                // Emit any insertions after the end of this set range
2902                if *end_index < insert_after.len() {
2903                    for content in &insert_after[*end_index] {
2904                        result.push_str(content);
2905                        if !content.ends_with('\n') {
2906                            result.push('\n');
2907                        }
2908                    }
2909                }
2910                i = end_index + 1;
2911            } else {
2912                // Keep the original line
2913                result.push_str(original_lines[i]);
2914                result.push('\n');
2915                // Emit any insertions after this line
2916                for content in &insert_after[i] {
2917                    result.push_str(content);
2918                    if !content.ends_with('\n') {
2919                        result.push('\n');
2920                    }
2921                }
2922                i += 1;
2923            }
2924        }
2925
2926        // Preserve trailing newline behavior: if the original ended with a
2927        // newline the result already has one; if it didn't, trim the extra one
2928        // we added.
2929        if !editable_region.ends_with('\n') && result.ends_with('\n') {
2930            result.pop();
2931        }
2932
2933        result
2934    }
2935
2936    /// Convert a unified diff patch into hashline edit commands.
2937    ///
2938    /// Parses the unified diff `patch` directly to determine which lines of
2939    /// `old_text` are deleted/replaced and what new lines are added, then emits
2940    /// `<|set|>` and `<|insert|>` edit commands referencing old lines by their
2941    /// `{index}:{hash}` identifiers.
2942    ///
2943    /// `cursor_offset` is an optional byte offset into the first hunk's new
2944    /// text (context + additions) where the cursor marker should be placed.
2945    pub fn patch_to_edit_commands(
2946        old_text: &str,
2947        patch: &str,
2948        cursor_offset: Option<usize>,
2949    ) -> Result<String> {
2950        let old_lines: Vec<&str> = old_text.lines().collect();
2951        let old_hashes: Vec<u8> = old_lines
2952            .iter()
2953            .map(|line| hash_line(line.as_bytes()))
2954            .collect();
2955
2956        let mut result = String::new();
2957        let mut first_hunk = true;
2958
2959        struct Hunk<'a> {
2960            line_range: Range<usize>,
2961            new_text_lines: Vec<&'a str>,
2962            cursor_line_offset_in_new_text: Option<(usize, usize)>,
2963        }
2964
2965        // Parse the patch line by line. We only care about hunk headers,
2966        // context, deletions, and additions.
2967        let mut old_line_index: usize = 0;
2968        let mut current_hunk: Option<Hunk> = None;
2969        // Byte offset tracking within the hunk's new text for cursor placement.
2970        let mut new_text_byte_offset: usize = 0;
2971        // The line index of the last old line seen before/in the current hunk
2972        // (used for insert-after reference).
2973        let mut last_old_line_before_hunk: Option<usize> = None;
2974
2975        fn flush_hunk(
2976            hunk: Hunk,
2977            last_old_line: Option<usize>,
2978            result: &mut String,
2979            old_hashes: &[u8],
2980        ) {
2981            if hunk.line_range.is_empty() {
2982                // Pure insertion — reference the old line to insert after when in bounds.
2983                if let Some(after) = last_old_line
2984                    && let Some(&hash) = old_hashes.get(after)
2985                {
2986                    write!(
2987                        result,
2988                        "{INSERT_COMMAND_MARKER}{}\n",
2989                        LineRef { index: after, hash }
2990                    )
2991                    .unwrap();
2992                } else {
2993                    result.push_str(INSERT_COMMAND_MARKER);
2994                    result.push('\n');
2995                }
2996            } else {
2997                let start = hunk.line_range.start;
2998                let end_exclusive = hunk.line_range.end;
2999                let deleted_line_count = end_exclusive.saturating_sub(start);
3000
3001                if deleted_line_count == 1 {
3002                    if let Some(&hash) = old_hashes.get(start) {
3003                        write!(
3004                            result,
3005                            "{SET_COMMAND_MARKER}{}\n",
3006                            LineRef { index: start, hash }
3007                        )
3008                        .unwrap();
3009                    } else {
3010                        result.push_str(SET_COMMAND_MARKER);
3011                        result.push('\n');
3012                    }
3013                } else {
3014                    let end_inclusive = end_exclusive - 1;
3015                    match (
3016                        old_hashes.get(start).copied(),
3017                        old_hashes.get(end_inclusive).copied(),
3018                    ) {
3019                        (Some(start_hash), Some(end_hash)) => {
3020                            write!(
3021                                result,
3022                                "{SET_COMMAND_MARKER}{}-{}\n",
3023                                LineRef {
3024                                    index: start,
3025                                    hash: start_hash
3026                                },
3027                                LineRef {
3028                                    index: end_inclusive,
3029                                    hash: end_hash
3030                                }
3031                            )
3032                            .unwrap();
3033                        }
3034                        _ => {
3035                            result.push_str(SET_COMMAND_MARKER);
3036                            result.push('\n');
3037                        }
3038                    }
3039                }
3040            }
3041            for (line_offset, line) in hunk.new_text_lines.iter().enumerate() {
3042                if let Some((cursor_line_offset, char_offset)) = hunk.cursor_line_offset_in_new_text
3043                    && line_offset == cursor_line_offset
3044                {
3045                    result.push_str(&line[..char_offset]);
3046                    result.push_str(CURSOR_MARKER);
3047                    result.push_str(&line[char_offset..]);
3048                    continue;
3049                }
3050
3051                result.push_str(line);
3052            }
3053        }
3054
3055        for raw_line in patch.split_inclusive('\n') {
3056            if raw_line.starts_with("@@") {
3057                // Flush any pending change hunk from a previous patch hunk.
3058                if let Some(hunk) = current_hunk.take() {
3059                    flush_hunk(hunk, last_old_line_before_hunk, &mut result, &old_hashes);
3060                }
3061
3062                // Parse hunk header: @@ -old_start[,old_count] +new_start[,new_count] @@
3063                // We intentionally do not trust old_start as a direct local index into `old_text`,
3064                // because some patches are produced against a larger file region and carry
3065                // non-local line numbers. We keep indexing local by advancing from parsed patch lines.
3066                if first_hunk {
3067                    new_text_byte_offset = 0;
3068                    first_hunk = false;
3069                }
3070                continue;
3071            }
3072
3073            if raw_line.starts_with("---") || raw_line.starts_with("+++") {
3074                continue;
3075            }
3076            if raw_line.starts_with("\\ No newline") {
3077                continue;
3078            }
3079
3080            if raw_line.starts_with('-') {
3081                // Extend or start a change hunk with this deleted old line.
3082                match &mut current_hunk {
3083                    Some(Hunk {
3084                        line_range: range, ..
3085                    }) => range.end = old_line_index + 1,
3086                    None => {
3087                        current_hunk = Some(Hunk {
3088                            line_range: old_line_index..old_line_index + 1,
3089                            new_text_lines: Vec::new(),
3090                            cursor_line_offset_in_new_text: None,
3091                        });
3092                    }
3093                }
3094                old_line_index += 1;
3095            } else if let Some(added_content) = raw_line.strip_prefix('+') {
3096                // Place cursor marker if cursor_offset falls within this line.
3097                let mut cursor_line_offset = None;
3098                if let Some(cursor_off) = cursor_offset
3099                    && (first_hunk
3100                        || cursor_off >= new_text_byte_offset
3101                            && cursor_off <= new_text_byte_offset + added_content.len())
3102                {
3103                    let line_offset = added_content.floor_char_boundary(
3104                        cursor_off
3105                            .saturating_sub(new_text_byte_offset)
3106                            .min(added_content.len()),
3107                    );
3108                    cursor_line_offset = Some(line_offset);
3109                }
3110
3111                new_text_byte_offset += added_content.len();
3112
3113                let hunk = current_hunk.get_or_insert(Hunk {
3114                    line_range: old_line_index..old_line_index,
3115                    new_text_lines: vec![],
3116                    cursor_line_offset_in_new_text: None,
3117                });
3118                hunk.new_text_lines.push(added_content);
3119                hunk.cursor_line_offset_in_new_text = cursor_line_offset
3120                    .map(|offset_in_line| (hunk.new_text_lines.len() - 1, offset_in_line));
3121            } else {
3122                // Context line (starts with ' ' or is empty).
3123                if let Some(hunk) = current_hunk.take() {
3124                    flush_hunk(hunk, last_old_line_before_hunk, &mut result, &old_hashes);
3125                }
3126                last_old_line_before_hunk = Some(old_line_index);
3127                old_line_index += 1;
3128                let content = raw_line.strip_prefix(' ').unwrap_or(raw_line);
3129                new_text_byte_offset += content.len();
3130            }
3131        }
3132
3133        // Flush final group.
3134        if let Some(hunk) = current_hunk.take() {
3135            flush_hunk(hunk, last_old_line_before_hunk, &mut result, &old_hashes);
3136        }
3137
3138        // Trim a single trailing newline.
3139        if result.ends_with('\n') {
3140            result.pop();
3141        }
3142
3143        if result.is_empty() {
3144            return Ok(NO_EDITS_COMMAND_MARKER.to_string());
3145        }
3146
3147        Ok(result)
3148    }
3149
3150    #[cfg(test)]
3151    mod tests {
3152        use super::*;
3153        use indoc::indoc;
3154
3155        #[test]
3156        fn test_format_cursor_region() {
3157            struct Case {
3158                name: &'static str,
3159                context: &'static str,
3160                editable_range: Range<usize>,
3161                cursor_offset: usize,
3162                expected: &'static str,
3163            }
3164
3165            let cases = [
3166                Case {
3167                    name: "basic_cursor_placement",
3168                    context: "hello world\n",
3169                    editable_range: 0..12,
3170                    cursor_offset: 5,
3171                    expected: indoc! {"
3172                    <|file_sep|>test.rs
3173                    <|fim_prefix|>
3174                    <|fim_middle|>current
3175                    0:5c|hello<|user_cursor|> world
3176                    <|fim_suffix|>
3177                    <|fim_middle|>updated
3178                    "},
3179                },
3180                Case {
3181                    name: "multiline_cursor_on_second_line",
3182                    context: "aaa\nbbb\nccc\n",
3183                    editable_range: 0..12,
3184                    cursor_offset: 5, // byte 5 → 1 byte into "bbb"
3185                    expected: indoc! {"
3186                    <|file_sep|>test.rs
3187                    <|fim_prefix|>
3188                    <|fim_middle|>current
3189                    0:23|aaa
3190                    1:26|b<|user_cursor|>bb
3191                    2:29|ccc
3192                    <|fim_suffix|>
3193                    <|fim_middle|>updated
3194                    "},
3195                },
3196                Case {
3197                    name: "no_trailing_newline_in_context",
3198                    context: "line1\nline2",
3199                    editable_range: 0..11,
3200                    cursor_offset: 3,
3201                    expected: indoc! {"
3202                    <|file_sep|>test.rs
3203                    <|fim_prefix|>
3204                    <|fim_middle|>current
3205                    0:d9|lin<|user_cursor|>e1
3206                    1:da|line2
3207                    <|fim_suffix|>
3208                    <|fim_middle|>updated
3209                    "},
3210                },
3211                Case {
3212                    name: "leading_newline_in_editable_region",
3213                    context: "\nabc\n",
3214                    editable_range: 0..5,
3215                    cursor_offset: 2, // byte 2 = 'a' in "abc" (after leading \n)
3216                    expected: indoc! {"
3217                    <|file_sep|>test.rs
3218                    <|fim_prefix|>
3219                    <|fim_middle|>current
3220                    0:00|
3221                    1:26|a<|user_cursor|>bc
3222                    <|fim_suffix|>
3223                    <|fim_middle|>updated
3224                    "},
3225                },
3226                Case {
3227                    name: "with_suffix",
3228                    context: "abc\ndef",
3229                    editable_range: 0..4, // editable region = "abc\n", suffix = "def"
3230                    cursor_offset: 2,
3231                    expected: indoc! {"
3232                    <|file_sep|>test.rs
3233                    <|fim_prefix|>
3234                    <|fim_middle|>current
3235                    0:26|ab<|user_cursor|>c
3236                    <|fim_suffix|>
3237                    def
3238                    <|fim_middle|>updated
3239                    "},
3240                },
3241                Case {
3242                    name: "unicode_two_byte_chars",
3243                    context: "héllo\n",
3244                    editable_range: 0..7,
3245                    cursor_offset: 3, // byte 3 = after "hé" (h=1 byte, é=2 bytes), before "llo"
3246                    expected: indoc! {"
3247                    <|file_sep|>test.rs
3248                    <|fim_prefix|>
3249                    <|fim_middle|>current
3250                    0:1b|hé<|user_cursor|>llo
3251                    <|fim_suffix|>
3252                    <|fim_middle|>updated
3253                    "},
3254                },
3255                Case {
3256                    name: "unicode_three_byte_chars",
3257                    context: "日本語\n",
3258                    editable_range: 0..10,
3259                    cursor_offset: 6, // byte 6 = after "日本" (3+3 bytes), before "語"
3260                    expected: indoc! {"
3261                    <|file_sep|>test.rs
3262                    <|fim_prefix|>
3263                    <|fim_middle|>current
3264                    0:80|日本<|user_cursor|>語
3265                    <|fim_suffix|>
3266                    <|fim_middle|>updated
3267                    "},
3268                },
3269                Case {
3270                    name: "unicode_four_byte_chars",
3271                    context: "a🌍b\n",
3272                    editable_range: 0..7,
3273                    cursor_offset: 5, // byte 5 = after "a🌍" (1+4 bytes), before "b"
3274                    expected: indoc! {"
3275                    <|file_sep|>test.rs
3276                    <|fim_prefix|>
3277                    <|fim_middle|>current
3278                    0:6b|a🌍<|user_cursor|>b
3279                    <|fim_suffix|>
3280                    <|fim_middle|>updated
3281                    "},
3282                },
3283                Case {
3284                    name: "cursor_at_start_of_region_not_placed",
3285                    context: "abc\n",
3286                    editable_range: 0..4,
3287                    cursor_offset: 0, // cursor_offset(0) > offset(0) is false → cursor not placed
3288                    expected: indoc! {"
3289                    <|file_sep|>test.rs
3290                    <|fim_prefix|>
3291                    <|fim_middle|>current
3292                    0:26|abc
3293                    <|fim_suffix|>
3294                    <|fim_middle|>updated
3295                    "},
3296                },
3297                Case {
3298                    name: "cursor_at_end_of_line_not_placed",
3299                    context: "abc\ndef\n",
3300                    editable_range: 0..8,
3301                    cursor_offset: 3, // byte 3 = the \n after "abc" → falls between lines, not placed
3302                    expected: indoc! {"
3303                    <|file_sep|>test.rs
3304                    <|fim_prefix|>
3305                    <|fim_middle|>current
3306                    0:26|abc
3307                    1:2f|def
3308                    <|fim_suffix|>
3309                    <|fim_middle|>updated
3310                    "},
3311                },
3312                Case {
3313                    name: "cursor_offset_relative_to_context_not_editable_region",
3314                    // cursor_offset is relative to `context`, so when editable_range.start > 0,
3315                    // write_cursor_excerpt_section must subtract it before comparing against
3316                    // per-line offsets within the editable region.
3317                    context: "pre\naaa\nbbb\nsuf\n",
3318                    editable_range: 4..12, // editable region = "aaa\nbbb\n"
3319                    cursor_offset: 9,      // byte 9 in context = second 'b' in "bbb"
3320                    expected: indoc! {"
3321                    <|file_sep|>test.rs
3322                    <|fim_prefix|>
3323                    pre
3324                    <|fim_middle|>current
3325                    0:23|aaa
3326                    1:26|b<|user_cursor|>bb
3327                    <|fim_suffix|>
3328                    suf
3329                    <|fim_middle|>updated
3330                    "},
3331                },
3332            ];
3333
3334            for case in &cases {
3335                let mut prompt = String::new();
3336                hashline::write_cursor_excerpt_section(
3337                    &mut prompt,
3338                    Path::new("test.rs"),
3339                    case.context,
3340                    &case.editable_range,
3341                    case.cursor_offset,
3342                );
3343                assert_eq!(prompt, case.expected, "failed case: {}", case.name);
3344            }
3345        }
3346
3347        #[test]
3348        fn test_apply_edit_commands() {
3349            struct Case {
3350                name: &'static str,
3351                original: &'static str,
3352                model_output: &'static str,
3353                expected: &'static str,
3354            }
3355
3356            let cases = vec![
3357                Case {
3358                    name: "set_single_line",
3359                    original: indoc! {"
3360                    let mut total = 0;
3361                    for product in products {
3362                        total += ;
3363                    }
3364                    total
3365                "},
3366                    model_output: indoc! {"
3367                    <|set|>2:87
3368                        total += product.price;
3369                "},
3370                    expected: indoc! {"
3371                    let mut total = 0;
3372                    for product in products {
3373                        total += product.price;
3374                    }
3375                    total
3376                "},
3377                },
3378                Case {
3379                    name: "set_range",
3380                    original: indoc! {"
3381                    fn foo() {
3382                        let x = 1;
3383                        let y = 2;
3384                        let z = 3;
3385                    }
3386                "},
3387                    model_output: indoc! {"
3388                    <|set|>1:46-3:4a
3389                        let sum = 6;
3390                "},
3391                    expected: indoc! {"
3392                    fn foo() {
3393                        let sum = 6;
3394                    }
3395                "},
3396                },
3397                Case {
3398                    name: "insert_after_line",
3399                    original: indoc! {"
3400                    fn main() {
3401                        let x = 1;
3402                    }
3403                "},
3404                    model_output: indoc! {"
3405                    <|insert|>1:46
3406                        let y = 2;
3407                "},
3408                    expected: indoc! {"
3409                    fn main() {
3410                        let x = 1;
3411                        let y = 2;
3412                    }
3413                "},
3414                },
3415                Case {
3416                    name: "insert_before_first",
3417                    original: indoc! {"
3418                    let x = 1;
3419                    let y = 2;
3420                "},
3421                    model_output: indoc! {"
3422                    <|insert|>
3423                    use std::io;
3424                "},
3425                    expected: indoc! {"
3426                    use std::io;
3427                    let x = 1;
3428                    let y = 2;
3429                "},
3430                },
3431                Case {
3432                    name: "set_with_cursor_marker",
3433                    original: indoc! {"
3434                    fn main() {
3435                        println!();
3436                    }
3437                "},
3438                    model_output: indoc! {"
3439                    <|set|>1:34
3440                        eprintln!(\"<|user_cursor|>\");
3441                "},
3442                    expected: indoc! {"
3443                    fn main() {
3444                        eprintln!(\"<|user_cursor|>\");
3445                    }
3446                "},
3447                },
3448                Case {
3449                    name: "multiple_set_commands",
3450                    original: indoc! {"
3451                    aaa
3452                    bbb
3453                    ccc
3454                    ddd
3455                "},
3456                    model_output: indoc! {"
3457                    <|set|>0:23
3458                    AAA
3459                    <|set|>2:29
3460                    CCC
3461                "},
3462                    expected: indoc! {"
3463                    AAA
3464                    bbb
3465                    CCC
3466                    ddd
3467                "},
3468                },
3469                Case {
3470                    name: "set_range_multiline_replacement",
3471                    original: indoc! {"
3472                    fn handle_submit() {
3473                    }
3474
3475                    fn handle_keystroke() {
3476                "},
3477                    model_output: indoc! {"
3478                    <|set|>0:3f-1:7d
3479                    fn handle_submit(modal_state: &mut ModalState) {
3480                        <|user_cursor|>
3481                    }
3482                "},
3483                    expected: indoc! {"
3484                    fn handle_submit(modal_state: &mut ModalState) {
3485                        <|user_cursor|>
3486                    }
3487
3488                    fn handle_keystroke() {
3489                "},
3490                },
3491                Case {
3492                    name: "no_edit_commands_returns_original",
3493                    original: indoc! {"
3494                    hello
3495                    world
3496                "},
3497                    model_output: "some random text with no commands",
3498                    expected: indoc! {"
3499                    hello
3500                    world
3501                "},
3502                },
3503                Case {
3504                    name: "no_edits_command_returns_original",
3505                    original: indoc! {"
3506                    hello
3507                    world
3508                "},
3509                    model_output: "<|no_edits|>",
3510                    expected: indoc! {"
3511                    hello
3512                    world
3513                "},
3514                },
3515                Case {
3516                    name: "wrong_hash_set_ignored",
3517                    original: indoc! {"
3518                    aaa
3519                    bbb
3520                "},
3521                    model_output: indoc! {"
3522                    <|set|>0:ff
3523                    ZZZ
3524                "},
3525                    expected: indoc! {"
3526                    aaa
3527                    bbb
3528                "},
3529                },
3530                Case {
3531                    name: "insert_and_set_combined",
3532                    original: indoc! {"
3533                    alpha
3534                    beta
3535                    gamma
3536                "},
3537                    model_output: indoc! {"
3538                    <|set|>0:06
3539                    ALPHA
3540                    <|insert|>1:9c
3541                    beta_extra
3542                "},
3543                    expected: indoc! {"
3544                    ALPHA
3545                    beta
3546                    beta_extra
3547                    gamma
3548                "},
3549                },
3550                Case {
3551                    name: "no_trailing_newline_preserved",
3552                    original: "hello\nworld",
3553                    model_output: indoc! {"
3554                    <|set|>0:14
3555                    HELLO
3556                "},
3557                    expected: "HELLO\nworld",
3558                },
3559                Case {
3560                    name: "set_range_hash_mismatch_in_end_bound",
3561                    original: indoc! {"
3562                    one
3563                    two
3564                    three
3565                "},
3566                    model_output: indoc! {"
3567                    <|set|>0:42-2:ff
3568                    ONE_TWO_THREE
3569                "},
3570                    expected: indoc! {"
3571                    one
3572                    two
3573                    three
3574                "},
3575                },
3576                Case {
3577                    name: "set_range_start_greater_than_end_ignored",
3578                    original: indoc! {"
3579                    a
3580                    b
3581                    c
3582                "},
3583                    model_output: indoc! {"
3584                    <|set|>2:63-1:62
3585                    X
3586                "},
3587                    expected: indoc! {"
3588                    a
3589                    b
3590                    c
3591                "},
3592                },
3593                Case {
3594                    name: "insert_out_of_bounds_ignored",
3595                    original: indoc! {"
3596                    x
3597                    y
3598                "},
3599                    model_output: indoc! {"
3600                    <|insert|>99:aa
3601                    z
3602                "},
3603                    expected: indoc! {"
3604                    x
3605                    y
3606                "},
3607                },
3608                Case {
3609                    name: "set_out_of_bounds_ignored",
3610                    original: indoc! {"
3611                    x
3612                    y
3613                "},
3614                    model_output: indoc! {"
3615                    <|set|>99:aa
3616                    z
3617                "},
3618                    expected: indoc! {"
3619                    x
3620                    y
3621                "},
3622                },
3623                Case {
3624                    name: "malformed_set_command_ignored",
3625                    original: indoc! {"
3626                    alpha
3627                    beta
3628                "},
3629                    model_output: indoc! {"
3630                    <|set|>not-a-line-ref
3631                    UPDATED
3632                "},
3633                    expected: indoc! {"
3634                    alpha
3635                    beta
3636                "},
3637                },
3638                Case {
3639                    name: "malformed_insert_hash_treated_as_before_first",
3640                    original: indoc! {"
3641                    alpha
3642                    beta
3643                "},
3644                    model_output: indoc! {"
3645                    <|insert|>1:nothex
3646                    preamble
3647                "},
3648                    expected: indoc! {"
3649                    preamble
3650                    alpha
3651                    beta
3652                "},
3653                },
3654                Case {
3655                    name: "set_then_insert_same_target_orders_insert_after_replacement",
3656                    original: indoc! {"
3657                    cat
3658                    dog
3659                "},
3660                    model_output: indoc! {"
3661                    <|set|>0:38
3662                    CAT
3663                    <|insert|>0:38
3664                    TAIL
3665                "},
3666                    expected: indoc! {"
3667                    CAT
3668                    TAIL
3669                    dog
3670                "},
3671                },
3672                Case {
3673                    name: "overlapping_set_ranges_last_wins",
3674                    original: indoc! {"
3675                    a
3676                    b
3677                    c
3678                    d
3679                "},
3680                    model_output: indoc! {"
3681                    <|set|>0:61-2:63
3682                    FIRST
3683                    <|set|>1:62-3:64
3684                    SECOND
3685                "},
3686                    expected: indoc! {"
3687                    FIRST
3688                    d
3689                "},
3690                },
3691                Case {
3692                    name: "insert_before_first_and_after_line",
3693                    original: indoc! {"
3694                        a
3695                        b
3696                    "},
3697                    model_output: indoc! {"
3698                        <|insert|>
3699                        HEAD
3700                        <|insert|>0:61
3701                        MID
3702                    "},
3703                    expected: indoc! {"
3704                        HEAD
3705                        a
3706                        MID
3707                        b
3708                    "},
3709                },
3710            ];
3711
3712            for case in &cases {
3713                let result = hashline::apply_edit_commands(case.original, &case.model_output);
3714                assert_eq!(result, case.expected, "failed case: {}", case.name);
3715            }
3716        }
3717
3718        #[test]
3719        fn test_output_has_edit_commands() {
3720            assert!(hashline::output_has_edit_commands(&format!(
3721                "{}0:ab\nnew",
3722                SET_COMMAND_MARKER
3723            )));
3724            assert!(hashline::output_has_edit_commands(&format!(
3725                "{}0:ab\nnew",
3726                INSERT_COMMAND_MARKER
3727            )));
3728            assert!(hashline::output_has_edit_commands(&format!(
3729                "some text\n{}1:cd\nstuff",
3730                SET_COMMAND_MARKER
3731            )));
3732            assert!(!hashline::output_has_edit_commands("just plain text"));
3733            assert!(!hashline::output_has_edit_commands("NO_EDITS"));
3734            assert!(hashline::output_has_edit_commands("<|no_edits|>"));
3735        }
3736
3737        // ---- hashline::patch_to_edit_commands round-trip tests ----
3738
3739        #[test]
3740        fn test_patch_to_edit_commands() {
3741            struct Case {
3742                name: &'static str,
3743                old: &'static str,
3744                patch: &'static str,
3745                expected_new: &'static str,
3746            }
3747
3748            let cases = [
3749                Case {
3750                    name: "single_line_replacement",
3751                    old: indoc! {"
3752                    let mut total = 0;
3753                    for product in products {
3754                        total += ;
3755                    }
3756                    total
3757                "},
3758                    patch: indoc! {"
3759                    @@ -1,5 +1,5 @@
3760                     let mut total = 0;
3761                     for product in products {
3762                    -    total += ;
3763                    +    total += product.price;
3764                     }
3765                     total
3766                "},
3767                    expected_new: indoc! {"
3768                    let mut total = 0;
3769                    for product in products {
3770                        total += product.price;
3771                    }
3772                    total
3773                "},
3774                },
3775                Case {
3776                    name: "multiline_replacement",
3777                    old: indoc! {"
3778                    fn foo() {
3779                        let x = 1;
3780                        let y = 2;
3781                        let z = 3;
3782                    }
3783                "},
3784                    patch: indoc! {"
3785                    @@ -1,5 +1,3 @@
3786                     fn foo() {
3787                    -    let x = 1;
3788                    -    let y = 2;
3789                    -    let z = 3;
3790                    +    let sum = 1 + 2 + 3;
3791                     }
3792                "},
3793                    expected_new: indoc! {"
3794                    fn foo() {
3795                        let sum = 1 + 2 + 3;
3796                    }
3797                "},
3798                },
3799                Case {
3800                    name: "insertion",
3801                    old: indoc! {"
3802                    fn main() {
3803                        let x = 1;
3804                    }
3805                "},
3806                    patch: indoc! {"
3807                    @@ -1,3 +1,4 @@
3808                     fn main() {
3809                         let x = 1;
3810                    +    let y = 2;
3811                     }
3812                "},
3813                    expected_new: indoc! {"
3814                    fn main() {
3815                        let x = 1;
3816                        let y = 2;
3817                    }
3818                "},
3819                },
3820                Case {
3821                    name: "insertion_before_first",
3822                    old: indoc! {"
3823                    let x = 1;
3824                    let y = 2;
3825                "},
3826                    patch: indoc! {"
3827                    @@ -1,2 +1,3 @@
3828                    +use std::io;
3829                     let x = 1;
3830                     let y = 2;
3831                "},
3832                    expected_new: indoc! {"
3833                    use std::io;
3834                    let x = 1;
3835                    let y = 2;
3836                "},
3837                },
3838                Case {
3839                    name: "deletion",
3840                    old: indoc! {"
3841                    aaa
3842                    bbb
3843                    ccc
3844                    ddd
3845                "},
3846                    patch: indoc! {"
3847                    @@ -1,4 +1,2 @@
3848                     aaa
3849                    -bbb
3850                    -ccc
3851                     ddd
3852                "},
3853                    expected_new: indoc! {"
3854                    aaa
3855                    ddd
3856                "},
3857                },
3858                Case {
3859                    name: "multiple_changes",
3860                    old: indoc! {"
3861                    alpha
3862                    beta
3863                    gamma
3864                    delta
3865                    epsilon
3866                "},
3867                    patch: indoc! {"
3868                    @@ -1,5 +1,5 @@
3869                    -alpha
3870                    +ALPHA
3871                     beta
3872                     gamma
3873                    -delta
3874                    +DELTA
3875                     epsilon
3876                "},
3877                    expected_new: indoc! {"
3878                    ALPHA
3879                    beta
3880                    gamma
3881                    DELTA
3882                    epsilon
3883                "},
3884                },
3885                Case {
3886                    name: "replace_with_insertion",
3887                    old: indoc! {r#"
3888                    fn handle() {
3889                        modal_state.close();
3890                        modal_state.dismiss();
3891                "#},
3892                    patch: indoc! {r#"
3893                    @@ -1,3 +1,4 @@
3894                     fn handle() {
3895                         modal_state.close();
3896                    +    eprintln!("");
3897                         modal_state.dismiss();
3898                "#},
3899                    expected_new: indoc! {r#"
3900                    fn handle() {
3901                        modal_state.close();
3902                        eprintln!("");
3903                        modal_state.dismiss();
3904                "#},
3905                },
3906                Case {
3907                    name: "complete_replacement",
3908                    old: indoc! {"
3909                    aaa
3910                    bbb
3911                    ccc
3912                "},
3913                    patch: indoc! {"
3914                    @@ -1,3 +1,3 @@
3915                    -aaa
3916                    -bbb
3917                    -ccc
3918                    +xxx
3919                    +yyy
3920                    +zzz
3921                "},
3922                    expected_new: indoc! {"
3923                    xxx
3924                    yyy
3925                    zzz
3926                "},
3927                },
3928                Case {
3929                    name: "add_function_body",
3930                    old: indoc! {"
3931                    fn foo() {
3932                        modal_state.dismiss();
3933                    }
3934
3935                    fn
3936
3937                    fn handle_keystroke() {
3938                "},
3939                    patch: indoc! {"
3940                    @@ -1,6 +1,8 @@
3941                     fn foo() {
3942                         modal_state.dismiss();
3943                     }
3944
3945                    -fn
3946                    +fn handle_submit() {
3947                    +    todo()
3948                    +}
3949
3950                     fn handle_keystroke() {
3951                "},
3952                    expected_new: indoc! {"
3953                    fn foo() {
3954                        modal_state.dismiss();
3955                    }
3956
3957                    fn handle_submit() {
3958                        todo()
3959                    }
3960
3961                    fn handle_keystroke() {
3962                "},
3963                },
3964                Case {
3965                    name: "with_cursor_offset",
3966                    old: indoc! {r#"
3967                    fn main() {
3968                        println!();
3969                    }
3970                "#},
3971                    patch: indoc! {r#"
3972                        @@ -1,3 +1,3 @@
3973                        fn main() {
3974                        -    println!();
3975                        +    eprintln!("");
3976                        }
3977                    "#},
3978                    expected_new: indoc! {r#"
3979                        fn main() {
3980                            eprintln!("<|user_cursor|>");
3981                        }
3982                    "#},
3983                },
3984                Case {
3985                    name: "non_local_hunk_header_pure_insertion_repro",
3986                    old: indoc! {"
3987                        aaa
3988                        bbb
3989                    "},
3990                    patch: indoc! {"
3991                        @@ -20,2 +20,3 @@
3992                        aaa
3993                        +xxx
3994                        bbb
3995                    "},
3996                    expected_new: indoc! {"
3997                        aaa
3998                        xxx
3999                        bbb
4000                    "},
4001                },
4002                Case {
4003                    name: "empty_patch_produces_no_edits_marker",
4004                    old: indoc! {"
4005                        aaa
4006                        bbb
4007                    "},
4008                    patch: "@@ -20,2 +20,3 @@\n",
4009                    expected_new: indoc! {"
4010                        aaa
4011                        bbb
4012                    "},
4013                },
4014            ];
4015
4016            for case in &cases {
4017                // The cursor_offset for patch_to_edit_commands is relative to
4018                // the first hunk's new text (context + additions). We compute
4019                // it by finding where the marker sits in the expected output
4020                // (which mirrors the new text of the hunk).
4021                let cursor_offset = case.expected_new.find(CURSOR_MARKER);
4022
4023                let commands =
4024                    hashline::patch_to_edit_commands(case.old, case.patch, cursor_offset)
4025                        .unwrap_or_else(|e| panic!("failed case {}: {e}", case.name));
4026
4027                assert!(
4028                    hashline::output_has_edit_commands(&commands),
4029                    "case {}: expected edit commands, got: {commands:?}",
4030                    case.name,
4031                );
4032
4033                let applied = hashline::apply_edit_commands(case.old, &commands);
4034                assert_eq!(applied, case.expected_new, "case {}", case.name);
4035            }
4036        }
4037    }
4038}
4039
4040pub mod qwen {
4041    use super::*;
4042
4043    pub const FIM_PREFIX: &str = "<|fim_prefix|>";
4044    pub const FIM_SUFFIX: &str = "<|fim_suffix|>";
4045    pub const FIM_MIDDLE: &str = "<|fim_middle|>";
4046    pub const FILE_MARKER: &str = "<|file_sep|>";
4047    pub const END_MARKER: &str = "<|im_end|>";
4048
4049    pub fn assemble_fim_prompt(
4050        context: &str,
4051        editable_range: &Range<usize>,
4052        cursor_prefix_section: &str,
4053        events: &[Arc<Event>],
4054        related_files: &[RelatedFile],
4055        max_tokens: usize,
4056    ) -> String {
4057        let suffix_section = build_suffix_section(context, editable_range);
4058
4059        let cursor_prefix_tokens = estimate_tokens(cursor_prefix_section.len() + FIM_PREFIX.len());
4060        let suffix_tokens =
4061            estimate_tokens(suffix_section.len() + FIM_SUFFIX.len() + FIM_MIDDLE.len());
4062        let budget_after_cursor = max_tokens.saturating_sub(cursor_prefix_tokens + suffix_tokens);
4063
4064        let edit_history_section = super::format_edit_history_within_budget(
4065            events,
4066            FILE_MARKER,
4067            "edit_history",
4068            budget_after_cursor,
4069            max_edit_event_count_for_format(&ZetaFormat::V0608QwenMultiRegions),
4070        );
4071        let edit_history_tokens = estimate_tokens(edit_history_section.len() + "\n".len());
4072        let budget_after_edit_history = budget_after_cursor.saturating_sub(edit_history_tokens);
4073
4074        let related_files_section = super::format_related_files_within_budget(
4075            related_files,
4076            FILE_MARKER,
4077            "",
4078            budget_after_edit_history,
4079        );
4080
4081        let mut prompt = String::new();
4082        prompt.push_str(FIM_PREFIX);
4083        prompt.push_str(&related_files_section);
4084        if !related_files_section.is_empty() {
4085            prompt.push('\n');
4086        }
4087        prompt.push_str(&edit_history_section);
4088        if !edit_history_section.is_empty() {
4089            prompt.push('\n');
4090        }
4091        prompt.push_str(cursor_prefix_section);
4092        prompt.push_str(&suffix_section);
4093        prompt.push_str(FIM_MIDDLE);
4094        prompt
4095    }
4096
4097    fn build_suffix_section(context: &str, editable_range: &Range<usize>) -> String {
4098        let mut section = String::new();
4099        section.push_str(FIM_SUFFIX);
4100        section.push_str(&context[editable_range.end..]);
4101        if !section.ends_with('\n') {
4102            section.push('\n');
4103        }
4104        section
4105    }
4106}
4107
4108pub mod seed_coder {
4109    //! Seed-Coder prompt format using SPM (Suffix-Prefix-Middle) FIM mode.
4110    //!
4111    //! Seed-Coder uses different FIM tokens and order than Qwen:
4112    //! - SPM order: suffix comes FIRST, then prefix, then middle
4113    //! - Tokens: `<[fim-suffix]>`, `<[fim-prefix]>`, `<[fim-middle]>`
4114    //! - File markers: StarCoder-style `<filename>path` (single token + path)
4115    //!
4116    //! All context (related files, edit history) goes in the PREFIX section.
4117    //! The suffix contains only code after the editable region.
4118    //!
4119    //! Example prompt:
4120    //!
4121    //! <[fim-suffix]>
4122    //! code after editable region
4123    //! <[fim-prefix]><filename>related/file.py
4124    //! related file content
4125    //!
4126    //! <filename>edit_history
4127    //! --- a/some_file.py
4128    //! +++ b/some_file.py
4129    //! -old
4130    //! +new
4131    //!
4132    //! <filename>path/to/target_file.py
4133    //! code before editable region
4134    //! <<<<<<< CURRENT
4135    //! code that
4136    //! needs to<|user_cursor|>
4137    //! be rewritten
4138    //! =======
4139    //! <[fim-middle]>
4140    //!
4141    //! Expected output (model generates):
4142    //!
4143    //! updated
4144    //! code with
4145    //! changes applied
4146    //! >>>>>>> UPDATED
4147
4148    use super::*;
4149
4150    pub const FIM_SUFFIX: &str = "<[fim-suffix]>";
4151    pub const FIM_PREFIX: &str = "<[fim-prefix]>";
4152    pub const FIM_MIDDLE: &str = "<[fim-middle]>";
4153    pub const FILE_MARKER: &str = "<filename>";
4154
4155    pub const START_MARKER: &str = "<<<<<<< CURRENT\n";
4156    pub const SEPARATOR: &str = "=======\n";
4157    pub const END_MARKER: &str = ">>>>>>> UPDATED\n";
4158
4159    pub const NO_EDITS: &str = "NO_EDITS\n";
4160
4161    pub fn special_tokens() -> &'static [&'static str] {
4162        &[
4163            FIM_SUFFIX,
4164            FIM_PREFIX,
4165            FIM_MIDDLE,
4166            FILE_MARKER,
4167            START_MARKER,
4168            SEPARATOR,
4169            END_MARKER,
4170            CURSOR_MARKER,
4171        ]
4172    }
4173
4174    pub fn write_cursor_excerpt_section(
4175        prompt: &mut String,
4176        path: &Path,
4177        context: &str,
4178        editable_range: &Range<usize>,
4179        cursor_offset: usize,
4180    ) {
4181        let section = build_cursor_prefix_section(path, context, editable_range, cursor_offset);
4182        prompt.push_str(&section);
4183    }
4184
4185    pub fn format_prompt_with_budget(
4186        path: &Path,
4187        context: &str,
4188        editable_range: &Range<usize>,
4189        cursor_offset: usize,
4190        events: &[Arc<Event>],
4191        related_files: &[RelatedFile],
4192        diagnostics: &[ActiveBufferDiagnostic],
4193        max_tokens: usize,
4194    ) -> String {
4195        let cursor_prefix_section =
4196            build_cursor_prefix_section(path, context, editable_range, cursor_offset);
4197        assemble_fim_prompt(
4198            context,
4199            editable_range,
4200            &cursor_prefix_section,
4201            events,
4202            related_files,
4203            diagnostics,
4204            None,
4205            max_tokens,
4206        )
4207    }
4208
4209    pub fn assemble_fim_prompt(
4210        context: &str,
4211        editable_range: &Range<usize>,
4212        cursor_prefix_section: &str,
4213        events: &[Arc<Event>],
4214        related_files: &[RelatedFile],
4215        diagnostics: &[ActiveBufferDiagnostic],
4216        cursor_buffer_row: Option<u32>,
4217        max_tokens: usize,
4218    ) -> String {
4219        let suffix_section = build_suffix_section(context, editable_range);
4220
4221        let suffix_tokens = estimate_tokens(suffix_section.len() + FIM_PREFIX.len());
4222        let cursor_prefix_tokens = estimate_tokens(cursor_prefix_section.len() + FIM_MIDDLE.len());
4223        let budget_after_cursor = max_tokens.saturating_sub(suffix_tokens + cursor_prefix_tokens);
4224
4225        let edit_history_section = super::format_edit_history_within_budget(
4226            events,
4227            FILE_MARKER,
4228            "edit_history",
4229            budget_after_cursor,
4230            max_edit_event_count_for_format(&ZetaFormat::V0211SeedCoder),
4231        );
4232        let edit_history_tokens = estimate_tokens(edit_history_section.len() + "\n".len());
4233        let budget_after_edit_history = budget_after_cursor.saturating_sub(edit_history_tokens);
4234
4235        let diagnostics_section = super::format_active_buffer_diagnostics_with_budget(
4236            diagnostics,
4237            cursor_buffer_row,
4238            budget_after_edit_history,
4239        );
4240        let diagnostics_tokens = estimate_tokens(diagnostics_section.len() + "\n".len());
4241        let budget_after_diagnostics = budget_after_edit_history.saturating_sub(diagnostics_tokens);
4242
4243        let related_files_section = super::format_related_files_within_budget(
4244            related_files,
4245            FILE_MARKER,
4246            "",
4247            budget_after_diagnostics,
4248        );
4249
4250        let mut prompt = String::new();
4251        prompt.push_str(&suffix_section);
4252        prompt.push_str(FIM_PREFIX);
4253        prompt.push_str(&diagnostics_section);
4254        if !diagnostics_section.is_empty() {
4255            prompt.push('\n');
4256        }
4257        prompt.push_str(&related_files_section);
4258        if !related_files_section.is_empty() {
4259            prompt.push('\n');
4260        }
4261        prompt.push_str(&edit_history_section);
4262        if !edit_history_section.is_empty() {
4263            prompt.push('\n');
4264        }
4265        prompt.push_str(cursor_prefix_section);
4266        prompt.push_str(FIM_MIDDLE);
4267
4268        prompt
4269    }
4270
4271    pub(crate) fn build_suffix_section(context: &str, editable_range: &Range<usize>) -> String {
4272        let mut section = String::new();
4273        section.push_str(FIM_SUFFIX);
4274        section.push_str(&context[editable_range.end..]);
4275        if !section.ends_with('\n') {
4276            section.push('\n');
4277        }
4278        section
4279    }
4280
4281    fn build_cursor_prefix_section(
4282        path: &Path,
4283        context: &str,
4284        editable_range: &Range<usize>,
4285        cursor_offset: usize,
4286    ) -> String {
4287        let mut section = String::new();
4288        let path_str = path.to_string_lossy();
4289        write!(section, "{}{}\n", FILE_MARKER, path_str).ok();
4290
4291        section.push_str(&context[..editable_range.start]);
4292        section.push_str(START_MARKER);
4293        section.push_str(&context[editable_range.start..cursor_offset]);
4294        section.push_str(CURSOR_MARKER);
4295        section.push_str(&context[cursor_offset..editable_range.end]);
4296        if !section.ends_with('\n') {
4297            section.push('\n');
4298        }
4299        section.push_str(SEPARATOR);
4300        section
4301    }
4302
4303    /// Format patch as containing no changes if it's empty; otherwise return None.
4304    pub(crate) fn no_edits(patch: &str) -> Option<String> {
4305        // Count lines in the patch
4306        let empty_patch = patch.lines().count() <= 3;
4307        if empty_patch {
4308            Some(format!("{NO_EDITS}{END_MARKER}"))
4309        } else {
4310            None
4311        }
4312    }
4313}
4314
4315pub mod v0304_variable_edit {
4316    //! A prompt format with no fixed editable region. The entire context is shown
4317    //! to the model, and it chooses which text to replace by outputting surrounding
4318    //! context lines with `<|fim_middle|>` and `<|fim_suffix|>` delimiting the new
4319    //! text.
4320    //!
4321    //! Example prompt:
4322    //!
4323    //! <|file_sep|>path/to/file.py
4324    //! zero
4325    //! one
4326    //! two
4327    //! three<|user_cursor|>
4328    //! four
4329    //! five
4330    //! <|fim_prefix|>
4331    //
4332    //! Expected output (model generates):
4333    //!
4334    //! two
4335    //! <|fim_middle|>
4336    //! THREE
4337    //! <|fim_suffix|>
4338    //! four
4339    //!
4340    //! The output means: find "two\n...\nfour" in the context, and replace
4341    //! everything between "two\n" and "four" with "THREE\n".
4342
4343    use super::*;
4344
4345    pub fn special_tokens() -> &'static [&'static str] {
4346        &[
4347            "<|fim_prefix|>",
4348            "<|fim_suffix|>",
4349            "<|fim_middle|>",
4350            "<|file_sep|>",
4351            CURSOR_MARKER,
4352        ]
4353    }
4354
4355    pub fn write_cursor_excerpt_section(
4356        prompt: &mut String,
4357        path: &Path,
4358        context: &str,
4359        cursor_offset: usize,
4360    ) {
4361        let path_str = path.to_string_lossy();
4362        write!(prompt, "<|file_sep|>{}\n", path_str).ok();
4363
4364        prompt.push_str(&context[..cursor_offset]);
4365        prompt.push_str(CURSOR_MARKER);
4366        prompt.push_str(&context[cursor_offset..]);
4367        if !prompt.ends_with('\n') {
4368            prompt.push('\n');
4369        }
4370        prompt.push_str("<|fim_prefix|>\n")
4371    }
4372
4373    /// Apply a variable-edit model output to the original context text.
4374    ///
4375    /// The model output has the form:
4376    ///
4377    /// - prefix context lines
4378    /// - `<|fim_middle|>`
4379    /// - new text
4380    /// - `<|fim_suffix|>`
4381    /// - suffix context lines
4382    ///
4383    /// We locate the prefix/suffix context lines in the original text and replace
4384    /// everything between them with the new text.
4385    pub fn apply_variable_edit(
4386        context: &str,
4387        model_output: &str,
4388    ) -> Result<(Range<usize>, String)> {
4389        let (prefix_context, rest) = model_output
4390            .split_once("<|fim_middle|>\n")
4391            .or_else(|| model_output.split_once("<|fim_middle|>"))
4392            .ok_or_else(|| anyhow::anyhow!("missing <|fim_middle|> in model output"))?;
4393
4394        let (new_text, suffix_context) = rest
4395            .split_once("<|fim_suffix|>\n")
4396            .or_else(|| rest.split_once("<|fim_suffix|>"))
4397            .unwrap_or((rest, ""));
4398
4399        let suffix_context = if prefix_context.is_empty() && !suffix_context.is_empty() {
4400            suffix_context.strip_prefix('\n').unwrap_or(suffix_context)
4401        } else {
4402            suffix_context
4403        };
4404
4405        let prefix_offset = find_substring_at_line_boundary(context, prefix_context)
4406            .ok_or_else(|| anyhow!("could not locate prefix lines"))?
4407            + prefix_context.len();
4408        let suffix_offset = if suffix_context.is_empty() {
4409            context.len()
4410        } else {
4411            find_substring_at_line_boundary(&context[prefix_offset..], suffix_context)
4412                .ok_or_else(|| anyhow!("could not locate suffix lines"))?
4413                + prefix_offset
4414        };
4415
4416        let edit_range = prefix_offset..suffix_offset;
4417        return Ok((edit_range, new_text.to_string()));
4418    }
4419
4420    fn find_substring_at_line_boundary(haystack: &str, needle: &str) -> Option<usize> {
4421        if needle.is_empty() {
4422            return Some(0);
4423        }
4424
4425        haystack.match_indices(needle).find_map(|(offset, _)| {
4426            let matched_line_start = offset == 0 || haystack[..offset].ends_with('\n');
4427            matched_line_start.then_some(offset)
4428        })
4429    }
4430
4431    /// Convert a unified diff patch into the variable-edit output format.
4432    ///
4433    /// Parses `patch` as a unified diff against `old_text` and produces model
4434    /// output with context lines surrounding `<|fim_middle|>` / `<|fim_suffix|>`
4435    /// delimiters. The diff is resolved by content matching rather than line
4436    /// numbers.
4437    pub fn patch_to_variable_edit_output(
4438        old_text: &str,
4439        patch: &str,
4440        cursor_offset: Option<usize>,
4441    ) -> Result<String> {
4442        // Parse the unified diff into hunks. Each hunk has an `old_context`
4443        // string (context + deleted lines interleaved in order) and a list of
4444        // edits expressed as byte ranges within that context plus replacement
4445        // text.
4446        let hunks = parse_hunks(patch);
4447        if hunks.is_empty() {
4448            return Ok(String::new());
4449        }
4450
4451        // Apply each hunk by finding its old_context in the text and
4452        // performing the edits. We search forward from where the previous
4453        // hunk ended so that hunks are applied in order.
4454        let mut new_text = old_text.to_string();
4455        let mut search_from: usize = 0;
4456        let mut first_hunk_pos: Option<usize> = None;
4457
4458        for hunk in &hunks {
4459            let context_pos = new_text[search_from..]
4460                .find(&hunk.old_context)
4461                .map(|pos| pos + search_from)
4462                .ok_or_else(|| anyhow::anyhow!("could not locate hunk context in text"))?;
4463
4464            if first_hunk_pos.is_none() {
4465                first_hunk_pos = Some(context_pos);
4466            }
4467
4468            // Apply edits in reverse order so byte offsets remain valid.
4469            for edit in hunk.edits.iter().rev() {
4470                let abs_start = context_pos + edit.range.start;
4471                let abs_end = context_pos + edit.range.end;
4472                new_text.replace_range(abs_start..abs_end, &edit.text);
4473            }
4474
4475            // Advance past this hunk's region in the (now modified) text.
4476            let new_region_len: usize =
4477                hunk.edits.iter().fold(hunk.old_context.len(), |len, edit| {
4478                    len + edit.text.len() - (edit.range.end - edit.range.start)
4479                });
4480            search_from = context_pos + new_region_len;
4481        }
4482
4483        // Now we have old_text and new_text. Find the changed line range by
4484        // comparing them.
4485        let old_lines: Vec<&str> = old_text.lines().collect();
4486        let new_lines: Vec<&str> = new_text.lines().collect();
4487
4488        // Find first differing line.
4489        let first_changed_row = old_lines
4490            .iter()
4491            .zip(new_lines.iter())
4492            .position(|(a, b)| a != b)
4493            .unwrap_or_else(|| old_lines.len().min(new_lines.len()));
4494
4495        // Find last differing line (from the end).
4496        let max_suffix = old_lines.len().min(new_lines.len()) - first_changed_row;
4497        let common_suffix = old_lines
4498            .iter()
4499            .rev()
4500            .zip(new_lines.iter().rev())
4501            .take(max_suffix)
4502            .take_while(|(a, b)| a == b)
4503            .count();
4504
4505        let old_end = old_lines.len() - common_suffix;
4506        let new_end = new_lines.len() - common_suffix;
4507
4508        if first_changed_row == old_end && first_changed_row == new_end {
4509            return Ok(String::new());
4510        }
4511
4512        // Build the replacement text from new_lines[first_diff..new_end].
4513        let mut merged_new_text = String::new();
4514        for line in &new_lines[first_changed_row..new_end] {
4515            merged_new_text.push_str(line);
4516            merged_new_text.push('\n');
4517        }
4518
4519        // cursor_offset is relative to the first hunk's new content in
4520        // new_text. Translate it to an offset within merged_new_text, which
4521        // only contains lines first_diff..new_end of new_text.
4522        if let Some(hunk_offset) = cursor_offset {
4523            let hunk_start = first_hunk_pos.unwrap_or(0);
4524            let absolute_pos = hunk_start + hunk_offset;
4525
4526            // Byte offset where first_diff starts in new_text.
4527            let merged_start: usize = new_lines[..first_changed_row]
4528                .iter()
4529                .map(|line| line.len() + 1)
4530                .sum();
4531
4532            if absolute_pos >= merged_start {
4533                let relative_offset = absolute_pos - merged_start;
4534                if relative_offset <= merged_new_text.len() {
4535                    merged_new_text.insert_str(relative_offset, CURSOR_MARKER);
4536                }
4537            }
4538        }
4539
4540        // Build output with 2 lines of context above and below.
4541        let context_lines_count = 2;
4542        let mut prefix_start = first_changed_row.saturating_sub(context_lines_count);
4543        let mut suffix_end = (old_end + context_lines_count).min(old_lines.len());
4544
4545        fn count_matches(line_range: Range<usize>, lines: &[&str]) -> usize {
4546            let pattern = &lines[line_range];
4547            let pattern_len = pattern.len();
4548
4549            let mut count = 0;
4550            for offset in 0..=lines.len() - pattern_len {
4551                if &lines[offset..offset + pattern_len] == pattern {
4552                    count += 1;
4553                }
4554            }
4555            count
4556        }
4557
4558        // Expand prefix and suffix until they are unique
4559        while prefix_start > 0 {
4560            if count_matches(prefix_start..first_changed_row, &old_lines) > 1 {
4561                prefix_start -= 1;
4562            } else {
4563                break;
4564            }
4565        }
4566        while suffix_end < old_lines.len() {
4567            if count_matches(old_end..suffix_end, &old_lines) > 1 {
4568                suffix_end += 1;
4569            } else {
4570                break;
4571            }
4572        }
4573
4574        let mut output = String::new();
4575        for line in &old_lines[prefix_start..first_changed_row] {
4576            output.push_str(line);
4577            output.push('\n');
4578        }
4579        output.push_str("<|fim_middle|>\n");
4580        output.push_str(&merged_new_text);
4581        output.push_str("<|fim_suffix|>\n");
4582        for line in &old_lines[old_end..suffix_end] {
4583            output.push_str(line);
4584            output.push('\n');
4585        }
4586
4587        Ok(output)
4588    }
4589
4590    struct ParsedHunk {
4591        old_context: String,
4592        edits: Vec<ParsedEdit>,
4593    }
4594
4595    struct ParsedEdit {
4596        range: Range<usize>,
4597        text: String,
4598    }
4599
4600    /// Parse a unified diff into content-based hunks. Each hunk contains an
4601    /// `old_context` string (context lines + deleted lines, which together
4602    /// form the text that should be found in the original) and a list of edits
4603    /// expressed as byte ranges within that context.
4604    fn parse_hunks(patch: &str) -> Vec<ParsedHunk> {
4605        let mut hunks = Vec::new();
4606        let mut current: Option<ParsedHunk> = None;
4607
4608        for line in patch.lines() {
4609            if line.starts_with("@@") {
4610                if let Some(hunk) = current.take() {
4611                    if !hunk.old_context.is_empty() || !hunk.edits.is_empty() {
4612                        hunks.push(hunk);
4613                    }
4614                }
4615                current = Some(ParsedHunk {
4616                    old_context: String::new(),
4617                    edits: Vec::new(),
4618                });
4619            } else if line.starts_with("---") || line.starts_with("+++") {
4620                continue;
4621            } else if let Some(hunk) = &mut current {
4622                if let Some(added) = line.strip_prefix('+') {
4623                    let pos = hunk.old_context.len();
4624                    if let Some(last_edit) = hunk.edits.last_mut() {
4625                        if last_edit.range.end == pos {
4626                            writeln!(&mut last_edit.text, "{added}").ok();
4627                            continue;
4628                        }
4629                    }
4630                    hunk.edits.push(ParsedEdit {
4631                        range: pos..pos,
4632                        text: format!("{added}\n"),
4633                    });
4634                } else if let Some(removed) = line.strip_prefix('-') {
4635                    let start = hunk.old_context.len();
4636                    writeln!(&mut hunk.old_context, "{removed}").ok();
4637                    let end = hunk.old_context.len();
4638                    if let Some(last_edit) = hunk.edits.last_mut() {
4639                        if last_edit.range.end == start {
4640                            last_edit.range.end = end;
4641                            continue;
4642                        }
4643                    }
4644                    hunk.edits.push(ParsedEdit {
4645                        range: start..end,
4646                        text: String::new(),
4647                    });
4648                } else {
4649                    let ctx = line.strip_prefix(' ').unwrap_or(line);
4650                    writeln!(&mut hunk.old_context, "{ctx}").ok();
4651                }
4652            }
4653        }
4654
4655        if let Some(hunk) = current {
4656            if !hunk.old_context.is_empty() || !hunk.edits.is_empty() {
4657                hunks.push(hunk);
4658            }
4659        }
4660
4661        hunks
4662    }
4663
4664    #[cfg(test)]
4665    mod tests {
4666        use super::*;
4667        use indoc::indoc;
4668
4669        #[test]
4670        fn test_apply_variable_edit() {
4671            struct Case {
4672                name: &'static str,
4673                original: &'static str,
4674                model_output: &'static str,
4675                expected: &'static str,
4676            }
4677
4678            let cases = [
4679                Case {
4680                    name: "simple_single_line_replacement",
4681                    original: indoc! {"
4682                        zero
4683                        one
4684                        two
4685                        three
4686                        four
4687                        five
4688                    "},
4689                    model_output: indoc! {"
4690                        two
4691                        <|fim_middle|>
4692                        THREE
4693                        <|fim_suffix|>
4694                        four
4695                    "},
4696                    expected: indoc! {"
4697                        zero
4698                        one
4699                        two
4700                        THREE
4701                        four
4702                        five
4703                    "},
4704                },
4705                Case {
4706                    name: "multi_line_replacement",
4707                    original: indoc! {"
4708                        a
4709                        b
4710                        c
4711                        d
4712                        e
4713                    "},
4714                    model_output: indoc! {"
4715                        a
4716                        <|fim_middle|>
4717                        B
4718                        C
4719                        D
4720                        <|fim_suffix|>
4721                        e
4722                    "},
4723                    expected: indoc! {"
4724                        a
4725                        B
4726                        C
4727                        D
4728                        e
4729                    "},
4730                },
4731                Case {
4732                    name: "insertion_between_existing_lines",
4733                    original: indoc! {"
4734                        a
4735                        b
4736                        c
4737                    "},
4738                    model_output: indoc! {"
4739                        a
4740                        <|fim_middle|>
4741                        X
4742                        <|fim_suffix|>
4743                        b
4744                    "},
4745                    expected: indoc! {"
4746                        a
4747                        X
4748                        b
4749                        c
4750                    "},
4751                },
4752                Case {
4753                    name: "deletion",
4754                    original: indoc! {"
4755                        a
4756                        b
4757                        c
4758                        d
4759                    "},
4760                    model_output: indoc! {"
4761                        a
4762                        <|fim_middle|>
4763                        <|fim_suffix|>
4764                        c
4765                    "},
4766                    expected: indoc! {"
4767                        a
4768                        c
4769                        d
4770                    "},
4771                },
4772                Case {
4773                    name: "replacement_at_start_no_prefix_context",
4774                    original: indoc! {"
4775                        a
4776                        b
4777                        c
4778                    "},
4779                    model_output: indoc! {"
4780                        <|fim_middle|>
4781                        X
4782                        <|fim_suffix|>
4783                        b
4784                    "},
4785                    expected: indoc! {"
4786                        X
4787                        b
4788                        c
4789                    "},
4790                },
4791                Case {
4792                    name: "replacement_at_end_no_suffix_context",
4793                    original: indoc! {"
4794                        a
4795                        b
4796                        c
4797                    "},
4798                    model_output: indoc! {"
4799                        b
4800                        <|fim_middle|>
4801                        Z
4802                        <|fim_suffix|>
4803                    "},
4804                    expected: indoc! {"
4805                        a
4806                        b
4807                        Z
4808                    "},
4809                },
4810                Case {
4811                    name: "context_with_trailing_newline_is_preserved",
4812                    original: indoc! {"
4813                        a
4814                        b
4815                        c
4816                    "},
4817                    model_output: indoc! {"
4818                        a
4819                        <|fim_middle|>
4820                        B
4821                        <|fim_suffix|>
4822                        c
4823                    "},
4824                    expected: indoc! {"
4825                        a
4826                        B
4827                        c
4828                    "},
4829                },
4830                Case {
4831                    name: "cursor_marker_passes_through_untouched",
4832                    original: indoc! {"
4833                        a
4834                        b
4835                        c
4836                    "},
4837                    model_output: indoc! {"
4838                        a
4839                        <|fim_middle|>
4840                        B<|user_cursor|>B
4841                        <|fim_suffix|>
4842                        c
4843                    "},
4844                    expected: indoc! {"
4845                        a
4846                        B<|user_cursor|>B
4847                        c
4848                    "},
4849                },
4850                Case {
4851                    name: "multiple_prefix_context_lines",
4852                    original: indoc! {"
4853                        a
4854                        b
4855                        c
4856                        d
4857                        e
4858                    "},
4859                    model_output: indoc! {"
4860                        b
4861                        c
4862                        <|fim_middle|>
4863                        D
4864                        <|fim_suffix|>
4865                        e
4866                    "},
4867                    expected: indoc! {"
4868                        a
4869                        b
4870                        c
4871                        D
4872                        e
4873                    "},
4874                },
4875            ];
4876
4877            for case in cases {
4878                let (edit_range, replacement) =
4879                    apply_variable_edit(case.original, case.model_output).unwrap();
4880                let mut edited = case.original.to_string();
4881                edited.replace_range(edit_range, &replacement);
4882                assert_eq!(edited, case.expected, "{}", case.name);
4883            }
4884        }
4885
4886        #[test]
4887        fn test_patch_to_variable_edit() {
4888            struct Case {
4889                name: &'static str,
4890                old: &'static str,
4891                patch: &'static str,
4892                cursor_offset: Option<usize>,
4893                expected_variable_edit: &'static str,
4894                expected_after_apply: &'static str,
4895            }
4896
4897            let cases = [
4898                Case {
4899                    name: "simple_replacement",
4900                    old: indoc! {"
4901                        zero
4902                        one
4903                        two
4904                        three
4905                        four
4906                        five
4907                    "},
4908                    patch: indoc! {"
4909                        @@ -3,3 +3,3 @@
4910                         two
4911                        -three
4912                        +THREE
4913                         four
4914                    "},
4915                    cursor_offset: None,
4916                    expected_variable_edit: indoc! {"
4917                        one
4918                        two
4919                        <|fim_middle|>
4920                        THREE
4921                        <|fim_suffix|>
4922                        four
4923                        five
4924                    "},
4925                    expected_after_apply: indoc! {"
4926                        zero
4927                        one
4928                        two
4929                        THREE
4930                        four
4931                        five
4932                    "},
4933                },
4934                Case {
4935                    name: "insertion",
4936                    old: indoc! {"
4937                        a
4938                        b
4939                        c
4940                        d
4941                        e
4942                    "},
4943                    patch: indoc! {"
4944                        @@ -2,0 +3,1 @@
4945                         b
4946                        +X
4947                         c
4948                    "},
4949                    cursor_offset: None,
4950                    expected_variable_edit: indoc! {"
4951                        a
4952                        b
4953                        <|fim_middle|>
4954                        X
4955                        <|fim_suffix|>
4956                        c
4957                        d
4958                    "},
4959                    expected_after_apply: indoc! {"
4960                        a
4961                        b
4962                        X
4963                        c
4964                        d
4965                        e
4966                    "},
4967                },
4968                Case {
4969                    name: "deletion",
4970                    old: indoc! {"
4971                        a
4972                        b
4973                        c
4974                        d
4975                        e
4976                    "},
4977                    patch: indoc! {"
4978                        @@ -2,3 +2,2 @@
4979                         b
4980                        -c
4981                         d
4982                    "},
4983                    cursor_offset: None,
4984                    expected_variable_edit: indoc! {"
4985                        a
4986                        b
4987                        <|fim_middle|>
4988                        <|fim_suffix|>
4989                        d
4990                        e
4991                    "},
4992                    expected_after_apply: indoc! {"
4993                        a
4994                        b
4995                        d
4996                        e
4997                    "},
4998                },
4999                Case {
5000                    name: "edit_near_start",
5001                    old: indoc! {"
5002                        first
5003                        second
5004                        third
5005                        fourth
5006                    "},
5007                    patch: indoc! {"
5008                        @@ -1,1 +1,1 @@
5009                        -first
5010                        +FIRST
5011                    "},
5012                    cursor_offset: None,
5013                    expected_variable_edit: indoc! {"
5014                        <|fim_middle|>
5015                        FIRST
5016                        <|fim_suffix|>
5017                        second
5018                        third
5019                    "},
5020                    expected_after_apply: indoc! {"
5021                        FIRST
5022                        second
5023                        third
5024                        fourth
5025                    "},
5026                },
5027                Case {
5028                    name: "edit_near_end",
5029                    old: indoc! {"
5030                        first
5031                        second
5032                        third
5033                        fourth
5034                    "},
5035                    patch: indoc! {"
5036                        @@ -4,1 +4,1 @@
5037                        -fourth
5038                        +FOURTH
5039                    "},
5040                    cursor_offset: None,
5041                    expected_variable_edit: indoc! {"
5042                        second
5043                        third
5044                        <|fim_middle|>
5045                        FOURTH
5046                        <|fim_suffix|>
5047                    "},
5048                    expected_after_apply: indoc! {"
5049                        first
5050                        second
5051                        third
5052                        FOURTH
5053                    "},
5054                },
5055                Case {
5056                    name: "cursor_at_start_of_replacement",
5057                    old: indoc! {"
5058                        zero
5059                        one
5060                        two
5061                        three
5062                        four
5063                        five
5064                    "},
5065                    patch: indoc! {"
5066                        @@ -3,3 +3,3 @@
5067                         two
5068                        -three
5069                        +THREE
5070                         four
5071                    "},
5072                    cursor_offset: Some(4),
5073                    expected_variable_edit: indoc! {"
5074                        one
5075                        two
5076                        <|fim_middle|>
5077                        <|user_cursor|>THREE
5078                        <|fim_suffix|>
5079                        four
5080                        five
5081                    "},
5082                    expected_after_apply: indoc! {"
5083                        zero
5084                        one
5085                        two
5086                        <|user_cursor|>THREE
5087                        four
5088                        five
5089                    "},
5090                },
5091                Case {
5092                    name: "cursor_in_middle_of_replacement",
5093                    old: indoc! {"
5094                        zero
5095                        one
5096                        two
5097                        three
5098                        four
5099                        five
5100                    "},
5101                    patch: indoc! {"
5102                        @@ -3,3 +3,3 @@
5103                         two
5104                        -three
5105                        +THREE
5106                         four
5107                    "},
5108                    cursor_offset: Some(6),
5109                    expected_variable_edit: indoc! {"
5110                        one
5111                        two
5112                        <|fim_middle|>
5113                        TH<|user_cursor|>REE
5114                        <|fim_suffix|>
5115                        four
5116                        five
5117                    "},
5118                    expected_after_apply: indoc! {"
5119                        zero
5120                        one
5121                        two
5122                        TH<|user_cursor|>REE
5123                        four
5124                        five
5125                    "},
5126                },
5127                Case {
5128                    name: "expands_context_when_two_lines_not_unique_before_and_after",
5129                    old: indoc! {"
5130                        one
5131                        a
5132                        b
5133                        c
5134                        d
5135                        two
5136                        a
5137                        b
5138                        c
5139                        d
5140                        three
5141                        a
5142                        b
5143                        c
5144                        d
5145                        four
5146                    "},
5147                    patch: indoc! {"
5148                        @@ -4,5 +4,5 @@
5149                         two
5150                         a
5151                         b
5152                        -c
5153                        +C
5154                         d
5155                         three
5156                    "},
5157                    cursor_offset: None,
5158                    expected_variable_edit: indoc! {"
5159                        two
5160                        a
5161                        b
5162                        <|fim_middle|>
5163                        C
5164                        <|fim_suffix|>
5165                        d
5166                        three
5167                    "},
5168                    expected_after_apply: indoc! {"
5169                        one
5170                        a
5171                        b
5172                        c
5173                        d
5174                        two
5175                        a
5176                        b
5177                        C
5178                        d
5179                        three
5180                        a
5181                        b
5182                        c
5183                        d
5184                        four
5185                    "},
5186                },
5187                Case {
5188                    name: "expands_context_when_two_lines_not_unique_before_and_after",
5189                    old: indoc! {"
5190                        {
5191                            {
5192                                one();
5193                            }
5194                        }
5195                        {
5196                            {
5197                                two();
5198                            }
5199                        }
5200                        {
5201                            {
5202                                three();
5203                            }
5204                        }
5205                        {
5206                            {
5207                                four();
5208                            }
5209                        }
5210                    "},
5211                    patch: indoc! {"
5212                        @@ -4,5 +4,5 @@
5213                             {
5214                        -        two();
5215                        +        TWO();
5216                             }
5217                    "},
5218                    cursor_offset: None,
5219                    expected_variable_edit: indoc! {"
5220                                one();
5221                            }
5222                        }
5223                        {
5224                            {
5225                        <|fim_middle|>
5226                                TWO();
5227                        <|fim_suffix|>
5228                            }
5229                        }
5230                        {
5231                            {
5232                                three();
5233                    "},
5234                    expected_after_apply: indoc! {"
5235                        {
5236                            {
5237                                one();
5238                            }
5239                        }
5240                        {
5241                            {
5242                                TWO();
5243                            }
5244                        }
5245                        {
5246                            {
5247                                three();
5248                            }
5249                        }
5250                        {
5251                            {
5252                                four();
5253                            }
5254                        }
5255                    "},
5256                },
5257            ];
5258
5259            for case in cases {
5260                let output =
5261                    patch_to_variable_edit_output(case.old, case.patch, case.cursor_offset)
5262                        .unwrap_or_else(|error| {
5263                            panic!("failed converting patch for {}: {error}", case.name)
5264                        });
5265                assert_eq!(
5266                    output, case.expected_variable_edit,
5267                    "patch->variable_edit mismatch for {}",
5268                    case.name
5269                );
5270
5271                let (edit_range, replacement) = apply_variable_edit(case.old, &output)
5272                    .unwrap_or_else(|error| {
5273                        panic!("failed applying variable_edit for {}: {error}", case.name)
5274                    });
5275                let mut edited_by_variable_edit = case.old.to_string();
5276                edited_by_variable_edit.replace_range(edit_range, &replacement);
5277                assert_eq!(
5278                    edited_by_variable_edit, case.expected_after_apply,
5279                    "variable_edit apply mismatch for {}",
5280                    case.name
5281                );
5282
5283                let (expected_edit_range, expected_replacement) =
5284                    apply_variable_edit(case.old, case.expected_variable_edit).unwrap_or_else(
5285                        |error| {
5286                            panic!(
5287                                "failed applying expected variable_edit for {}: {error}",
5288                                case.name
5289                            )
5290                        },
5291                    );
5292                let mut edited_by_expected_variable_edit = case.old.to_string();
5293                edited_by_expected_variable_edit
5294                    .replace_range(expected_edit_range, &expected_replacement);
5295                assert_eq!(
5296                    edited_by_expected_variable_edit, case.expected_after_apply,
5297                    "expected variable_edit apply mismatch for {}",
5298                    case.name
5299                );
5300            }
5301        }
5302
5303        #[test]
5304        fn test_write_cursor_excerpt_section() {
5305            let path = Path::new("test.rs");
5306            let context = "fn main() {\n    hello();\n}\n";
5307            let cursor_offset = 17;
5308            let mut prompt = String::new();
5309            write_cursor_excerpt_section(&mut prompt, path, context, cursor_offset);
5310            assert_eq!(
5311                prompt,
5312                "<|file_sep|>test.rs\nfn main() {\n    h<|user_cursor|>ello();\n}\n<|fim_prefix|>\n"
5313            );
5314        }
5315    }
5316}
5317
5318/// The zeta1 prompt format
5319pub mod zeta1 {
5320    use super::*;
5321    use std::fmt::Write;
5322
5323    pub const CURSOR_MARKER: &str = "<|user_cursor_is_here|>";
5324    pub const START_OF_FILE_MARKER: &str = "<|start_of_file|>";
5325    pub const EDITABLE_REGION_START_MARKER: &str = "<|editable_region_start|>";
5326    pub const EDITABLE_REGION_END_MARKER: &str = "<|editable_region_end|>";
5327
5328    const INSTRUCTION_HEADER: &str = concat!(
5329        "### Instruction:\n",
5330        "You are a code completion assistant and your task is to analyze user edits and then rewrite an ",
5331        "excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking ",
5332        "into account the cursor location.\n\n",
5333        "### User Edits:\n\n"
5334    );
5335    const EXCERPT_HEADER: &str = "\n\n### User Excerpt:\n\n";
5336    const RESPONSE_HEADER: &str = "\n\n### Response:\n";
5337
5338    /// Formats a complete zeta1 prompt from the input events and excerpt.
5339    pub fn format_zeta1_prompt(input_events: &str, input_excerpt: &str) -> String {
5340        let mut prompt = String::with_capacity(
5341            INSTRUCTION_HEADER.len()
5342                + input_events.len()
5343                + EXCERPT_HEADER.len()
5344                + input_excerpt.len()
5345                + RESPONSE_HEADER.len(),
5346        );
5347        prompt.push_str(INSTRUCTION_HEADER);
5348        prompt.push_str(input_events);
5349        prompt.push_str(EXCERPT_HEADER);
5350        prompt.push_str(input_excerpt);
5351        prompt.push_str(RESPONSE_HEADER);
5352        prompt
5353    }
5354
5355    /// Formats a complete zeta1 prompt from a `Zeta2PromptInput` using the given
5356    /// editable and context byte-offset ranges within `cursor_excerpt`.
5357    pub fn format_zeta1_from_input(
5358        input: &Zeta2PromptInput,
5359        editable_range: Range<usize>,
5360        context_range: Range<usize>,
5361    ) -> String {
5362        let events = format_zeta1_events(&input.events);
5363        let excerpt = format_zeta1_excerpt(input, editable_range, context_range);
5364        format_zeta1_prompt(&events, &excerpt)
5365    }
5366
5367    /// Formats events in zeta1 style (oldest first).
5368    fn format_zeta1_events(events: &[Arc<Event>]) -> String {
5369        let mut result = String::new();
5370        for event in
5371            events
5372                .iter()
5373                .skip(events.len().saturating_sub(max_edit_event_count_for_format(
5374                    &ZetaFormat::V0114180EditableRegion,
5375                )))
5376        {
5377            let event_string = format_zeta1_event(event);
5378            if event_string.is_empty() {
5379                continue;
5380            }
5381            if !result.is_empty() {
5382                result.push_str("\n\n");
5383            }
5384            result.push_str(&event_string);
5385        }
5386        result
5387    }
5388
5389    fn format_zeta1_event(event: &Event) -> String {
5390        match event {
5391            Event::BufferChange {
5392                path,
5393                old_path,
5394                diff,
5395                ..
5396            } => {
5397                let mut prompt = String::new();
5398                if old_path != path {
5399                    writeln!(
5400                        prompt,
5401                        "User renamed {} to {}\n",
5402                        old_path.display(),
5403                        path.display()
5404                    )
5405                    .ok();
5406                }
5407                if !diff.is_empty() {
5408                    write!(
5409                        prompt,
5410                        "User edited {}:\n```diff\n{}\n```",
5411                        path.display(),
5412                        diff
5413                    )
5414                    .ok();
5415                }
5416                prompt
5417            }
5418        }
5419    }
5420
5421    /// Formats the excerpt section of a zeta1 prompt using byte-offset ranges
5422    /// within `cursor_excerpt`.
5423    fn format_zeta1_excerpt(
5424        input: &Zeta2PromptInput,
5425        editable_range: Range<usize>,
5426        context_range: Range<usize>,
5427    ) -> String {
5428        let path_str = input.cursor_path.to_string_lossy();
5429        let excerpt = &*input.cursor_excerpt;
5430        let cursor_offset = input.cursor_offset_in_excerpt;
5431
5432        let mut prompt = String::new();
5433        writeln!(&mut prompt, "```{path_str}").ok();
5434
5435        let starts_at_file_beginning =
5436            input.excerpt_start_row == Some(0) && context_range.start == 0;
5437        if starts_at_file_beginning {
5438            writeln!(&mut prompt, "{START_OF_FILE_MARKER}").ok();
5439        }
5440
5441        prompt.push_str(&excerpt[context_range.start..editable_range.start]);
5442
5443        writeln!(&mut prompt, "{EDITABLE_REGION_START_MARKER}").ok();
5444        prompt.push_str(&excerpt[editable_range.start..cursor_offset]);
5445        prompt.push_str(CURSOR_MARKER);
5446        prompt.push_str(&excerpt[cursor_offset..editable_range.end]);
5447        write!(&mut prompt, "\n{EDITABLE_REGION_END_MARKER}").ok();
5448
5449        prompt.push_str(&excerpt[editable_range.end..context_range.end]);
5450        write!(prompt, "\n```").ok();
5451
5452        prompt
5453    }
5454
5455    /// Cleans zeta1 model output by extracting content between editable region
5456    /// markers and converting the zeta1 cursor marker to the universal one.
5457    /// Returns `None` if the output doesn't contain the expected markers.
5458    pub fn clean_zeta1_model_output(output: &str) -> Option<String> {
5459        let content = output.replace(CURSOR_MARKER, "");
5460
5461        let content_start = content
5462            .find(EDITABLE_REGION_START_MARKER)
5463            .map(|pos| pos + EDITABLE_REGION_START_MARKER.len())
5464            .map(|pos| {
5465                if content.as_bytes().get(pos) == Some(&b'\n') {
5466                    pos + 1
5467                } else {
5468                    pos
5469                }
5470            })
5471            .unwrap_or(0);
5472
5473        let content_end = content
5474            .find(EDITABLE_REGION_END_MARKER)
5475            .map(|pos| {
5476                if pos > 0 && content.as_bytes().get(pos - 1) == Some(&b'\n') {
5477                    pos - 1
5478                } else {
5479                    pos
5480                }
5481            })
5482            .unwrap_or(content.len());
5483
5484        if content_start > content_end {
5485            return Some(String::new());
5486        }
5487
5488        let extracted = &content[content_start..content_end];
5489
5490        let cursor_offset = output.find(CURSOR_MARKER).map(|zeta1_cursor_pos| {
5491            let text_before_cursor = output[..zeta1_cursor_pos].replace(CURSOR_MARKER, "");
5492            let text_before_cursor = text_before_cursor
5493                .find(EDITABLE_REGION_START_MARKER)
5494                .map(|pos| {
5495                    let after_marker = pos + EDITABLE_REGION_START_MARKER.len();
5496                    if text_before_cursor.as_bytes().get(after_marker) == Some(&b'\n') {
5497                        after_marker + 1
5498                    } else {
5499                        after_marker
5500                    }
5501                })
5502                .unwrap_or(0);
5503            let offset_in_extracted = zeta1_cursor_pos
5504                .saturating_sub(text_before_cursor)
5505                .min(extracted.len());
5506            offset_in_extracted
5507        });
5508
5509        let mut result = String::with_capacity(extracted.len() + super::CURSOR_MARKER.len());
5510        if let Some(offset) = cursor_offset {
5511            result.push_str(&extracted[..offset]);
5512            result.push_str(super::CURSOR_MARKER);
5513            result.push_str(&extracted[offset..]);
5514        } else {
5515            result.push_str(extracted);
5516        }
5517
5518        Some(result)
5519    }
5520}
5521
5522#[cfg(test)]
5523mod tests {
5524    use super::*;
5525    use indoc::indoc;
5526
5527    fn make_input(
5528        cursor_excerpt: &str,
5529        editable_range: Range<usize>,
5530        cursor_offset: usize,
5531        events: Vec<Event>,
5532        related_files: Vec<RelatedFile>,
5533    ) -> Zeta2PromptInput {
5534        let context_range = 0..cursor_excerpt.len();
5535        Zeta2PromptInput {
5536            cursor_path: Path::new("test.rs").into(),
5537            cursor_excerpt: cursor_excerpt.into(),
5538            cursor_offset_in_excerpt: cursor_offset,
5539            excerpt_start_row: None,
5540            events: events.into_iter().map(Arc::new).collect(),
5541            related_files: Some(related_files),
5542            active_buffer_diagnostics: vec![],
5543            excerpt_ranges: ExcerptRanges {
5544                editable_150: editable_range.clone(),
5545                editable_180: editable_range.clone(),
5546                editable_350: editable_range,
5547                editable_150_context_350: context_range.clone(),
5548                editable_180_context_350: context_range.clone(),
5549                editable_350_context_150: context_range,
5550                ..Default::default()
5551            },
5552            syntax_ranges: None,
5553            in_open_source_repo: false,
5554            can_collect_data: false,
5555            repo_url: None,
5556        }
5557    }
5558
5559    fn make_input_with_context_range(
5560        excerpt: &str,
5561        editable_range: Range<usize>,
5562        context_range: Range<usize>,
5563        cursor_offset: usize,
5564    ) -> Zeta2PromptInput {
5565        Zeta2PromptInput {
5566            cursor_path: Path::new("test.rs").into(),
5567            cursor_excerpt: excerpt.into(),
5568            cursor_offset_in_excerpt: cursor_offset,
5569            excerpt_start_row: None,
5570            events: vec![],
5571            related_files: Some(vec![]),
5572            active_buffer_diagnostics: vec![],
5573            excerpt_ranges: ExcerptRanges {
5574                editable_150: editable_range.clone(),
5575                editable_180: editable_range.clone(),
5576                editable_350: editable_range,
5577                editable_150_context_350: context_range.clone(),
5578                editable_180_context_350: context_range.clone(),
5579                editable_350_context_150: context_range,
5580                ..Default::default()
5581            },
5582            syntax_ranges: None,
5583            in_open_source_repo: false,
5584            can_collect_data: false,
5585            repo_url: None,
5586        }
5587    }
5588
5589    fn make_event(path: &str, diff: &str) -> Event {
5590        Event::BufferChange {
5591            path: Path::new(path).into(),
5592            old_path: Path::new(path).into(),
5593            diff: diff.to_string(),
5594            old_range: 0..diff.len(),
5595            new_range: 0..diff.len(),
5596            predicted: false,
5597            in_open_source_repo: false,
5598        }
5599    }
5600
5601    fn make_related_file(path: &str, content: &str) -> RelatedFile {
5602        RelatedFile {
5603            path: Path::new(path).into(),
5604            max_row: content.lines().count() as u32,
5605            excerpts: vec![RelatedExcerpt {
5606                row_range: 0..content.lines().count() as u32,
5607                text: content.into(),
5608                order: 0,
5609                context_source: ContextSource::Lsp,
5610            }],
5611            in_open_source_repo: false,
5612        }
5613    }
5614
5615    fn format_with_budget(input: &Zeta2PromptInput, max_tokens: usize) -> Option<String> {
5616        format_prompt_with_budget_for_format(input, ZetaFormat::V0114180EditableRegion, max_tokens)
5617    }
5618
5619    fn budget_with_margin(requested_tokens: usize) -> usize {
5620        ((requested_tokens as f64) / 0.9).ceil() as usize
5621    }
5622
5623    #[test]
5624    fn test_no_truncation_when_within_budget() {
5625        let input = make_input(
5626            "prefix\neditable\nsuffix",
5627            7..15,
5628            10,
5629            vec![make_event("a.rs", "-old\n+new\n")],
5630            vec![make_related_file("related.rs", "fn helper() {}\n")],
5631        );
5632
5633        assert_eq!(
5634            format_with_budget(&input, 10000).unwrap(),
5635            indoc! {r#"
5636                <|file_sep|>related.rs
5637                fn helper() {}
5638                <|file_sep|>edit history
5639                --- a/a.rs
5640                +++ b/a.rs
5641                -old
5642                +new
5643                <|file_sep|>test.rs
5644                <|fim_prefix|>
5645                prefix
5646                <|fim_middle|>current
5647                edi<|user_cursor|>table
5648                <|fim_suffix|>
5649
5650                suffix
5651                <|fim_middle|>updated
5652            "#}
5653            .to_string()
5654        );
5655    }
5656
5657    #[test]
5658    fn test_truncation_drops_edit_history_when_budget_tight() {
5659        let input = make_input(
5660            "code",
5661            0..4,
5662            2,
5663            vec![make_event("a.rs", "-x\n+y\n")],
5664            vec![
5665                make_related_file("r1.rs", "aaaaaaa\n"),
5666                make_related_file("r2.rs", "bbbbbbb\n"),
5667            ],
5668        );
5669
5670        assert_eq!(
5671            format_with_budget(&input, 10000).unwrap(),
5672            indoc! {r#"
5673                <|file_sep|>r1.rs
5674                aaaaaaa
5675                <|file_sep|>r2.rs
5676                bbbbbbb
5677                <|file_sep|>edit history
5678                --- a/a.rs
5679                +++ b/a.rs
5680                -x
5681                +y
5682                <|file_sep|>test.rs
5683                <|fim_prefix|>
5684                <|fim_middle|>current
5685                co<|user_cursor|>de
5686                <|fim_suffix|>
5687                <|fim_middle|>updated
5688            "#}
5689            .to_string()
5690        );
5691
5692        assert_eq!(
5693            format_with_budget(&input, budget_with_margin(55)),
5694            Some(
5695                indoc! {r#"
5696                <|file_sep|>edit history
5697                --- a/a.rs
5698                +++ b/a.rs
5699                -x
5700                +y
5701                <|file_sep|>test.rs
5702                <|fim_prefix|>
5703                <|fim_middle|>current
5704                co<|user_cursor|>de
5705                <|fim_suffix|>
5706                <|fim_middle|>updated
5707            "#}
5708                .to_string()
5709            )
5710        );
5711    }
5712
5713    #[test]
5714    fn test_truncation_includes_partial_excerpts() {
5715        let input = make_input(
5716            "x",
5717            0..1,
5718            0,
5719            vec![],
5720            vec![RelatedFile {
5721                path: Path::new("big.rs").into(),
5722                max_row: 30,
5723                in_open_source_repo: false,
5724                excerpts: vec![
5725                    RelatedExcerpt {
5726                        row_range: 0..10,
5727                        text: "first excerpt\n".into(),
5728                        order: 0,
5729                        context_source: ContextSource::Lsp,
5730                    },
5731                    RelatedExcerpt {
5732                        row_range: 11..20,
5733                        text: "second excerpt\n".into(),
5734                        order: 0,
5735                        context_source: ContextSource::Lsp,
5736                    },
5737                    RelatedExcerpt {
5738                        row_range: 21..30,
5739                        text: "third excerpt\n".into(),
5740                        order: 0,
5741                        context_source: ContextSource::Lsp,
5742                    },
5743                ],
5744            }],
5745        );
5746
5747        assert_eq!(
5748            format_with_budget(&input, 10000).unwrap(),
5749            indoc! {r#"
5750                <|file_sep|>big.rs
5751                first excerpt
5752                ...
5753                second excerpt
5754                ...
5755                third excerpt
5756                <|file_sep|>test.rs
5757                <|fim_prefix|>
5758                <|fim_middle|>current
5759                <|user_cursor|>x
5760                <|fim_suffix|>
5761                <|fim_middle|>updated
5762            "#}
5763            .to_string()
5764        );
5765
5766        assert_eq!(
5767            format_with_budget(&input, budget_with_margin(50)).unwrap(),
5768            indoc! {r#"
5769                <|file_sep|>big.rs
5770                first excerpt
5771                ...
5772                <|file_sep|>test.rs
5773                <|fim_prefix|>
5774                <|fim_middle|>current
5775                <|user_cursor|>x
5776                <|fim_suffix|>
5777                <|fim_middle|>updated
5778            "#}
5779            .to_string()
5780        );
5781    }
5782
5783    #[test]
5784    fn test_contiguous_excerpts_render_without_ellipsis() {
5785        // Excerpts whose row ranges touch (end == next start) are contiguous
5786        // segments of the same region and must render seamlessly, without an
5787        // ellipsis line between them.
5788        let input = make_input(
5789            "x",
5790            0..1,
5791            0,
5792            vec![],
5793            vec![RelatedFile {
5794                path: Path::new("big.rs").into(),
5795                max_row: 30,
5796                in_open_source_repo: false,
5797                excerpts: vec![
5798                    RelatedExcerpt {
5799                        row_range: 0..10,
5800                        text: "first segment\n".into(),
5801                        order: 1,
5802                        context_source: ContextSource::GitLog,
5803                    },
5804                    RelatedExcerpt {
5805                        row_range: 10..20,
5806                        text: "second segment\n".into(),
5807                        order: 0,
5808                        context_source: ContextSource::OracleSnippet,
5809                    },
5810                ],
5811            }],
5812        );
5813
5814        assert_eq!(
5815            format_with_budget(&input, 10000).unwrap(),
5816            indoc! {r#"
5817                <|file_sep|>big.rs
5818                first segment
5819                second segment
5820                ...
5821                <|file_sep|>test.rs
5822                <|fim_prefix|>
5823                <|fim_middle|>current
5824                <|user_cursor|>x
5825                <|fim_suffix|>
5826                <|fim_middle|>updated
5827            "#}
5828            .to_string()
5829        );
5830    }
5831
5832    #[test]
5833    fn test_truncation_prioritizes_lower_order_excerpts() {
5834        // Two files: file_a has a high-order excerpt, file_b has a low-order one.
5835        // With tight budget, only the lower-order excerpt from file_b should be included.
5836        let input = make_input(
5837            "x",
5838            0..1,
5839            0,
5840            vec![],
5841            vec![
5842                RelatedFile {
5843                    path: Path::new("file_a.rs").into(),
5844                    max_row: 10,
5845                    in_open_source_repo: false,
5846                    excerpts: vec![RelatedExcerpt {
5847                        row_range: 0..10,
5848                        text: "low priority content\n".into(),
5849                        order: 5,
5850                        context_source: ContextSource::Lsp,
5851                    }],
5852                },
5853                RelatedFile {
5854                    path: Path::new("file_b.rs").into(),
5855                    max_row: 10,
5856                    in_open_source_repo: false,
5857                    excerpts: vec![RelatedExcerpt {
5858                        row_range: 0..10,
5859                        text: "high priority content\n".into(),
5860                        order: 1,
5861                        context_source: ContextSource::Lsp,
5862                    }],
5863                },
5864            ],
5865        );
5866
5867        // With large budget, both files included; rendered in stable lexicographic order.
5868        assert_eq!(
5869            format_with_budget(&input, 10000).unwrap(),
5870            indoc! {r#"
5871                <|file_sep|>file_a.rs
5872                low priority content
5873                <|file_sep|>file_b.rs
5874                high priority content
5875                <|file_sep|>test.rs
5876                <|fim_prefix|>
5877                <|fim_middle|>current
5878                <|user_cursor|>x
5879                <|fim_suffix|>
5880                <|fim_middle|>updated
5881            "#}
5882            .to_string()
5883        );
5884
5885        // With tight budget, only file_b (lower order) fits.
5886        // Cursor section is ~37 tokens, so budget 52 leaves ~15 for related files.
5887        // file_b header (7) + excerpt (7) = 14 tokens, which fits.
5888        // file_a would need another 14 tokens, which doesn't fit.
5889        assert_eq!(
5890            format_with_budget(&input, budget_with_margin(52)).unwrap(),
5891            indoc! {r#"
5892                <|file_sep|>file_b.rs
5893                high priority content
5894                <|file_sep|>test.rs
5895                <|fim_prefix|>
5896                <|fim_middle|>current
5897                <|user_cursor|>x
5898                <|fim_suffix|>
5899                <|fim_middle|>updated
5900            "#}
5901            .to_string()
5902        );
5903    }
5904
5905    #[test]
5906    fn test_truncation_drops_high_order_excerpts_within_file() {
5907        // A single file has excerpts at order 1 and order 3. With a tight budget,
5908        // only the order-1 excerpts are included while the order-3 excerpt is
5909        // dropped — even though they belong to the same file. This also preserves
5910        // the parent invariant: parent outline items have order ≤ their best
5911        // child, so they're always included when any child is.
5912        let input = make_input(
5913            "x",
5914            0..1,
5915            0,
5916            vec![],
5917            vec![RelatedFile {
5918                path: Path::new("mod.rs").into(),
5919                max_row: 30,
5920                in_open_source_repo: false,
5921                excerpts: vec![
5922                    RelatedExcerpt {
5923                        row_range: 0..5,
5924                        text: "mod header\n".into(),
5925                        order: 1,
5926                        context_source: ContextSource::Lsp,
5927                    },
5928                    RelatedExcerpt {
5929                        row_range: 6..15,
5930                        text: "important fn\n".into(),
5931                        order: 1,
5932                        context_source: ContextSource::Lsp,
5933                    },
5934                    RelatedExcerpt {
5935                        row_range: 16..30,
5936                        text: "less important fn\n".into(),
5937                        order: 3,
5938                        context_source: ContextSource::Lsp,
5939                    },
5940                ],
5941            }],
5942        );
5943
5944        // With large budget, all three excerpts included.
5945        assert_eq!(
5946            format_with_budget(&input, 10000).unwrap(),
5947            indoc! {r#"
5948                <|file_sep|>mod.rs
5949                mod header
5950                ...
5951                important fn
5952                ...
5953                less important fn
5954                <|file_sep|>test.rs
5955                <|fim_prefix|>
5956                <|fim_middle|>current
5957                <|user_cursor|>x
5958                <|fim_suffix|>
5959                <|fim_middle|>updated
5960            "#}
5961            .to_string()
5962        );
5963
5964        // With tight budget, only order<=1 excerpts included (header + important fn).
5965        assert_eq!(
5966            format_with_budget(&input, budget_with_margin(55)).unwrap(),
5967            indoc! {r#"
5968                <|file_sep|>mod.rs
5969                mod header
5970                ...
5971                important fn
5972                ...
5973                <|file_sep|>test.rs
5974                <|fim_prefix|>
5975                <|fim_middle|>current
5976                <|user_cursor|>x
5977                <|fim_suffix|>
5978                <|fim_middle|>updated
5979            "#}
5980            .to_string()
5981        );
5982    }
5983
5984    #[test]
5985    fn test_truncation_drops_older_events_first() {
5986        let input = make_input(
5987            "x",
5988            0..1,
5989            0,
5990            vec![make_event("old.rs", "-1\n"), make_event("new.rs", "-2\n")],
5991            vec![],
5992        );
5993
5994        assert_eq!(
5995            format_with_budget(&input, 10000).unwrap(),
5996            indoc! {r#"
5997                <|file_sep|>edit history
5998                --- a/old.rs
5999                +++ b/old.rs
6000                -1
6001                --- a/new.rs
6002                +++ b/new.rs
6003                -2
6004                <|file_sep|>test.rs
6005                <|fim_prefix|>
6006                <|fim_middle|>current
6007                <|user_cursor|>x
6008                <|fim_suffix|>
6009                <|fim_middle|>updated
6010            "#}
6011            .to_string()
6012        );
6013
6014        assert_eq!(
6015            format_with_budget(&input, 60).unwrap(),
6016            indoc! {r#"
6017                <|file_sep|>edit history
6018                --- a/new.rs
6019                +++ b/new.rs
6020                -2
6021                <|file_sep|>test.rs
6022                <|fim_prefix|>
6023                <|fim_middle|>current
6024                <|user_cursor|>x
6025                <|fim_suffix|>
6026                <|fim_middle|>updated
6027            "#}
6028            .to_string()
6029        );
6030    }
6031
6032    #[test]
6033    fn test_cursor_excerpt_always_included_with_minimal_budget() {
6034        let input = make_input(
6035            "fn main() {}",
6036            0..12,
6037            3,
6038            vec![make_event("a.rs", "-old\n+new\n")],
6039            vec![make_related_file("related.rs", "helper\n")],
6040        );
6041
6042        assert!(format_with_budget(&input, 30).is_none())
6043    }
6044
6045    #[track_caller]
6046    fn format_seed_coder(input: &Zeta2PromptInput) -> String {
6047        format_prompt_with_budget_for_format(input, ZetaFormat::V0211SeedCoder, 10000)
6048            .expect("seed coder prompt formatting should succeed")
6049    }
6050
6051    #[track_caller]
6052    fn format_seed_coder_with_budget(input: &Zeta2PromptInput, max_tokens: usize) -> String {
6053        format_prompt_with_budget_for_format(input, ZetaFormat::V0211SeedCoder, max_tokens)
6054            .expect("seed coder prompt formatting should succeed")
6055    }
6056
6057    #[test]
6058    fn test_seed_coder_alias_matches_v0211_seed_coder() {
6059        let input = make_input(
6060            "prefix\neditable\nsuffix",
6061            7..15,
6062            10,
6063            vec![make_event("a.rs", "-old\n+new\n")],
6064            vec![make_related_file("related.rs", "fn helper() {}\n")],
6065        );
6066
6067        assert_eq!(
6068            format_prompt_with_budget_for_format(&input, ZetaFormat::V0211SeedCoder, 10000),
6069            format_prompt_with_budget_for_format(&input, ZetaFormat::V0331SeedCoderModelPy, 10000)
6070        );
6071        assert_eq!(
6072            ZetaFormat::parse("V0331SeedCoderModelPy").unwrap(),
6073            ZetaFormat::V0331SeedCoderModelPy
6074        );
6075    }
6076
6077    #[test]
6078    fn test_seed_coder_basic_format() {
6079        let input = make_input(
6080            "prefix\neditable\nsuffix",
6081            7..15,
6082            10,
6083            vec![make_event("a.rs", "-old\n+new\n")],
6084            vec![make_related_file("related.rs", "fn helper() {}\n")],
6085        );
6086
6087        assert_eq!(
6088            format_seed_coder(&input),
6089            indoc! {r#"
6090                <[fim-suffix]>
6091                suffix
6092                <[fim-prefix]><filename>related.rs
6093                fn helper() {}
6094
6095                <filename>edit_history
6096                --- a/a.rs
6097                +++ b/a.rs
6098                -old
6099                +new
6100
6101                <filename>test.rs
6102                prefix
6103                <<<<<<< CURRENT
6104                edi<|user_cursor|>table
6105                =======
6106                <[fim-middle]>"#}
6107        );
6108    }
6109
6110    #[test]
6111    fn test_qwen36_multi_region_uses_qwen_psm_fim_format() {
6112        let input = make_input(
6113            "prefix\neditable\nsuffix",
6114            7..15,
6115            10,
6116            vec![make_event("a.rs", "-old\n+new\n")],
6117            vec![make_related_file("related.rs", "fn helper() {}\n")],
6118        );
6119
6120        assert_eq!(
6121            format_prompt_with_budget_for_format(&input, ZetaFormat::V0608QwenMultiRegions, 10000)
6122                .expect("qwen prompt formatting should succeed"),
6123            indoc! {r#"
6124                <|fim_prefix|><|file_sep|>related.rs
6125                fn helper() {}
6126
6127                <|file_sep|>edit_history
6128                --- a/a.rs
6129                +++ b/a.rs
6130                -old
6131                +new
6132
6133                <|file_sep|>test.rs
6134                prefix
6135                <|marker_1|>edi<|user_cursor|>table<|marker_2|>
6136                <|fim_suffix|>
6137                suffix
6138                <|fim_middle|>"#}
6139        );
6140    }
6141
6142    #[test]
6143    fn test_v0420_formats_diagnostics_before_related_files() {
6144        let mut input = make_input(
6145            "prefix\neditable\nsuffix",
6146            7..15,
6147            10,
6148            vec![],
6149            vec![make_related_file("related.rs", "fn helper() {}\n")],
6150        );
6151        input.active_buffer_diagnostics = vec![
6152            ActiveBufferDiagnostic {
6153                severity: Some(1),
6154                message: "missing semicolon".to_string(),
6155                snippet: "let value = 1".to_string(),
6156                snippet_buffer_row_range: 1..2,
6157                diagnostic_range_in_snippet: 12..13,
6158            },
6159            ActiveBufferDiagnostic {
6160                severity: Some(2),
6161                message: "file-level warning".to_string(),
6162                snippet: String::new(),
6163                snippet_buffer_row_range: 0..0,
6164                diagnostic_range_in_snippet: 0..0,
6165            },
6166        ];
6167
6168        let prompt =
6169            format_prompt_with_budget_for_format(&input, ZetaFormat::V0420Diagnostics, 10000)
6170                .expect("v0420 prompt formatting should succeed");
6171
6172        assert_eq!(
6173            prompt,
6174            indoc! {r#"
6175                <[fim-suffix]>
6176                suffix
6177                <[fim-prefix]><filename>diagnostics
6178                *missing semicolon*:
6179                ```
6180                let value = 1
6181                ```
6182                *file-level warning*
6183
6184                <filename>related.rs
6185                fn helper() {}
6186
6187                <filename>test.rs
6188                prefix
6189                <|marker_1|>edi<|user_cursor|>table<|marker_2|>
6190                <[fim-middle]>"#}
6191        );
6192    }
6193
6194    #[test]
6195    fn test_v0317_formats_prompt_with_many_related_files() {
6196        let related_files = (0..900)
6197            .map(|index| {
6198                make_related_file(
6199                    &format!("related_{index}.rs"),
6200                    "fn helper() {\n    let value = 1;\n}\n",
6201                )
6202            })
6203            .collect();
6204
6205        let input = make_input(
6206            "code",
6207            0..4,
6208            2,
6209            vec![make_event("a.rs", "-x\n+y\n")],
6210            related_files,
6211        );
6212
6213        let prompt =
6214            format_prompt_with_budget_for_format(&input, ZetaFormat::V0317SeedMultiRegions, 4096);
6215
6216        assert!(prompt.is_some());
6217        let prompt = prompt.expect("v0317 should produce a prompt under high related-file count");
6218        assert!(prompt.contains("test.rs"));
6219        assert!(prompt.contains(CURSOR_MARKER));
6220    }
6221
6222    #[test]
6223    fn test_v0327_formats_single_file_prompt_without_related_files() {
6224        let excerpt = indoc! {"
6225            line01
6226            line02
6227            line03
6228            line04
6229            line05
6230            line06
6231            line07
6232            line08
6233            line09
6234            line10
6235            line11
6236            line12
6237            line13
6238            line14
6239            line15
6240            line16
6241            line17
6242            line18
6243            line19
6244            line20
6245        "};
6246        let cursor_offset = excerpt.find("line10").expect("cursor line exists");
6247        let input = make_input(
6248            excerpt,
6249            0..excerpt.len(),
6250            cursor_offset,
6251            vec![make_event("a.rs", "-x\n+y\n")],
6252            vec![make_related_file("related.rs", "fn helper() {}\n")],
6253        );
6254
6255        let prompt =
6256            format_prompt_with_budget_for_format(&input, ZetaFormat::V0327SingleFile, 4096)
6257                .expect("v0327 prompt should fit");
6258
6259        assert!(prompt.contains("line01"));
6260        assert!(prompt.contains("line20"));
6261        assert!(prompt.contains("<filename>edit_history"));
6262        assert!(prompt.contains("<filename>test.rs"));
6263        assert!(prompt.contains(CURSOR_MARKER));
6264        assert!(!prompt.contains("related.rs"));
6265        assert!(!prompt.contains("fn helper() {}"));
6266    }
6267
6268    #[test]
6269    fn test_v0327_resolve_cursor_region_uses_full_excerpt_context() {
6270        let excerpt = (0..80)
6271            .map(|index| format!("l{index:02}\n"))
6272            .collect::<String>();
6273        let cursor_offset = excerpt.find("l40").expect("cursor line exists");
6274        let input = make_input(&excerpt, 0..excerpt.len(), cursor_offset, vec![], vec![]);
6275
6276        let (context, editable_range, context_range, adjusted_cursor) =
6277            resolve_cursor_region(&input, ZetaFormat::V0327SingleFile);
6278
6279        assert_eq!(context, excerpt);
6280        assert_eq!(context_range, 0..excerpt.len());
6281        assert_eq!(adjusted_cursor, cursor_offset);
6282        assert!(editable_range.start < adjusted_cursor);
6283        assert!(editable_range.end > adjusted_cursor);
6284        assert!(editable_range.end < excerpt.len());
6285    }
6286
6287    #[test]
6288    fn test_v0615_formats_hashed_markers_for_rendered_related_context() {
6289        let current_text = "fn main() {\n    let value = 1;\n}\n";
6290        let cursor_offset = current_text.find("let value").unwrap();
6291        let input = Zeta2PromptInput {
6292            cursor_path: Path::new("test.rs").into(),
6293            cursor_excerpt: current_text.into(),
6294            cursor_offset_in_excerpt: cursor_offset,
6295            excerpt_start_row: Some(0),
6296            events: Vec::new(),
6297            related_files: Some(vec![
6298                RelatedFile {
6299                    path: Path::new("test.rs").into(),
6300                    max_row: 3,
6301                    excerpts: vec![RelatedExcerpt {
6302                        row_range: 0..3,
6303                        text: current_text.into(),
6304                        order: 0,
6305                        context_source: ContextSource::CurrentFile,
6306                    }],
6307                    in_open_source_repo: false,
6308                },
6309                RelatedFile {
6310                    path: Path::new("helper.rs").into(),
6311                    max_row: 3,
6312                    excerpts: vec![RelatedExcerpt {
6313                        row_range: 10..13,
6314                        text: "fn helper() {\n    one();\n}\n".into(),
6315                        order: 1,
6316                        context_source: ContextSource::EditHistory,
6317                    }],
6318                    in_open_source_repo: false,
6319                },
6320                RelatedFile {
6321                    path: Path::new("readonly.rs").into(),
6322                    max_row: 1,
6323                    excerpts: vec![RelatedExcerpt {
6324                        row_range: 0..1,
6325                        text: "pub fn readonly() {}\n".into(),
6326                        order: 2,
6327                        context_source: ContextSource::Lsp,
6328                    }],
6329                    in_open_source_repo: false,
6330                },
6331            ]),
6332            active_buffer_diagnostics: vec![],
6333            excerpt_ranges: ExcerptRanges::default(),
6334            syntax_ranges: None,
6335            in_open_source_repo: false,
6336            can_collect_data: false,
6337            repo_url: None,
6338        };
6339
6340        let prompt = format_zeta_prompt(&input, ZetaFormat::V0615HashRegions).unwrap();
6341        let marker_table = hashed_regions::build_marker_table(&input);
6342        let marker_count: usize = marker_table
6343            .iter()
6344            .map(|snippet| snippet.markers.len())
6345            .sum();
6346
6347        assert!(prompt.starts_with("<[fim-suffix]>\n<[fim-prefix]><filename>test.rs\n"));
6348        assert!(prompt.ends_with("<[fim-middle]>"));
6349        assert!(prompt.contains(CURSOR_MARKER));
6350        assert!(prompt.contains("<filename>helper.rs\n"));
6351        assert!(prompt.contains("<filename>readonly.rs\n"));
6352        assert!(prompt.contains("<filename>readonly.rs\n<|marker_"));
6353        assert!(prompt.contains("pub fn readonly() {}"));
6354        assert_eq!(
6355            prompt.matches(hashed_regions::MARKER_TAG_PREFIX).count(),
6356            marker_count
6357        );
6358        assert!(!prompt.contains(seed_coder::START_MARKER));
6359    }
6360
6361    #[test]
6362    fn test_v0615_parse_related_file_jump_as_patch() {
6363        let current_text = "fn main() {\n    helper();\n}\n";
6364        let helper_text = "fn helper() {\n    one();\n}\n";
6365        let input = Zeta2PromptInput {
6366            cursor_path: Path::new("test.rs").into(),
6367            cursor_excerpt: current_text.into(),
6368            cursor_offset_in_excerpt: current_text.find("helper").unwrap(),
6369            excerpt_start_row: Some(0),
6370            events: Vec::new(),
6371            related_files: Some(vec![
6372                RelatedFile {
6373                    path: Path::new("test.rs").into(),
6374                    max_row: 3,
6375                    excerpts: vec![RelatedExcerpt {
6376                        row_range: 0..3,
6377                        text: current_text.into(),
6378                        order: 0,
6379                        context_source: ContextSource::CurrentFile,
6380                    }],
6381                    in_open_source_repo: false,
6382                },
6383                RelatedFile {
6384                    path: Path::new("helper.rs").into(),
6385                    max_row: 13,
6386                    excerpts: vec![RelatedExcerpt {
6387                        row_range: 10..13,
6388                        text: helper_text.into(),
6389                        order: 1,
6390                        context_source: ContextSource::EditHistory,
6391                    }],
6392                    in_open_source_repo: false,
6393                },
6394            ]),
6395            active_buffer_diagnostics: vec![],
6396            excerpt_ranges: ExcerptRanges::default(),
6397            syntax_ranges: None,
6398            in_open_source_repo: false,
6399            can_collect_data: false,
6400            repo_url: None,
6401        };
6402        let marker_table = hashed_regions::build_marker_table(&input);
6403        let helper_markers = &marker_table[1].markers;
6404        let start_tag = hashed_regions::marker_tag(&helper_markers[0].0);
6405        let end_tag = hashed_regions::marker_tag(&helper_markers[helper_markers.len() - 1].0);
6406        let output = format!(
6407            "{start_tag}\nfn helper() {{\n    two();\n}}\n{end_tag}{}",
6408            hashed_regions::V0615_END_MARKER
6409        );
6410
6411        let patch =
6412            parse_zeta2_model_output_as_patch(&output, ZetaFormat::V0615HashRegions, &input)
6413                .unwrap();
6414
6415        assert!(patch.contains("--- a/helper.rs"), "patch: {patch}");
6416        assert!(patch.contains("@@ -11,"), "patch: {patch}");
6417        assert!(patch.contains("-    one();"), "patch: {patch}");
6418        assert!(patch.contains("+    two();"), "patch: {patch}");
6419    }
6420
6421    #[test]
6422    fn test_v0615_expected_output_round_trips_to_patch() {
6423        let current_text = "fn main() {\n    helper();\n}\n";
6424        let helper_text = "fn helper() {\n    one();\n}\n";
6425        let input = Zeta2PromptInput {
6426            cursor_path: Path::new("test.rs").into(),
6427            cursor_excerpt: current_text.into(),
6428            cursor_offset_in_excerpt: current_text.find("helper").unwrap(),
6429            excerpt_start_row: Some(0),
6430            events: Vec::new(),
6431            related_files: Some(vec![
6432                RelatedFile {
6433                    path: Path::new("test.rs").into(),
6434                    max_row: 3,
6435                    excerpts: vec![RelatedExcerpt {
6436                        row_range: 0..3,
6437                        text: current_text.into(),
6438                        order: 0,
6439                        context_source: ContextSource::CurrentFile,
6440                    }],
6441                    in_open_source_repo: false,
6442                },
6443                RelatedFile {
6444                    path: Path::new("helper.rs").into(),
6445                    max_row: 13,
6446                    excerpts: vec![RelatedExcerpt {
6447                        row_range: 10..13,
6448                        text: helper_text.into(),
6449                        order: 1,
6450                        context_source: ContextSource::EditHistory,
6451                    }],
6452                    in_open_source_repo: false,
6453                },
6454            ]),
6455            active_buffer_diagnostics: vec![],
6456            excerpt_ranges: ExcerptRanges::default(),
6457            syntax_ranges: None,
6458            in_open_source_repo: false,
6459            can_collect_data: false,
6460            repo_url: None,
6461        };
6462        let patch = indoc! {"
6463            --- a/helper.rs
6464            +++ b/helper.rs
6465            @@ -11,3 +11,3 @@
6466             fn helper() {
6467            -    one();
6468            +    two();
6469             }
6470        "};
6471
6472        let output =
6473            format_expected_output(&input, ZetaFormat::V0615HashRegions, patch, None).unwrap();
6474        let parsed_patch =
6475            parse_zeta2_model_output_as_patch(&output, ZetaFormat::V0615HashRegions, &input)
6476                .unwrap();
6477
6478        assert!(output.contains(hashed_regions::MARKER_TAG_PREFIX));
6479        assert!(
6480            parsed_patch.contains("-    one();"),
6481            "patch: {parsed_patch}"
6482        );
6483        assert!(
6484            parsed_patch.contains("+    two();"),
6485            "patch: {parsed_patch}"
6486        );
6487    }
6488
6489    #[test]
6490    fn test_zeta3_prompt_matches_zeta2_seed_multi_region_prompt() {
6491        let excerpt = "fn main() {\n    helper();\n}\n";
6492        let cursor_offset = excerpt.find("helper").expect("cursor text exists") + "help".len();
6493        let cursor_row_start = excerpt.find("    helper").expect("cursor row exists");
6494        let syntax_ranges = vec![0..excerpt.len()];
6495        let related_file = make_related_file("related.rs", "fn helper() {}\n");
6496        let mut zeta2_input = make_input(
6497            excerpt,
6498            0..excerpt.len(),
6499            cursor_offset,
6500            vec![make_event("test.rs", "-old\n+new\n")],
6501            vec![related_file.clone()],
6502        );
6503        zeta2_input.excerpt_start_row = Some(10);
6504        zeta2_input.excerpt_ranges =
6505            compute_legacy_excerpt_ranges(excerpt, cursor_offset, &syntax_ranges);
6506        zeta2_input.syntax_ranges = Some(syntax_ranges.clone());
6507
6508        let zeta3_input = Zeta3PromptInput {
6509            cursor_path: Path::new("test.rs").into(),
6510            cursor_position: FilePosition {
6511                row: 11,
6512                column: (cursor_offset - cursor_row_start) as u32,
6513            },
6514            events: vec![Arc::new(make_event("test.rs", "-old\n+new\n"))],
6515            editable_context: vec![
6516                RelatedFile {
6517                    path: Path::new("test.rs").into(),
6518                    max_row: 12,
6519                    excerpts: vec![RelatedExcerpt {
6520                        row_range: 10..12,
6521                        text: excerpt.into(),
6522                        order: 0,
6523                        context_source: ContextSource::CurrentFile,
6524                    }],
6525                    in_open_source_repo: false,
6526                },
6527                related_file,
6528            ],
6529            syntax_ranges,
6530            active_buffer_diagnostics: vec![],
6531            in_open_source_repo: false,
6532            can_collect_data: false,
6533            repo_url: None,
6534        };
6535
6536        assert_eq!(
6537            format_zeta3_prompt(&zeta3_input, ZetaFormat::V0318SeedMultiRegions),
6538            format_zeta_prompt(&zeta2_input, ZetaFormat::V0318SeedMultiRegions),
6539        );
6540    }
6541
6542    #[test]
6543    fn test_zeta3_parse_seed_multi_region_output_as_patch() {
6544        let prefix = (0..20)
6545            .map(|index| format!("prefix {index}\n"))
6546            .collect::<String>();
6547        let excerpt = "fn main() {\n    let value = 1;\n    dbg!(value);\n}\n";
6548        let new_excerpt = "fn main() {\n    let value = 2;\n    dbg!(value);\n}\n";
6549        let cursor_offset = excerpt.find("value =").expect("cursor text exists");
6550        let input = Zeta3PromptInput {
6551            cursor_path: Path::new("test.rs").into(),
6552            cursor_position: FilePosition { row: 21, column: 8 },
6553            events: vec![],
6554            editable_context: vec![RelatedFile {
6555                path: Path::new("test.rs").into(),
6556                max_row: 24,
6557                excerpts: vec![RelatedExcerpt {
6558                    row_range: 20..24,
6559                    text: excerpt.into(),
6560                    order: 0,
6561                    context_source: ContextSource::CurrentFile,
6562                }],
6563                in_open_source_repo: false,
6564            }],
6565            syntax_ranges: vec![0..excerpt.len()],
6566            active_buffer_diagnostics: vec![],
6567            in_open_source_repo: false,
6568            can_collect_data: false,
6569            repo_url: None,
6570        };
6571        let output = multi_region::encode_from_old_and_new_v0318(
6572            excerpt,
6573            new_excerpt,
6574            Some(cursor_offset),
6575            CURSOR_MARKER,
6576            multi_region::V0318_END_MARKER,
6577        )
6578        .unwrap();
6579
6580        let patch =
6581            parse_zeta3_model_output_as_patch(&output, ZetaFormat::V0318SeedMultiRegions, &input)
6582                .unwrap();
6583        let full_text = format!("{prefix}{excerpt}");
6584        let expected = format!("{prefix}{new_excerpt}");
6585
6586        assert!(patch.contains(CURSOR_MARKER));
6587        assert_eq!(
6588            udiff::apply_diff_to_string(&patch.replace(CURSOR_MARKER, ""), &full_text).unwrap(),
6589            expected
6590        );
6591    }
6592
6593    #[test]
6594    fn test_seed_coder_no_context() {
6595        let input = make_input("before\nmiddle\nafter", 7..13, 10, vec![], vec![]);
6596
6597        assert_eq!(
6598            format_seed_coder(&input),
6599            indoc! {r#"
6600                <[fim-suffix]>
6601                after
6602                <[fim-prefix]><filename>test.rs
6603                before
6604                <<<<<<< CURRENT
6605                mid<|user_cursor|>dle
6606                =======
6607                <[fim-middle]>"#}
6608        );
6609    }
6610
6611    #[test]
6612    fn test_seed_coder_truncation_drops_context() {
6613        let input = make_input(
6614            "code",
6615            0..4,
6616            2,
6617            vec![make_event("a.rs", "-x\n+y\n")],
6618            vec![make_related_file("r1.rs", "content\n")],
6619        );
6620
6621        // With large budget, everything is included
6622        assert_eq!(
6623            format_seed_coder(&input),
6624            indoc! {r#"
6625                <[fim-suffix]>
6626                <[fim-prefix]><filename>r1.rs
6627                content
6628
6629                <filename>edit_history
6630                --- a/a.rs
6631                +++ b/a.rs
6632                -x
6633                +y
6634
6635                <filename>test.rs
6636                <<<<<<< CURRENT
6637                co<|user_cursor|>de
6638                =======
6639                <[fim-middle]>"#}
6640        );
6641
6642        assert_eq!(
6643            format_prompt_with_budget_for_format(&input, ZetaFormat::V0211SeedCoder, 24),
6644            None
6645        );
6646
6647        assert_eq!(
6648            format_seed_coder_with_budget(&input, 40),
6649            indoc! {r#"
6650                <[fim-suffix]>
6651                <[fim-prefix]><filename>test.rs
6652                <<<<<<< CURRENT
6653                co<|user_cursor|>de
6654                =======
6655                <[fim-middle]>"#
6656            }
6657        )
6658    }
6659
6660    #[test]
6661    fn test_seed_coder_truncation_prioritizes_lower_order() {
6662        let input = make_input(
6663            "code",
6664            0..4,
6665            2,
6666            vec![],
6667            vec![
6668                RelatedFile {
6669                    path: Path::new("low_prio.rs").into(),
6670                    max_row: 5,
6671                    in_open_source_repo: false,
6672                    excerpts: vec![RelatedExcerpt {
6673                        row_range: 0..5,
6674                        text: "low prio\n".into(),
6675                        order: 10,
6676                        context_source: ContextSource::Lsp,
6677                    }],
6678                },
6679                RelatedFile {
6680                    path: Path::new("high_prio.rs").into(),
6681                    max_row: 5,
6682                    in_open_source_repo: false,
6683                    excerpts: vec![RelatedExcerpt {
6684                        row_range: 0..5,
6685                        text: "high prio\n".into(),
6686                        order: 1,
6687                        context_source: ContextSource::Lsp,
6688                    }],
6689                },
6690            ],
6691        );
6692
6693        // With large budget, both included; rendered in stable lexicographic order.
6694        assert_eq!(
6695            format_seed_coder(&input),
6696            indoc! {r#"
6697                <[fim-suffix]>
6698                <[fim-prefix]><filename>low_prio.rs
6699                low prio
6700                <filename>high_prio.rs
6701                high prio
6702
6703                <filename>test.rs
6704                <<<<<<< CURRENT
6705                co<|user_cursor|>de
6706                =======
6707                <[fim-middle]>"#}
6708        );
6709
6710        // With tight budget under the generic heuristic, context is dropped but the
6711        // minimal cursor section still fits.
6712        assert_eq!(
6713            format_prompt_with_budget_for_format(&input, ZetaFormat::V0211SeedCoder, 44),
6714            Some(
6715                indoc! {r#"
6716                    <[fim-suffix]>
6717                    <[fim-prefix]><filename>test.rs
6718                    <<<<<<< CURRENT
6719                    co<|user_cursor|>de
6720                    =======
6721                    <[fim-middle]>"#}
6722                .to_string()
6723            )
6724        );
6725    }
6726
6727    #[test]
6728    fn test_format_zeta1_from_input_basic() {
6729        let excerpt = "fn before() {}\nfn foo() {\n    let x = 1;\n}\nfn after() {}\n";
6730        let input = Zeta2PromptInput {
6731            cursor_path: Path::new("src/main.rs").into(),
6732            cursor_excerpt: excerpt.into(),
6733            cursor_offset_in_excerpt: 30,
6734            excerpt_start_row: Some(0),
6735            events: vec![Arc::new(make_event("other.rs", "-old\n+new\n"))],
6736            related_files: Some(vec![]),
6737            active_buffer_diagnostics: vec![],
6738            excerpt_ranges: ExcerptRanges {
6739                editable_150: 15..41,
6740                editable_180: 15..41,
6741                editable_350: 15..41,
6742                editable_150_context_350: 0..excerpt.len(),
6743                editable_180_context_350: 0..excerpt.len(),
6744                editable_350_context_150: 0..excerpt.len(),
6745                ..Default::default()
6746            },
6747            syntax_ranges: None,
6748            in_open_source_repo: false,
6749            can_collect_data: false,
6750            repo_url: None,
6751        };
6752
6753        let prompt = zeta1::format_zeta1_from_input(&input, 15..41, 0..excerpt.len());
6754
6755        assert_eq!(
6756            prompt,
6757            concat!(
6758                "### Instruction:\n",
6759                "You are a code completion assistant and your task is to analyze user edits and then rewrite an ",
6760                "excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking ",
6761                "into account the cursor location.\n",
6762                "\n",
6763                "### User Edits:\n",
6764                "\n",
6765                "User edited other.rs:\n",
6766                "```diff\n",
6767                "-old\n",
6768                "+new\n",
6769                "\n",
6770                "```\n",
6771                "\n",
6772                "### User Excerpt:\n",
6773                "\n",
6774                "```src/main.rs\n",
6775                "<|start_of_file|>\n",
6776                "fn before() {}\n",
6777                "<|editable_region_start|>\n",
6778                "fn foo() {\n",
6779                "    <|user_cursor_is_here|>let x = 1;\n",
6780                "\n",
6781                "<|editable_region_end|>}\n",
6782                "fn after() {}\n",
6783                "\n",
6784                "```\n",
6785                "\n",
6786                "### Response:\n",
6787            ),
6788        );
6789    }
6790
6791    #[test]
6792    fn test_format_zeta1_from_input_no_start_of_file() {
6793        let excerpt = "fn foo() {\n    let x = 1;\n}\n";
6794        let input = Zeta2PromptInput {
6795            cursor_path: Path::new("src/main.rs").into(),
6796            cursor_excerpt: excerpt.into(),
6797            cursor_offset_in_excerpt: 15,
6798            excerpt_start_row: Some(10),
6799            events: vec![],
6800            related_files: Some(vec![]),
6801            active_buffer_diagnostics: vec![],
6802            excerpt_ranges: ExcerptRanges {
6803                editable_150: 0..28,
6804                editable_180: 0..28,
6805                editable_350: 0..28,
6806                editable_150_context_350: 0..28,
6807                editable_180_context_350: 0..28,
6808                editable_350_context_150: 0..28,
6809                ..Default::default()
6810            },
6811            syntax_ranges: None,
6812            in_open_source_repo: false,
6813            can_collect_data: false,
6814            repo_url: None,
6815        };
6816
6817        let prompt = zeta1::format_zeta1_from_input(&input, 0..28, 0..28);
6818
6819        assert_eq!(
6820            prompt,
6821            concat!(
6822                "### Instruction:\n",
6823                "You are a code completion assistant and your task is to analyze user edits and then rewrite an ",
6824                "excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking ",
6825                "into account the cursor location.\n",
6826                "\n",
6827                "### User Edits:\n",
6828                "\n",
6829                "\n",
6830                "\n",
6831                "### User Excerpt:\n",
6832                "\n",
6833                "```src/main.rs\n",
6834                "<|editable_region_start|>\n",
6835                "fn foo() {\n",
6836                "    <|user_cursor_is_here|>let x = 1;\n",
6837                "}\n",
6838                "\n",
6839                "<|editable_region_end|>\n",
6840                "```\n",
6841                "\n",
6842                "### Response:\n",
6843            ),
6844        );
6845    }
6846
6847    #[test]
6848    fn test_format_zeta1_from_input_with_sub_ranges() {
6849        let excerpt = "// prefix\nfn foo() {\n    let x = 1;\n}\n// suffix\n";
6850        let editable_range = 10..37;
6851        let context_range = 0..excerpt.len();
6852
6853        let input = Zeta2PromptInput {
6854            cursor_path: Path::new("test.rs").into(),
6855            cursor_excerpt: excerpt.into(),
6856            cursor_offset_in_excerpt: 25,
6857            excerpt_start_row: Some(0),
6858            events: vec![],
6859            related_files: Some(vec![]),
6860            active_buffer_diagnostics: vec![],
6861            excerpt_ranges: ExcerptRanges {
6862                editable_150: editable_range.clone(),
6863                editable_180: editable_range.clone(),
6864                editable_350: editable_range.clone(),
6865                editable_150_context_350: context_range.clone(),
6866                editable_180_context_350: context_range.clone(),
6867                editable_350_context_150: context_range.clone(),
6868                ..Default::default()
6869            },
6870            syntax_ranges: None,
6871            in_open_source_repo: false,
6872            can_collect_data: false,
6873            repo_url: None,
6874        };
6875
6876        let prompt = zeta1::format_zeta1_from_input(&input, editable_range, context_range);
6877
6878        assert_eq!(
6879            prompt,
6880            concat!(
6881                "### Instruction:\n",
6882                "You are a code completion assistant and your task is to analyze user edits and then rewrite an ",
6883                "excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking ",
6884                "into account the cursor location.\n",
6885                "\n",
6886                "### User Edits:\n",
6887                "\n",
6888                "\n",
6889                "\n",
6890                "### User Excerpt:\n",
6891                "\n",
6892                "```test.rs\n",
6893                "<|start_of_file|>\n",
6894                "// prefix\n",
6895                "<|editable_region_start|>\n",
6896                "fn foo() {\n",
6897                "    <|user_cursor_is_here|>let x = 1;\n",
6898                "}\n",
6899                "<|editable_region_end|>\n",
6900                "// suffix\n",
6901                "\n",
6902                "```\n",
6903                "\n",
6904                "### Response:\n",
6905            ),
6906        );
6907    }
6908
6909    #[test]
6910    fn test_max_event_count() {
6911        fn make_numbered_event(index: usize) -> Event {
6912            return make_event(
6913                &format!("event-{index}.rs"),
6914                &format!("-old-{index}\n+new-{index}\n"),
6915            );
6916        }
6917        let input = make_input(
6918            "x",
6919            0..1,
6920            0,
6921            (0..3).map(make_numbered_event).collect(),
6922            vec![],
6923        );
6924
6925        let edit_history_section = format_edit_history_within_budget(
6926            &input.events,
6927            "<|file_sep|>",
6928            "edit history",
6929            usize::MAX,
6930            5,
6931        );
6932
6933        assert_eq!(
6934            &edit_history_section,
6935            indoc!(
6936                "
6937                <|file_sep|>edit history
6938                --- a/event-0.rs
6939                +++ b/event-0.rs
6940                -old-0
6941                +new-0
6942                --- a/event-1.rs
6943                +++ b/event-1.rs
6944                -old-1
6945                +new-1
6946                --- a/event-2.rs
6947                +++ b/event-2.rs
6948                -old-2
6949                +new-2
6950            "
6951            )
6952        );
6953
6954        let edit_history_section = format_edit_history_within_budget(
6955            &input.events,
6956            "<|file_sep|>",
6957            "edit history",
6958            usize::MAX,
6959            2,
6960        );
6961
6962        assert_eq!(
6963            &edit_history_section,
6964            indoc!(
6965                "
6966                <|file_sep|>edit history
6967                --- a/event-1.rs
6968                +++ b/event-1.rs
6969                -old-1
6970                +new-1
6971                --- a/event-2.rs
6972                +++ b/event-2.rs
6973                -old-2
6974                +new-2
6975            "
6976            )
6977        );
6978
6979        let edit_history_section = format_edit_history_within_budget(
6980            &input.events,
6981            "<|file_sep|>",
6982            "edit history",
6983            usize::MAX,
6984            0,
6985        );
6986
6987        assert_eq!(&edit_history_section, "");
6988    }
6989
6990    #[test]
6991    fn test_clean_zeta1_model_output_basic() {
6992        let output = indoc! {"
6993            <|editable_region_start|>
6994            fn main() {
6995                println!(\"hello\");
6996            }
6997            <|editable_region_end|>
6998        "};
6999
7000        let cleaned = zeta1::clean_zeta1_model_output(output).unwrap();
7001        assert_eq!(cleaned, "fn main() {\n    println!(\"hello\");\n}");
7002    }
7003
7004    #[test]
7005    fn test_clean_zeta1_model_output_with_cursor() {
7006        let output = indoc! {"
7007            <|editable_region_start|>
7008            fn main() {
7009                <|user_cursor_is_here|>println!(\"hello\");
7010            }
7011            <|editable_region_end|>
7012        "};
7013
7014        let cleaned = zeta1::clean_zeta1_model_output(output).unwrap();
7015        assert_eq!(
7016            cleaned,
7017            "fn main() {\n    <|user_cursor|>println!(\"hello\");\n}"
7018        );
7019    }
7020
7021    #[test]
7022    fn test_clean_zeta1_model_output_no_markers() {
7023        let output = "fn main() {}\n";
7024        let cleaned = zeta1::clean_zeta1_model_output(output).unwrap();
7025        assert_eq!(cleaned, "fn main() {}\n");
7026    }
7027
7028    #[test]
7029    fn test_clean_zeta1_model_output_empty_region() {
7030        let output = "<|editable_region_start|>\n<|editable_region_end|>\n";
7031        let cleaned = zeta1::clean_zeta1_model_output(output).unwrap();
7032        assert_eq!(cleaned, "");
7033    }
7034
7035    fn apply_edit(excerpt: &str, parsed_output: &ParsedOutput) -> String {
7036        let mut result = excerpt.to_string();
7037        result.replace_range(
7038            parsed_output.range_in_excerpt.clone(),
7039            &parsed_output.new_editable_region,
7040        );
7041        result
7042    }
7043
7044    #[test]
7045    fn test_parse_zeta2_model_output() {
7046        let excerpt = "before ctx\nctx start\neditable old\nctx end\nafter ctx\n";
7047        let context_start = excerpt.find("ctx start").unwrap();
7048        let context_end = excerpt.find("after ctx").unwrap();
7049        let editable_start = excerpt.find("editable old").unwrap();
7050        let editable_end = editable_start + "editable old\n".len();
7051        let input = make_input_with_context_range(
7052            excerpt,
7053            editable_start..editable_end,
7054            context_start..context_end,
7055            editable_start,
7056        );
7057
7058        let output = parse_zeta2_model_output(
7059            "editable new\n>>>>>>> UPDATED\n",
7060            ZetaFormat::V0131GitMergeMarkersPrefix,
7061            &input,
7062        )
7063        .unwrap();
7064
7065        assert_eq!(
7066            apply_edit(excerpt, &output),
7067            "before ctx\nctx start\neditable new\nctx end\nafter ctx\n"
7068        );
7069    }
7070
7071    #[test]
7072    fn test_parse_zeta2_model_output_identity() {
7073        let excerpt = "aaa\nbbb\nccc\nddd\neee\n";
7074        let editable_start = excerpt.find("bbb").unwrap();
7075        let editable_end = excerpt.find("ddd").unwrap();
7076        let input = make_input_with_context_range(
7077            excerpt,
7078            editable_start..editable_end,
7079            0..excerpt.len(),
7080            editable_start,
7081        );
7082
7083        let format = ZetaFormat::V0131GitMergeMarkersPrefix;
7084        let output =
7085            parse_zeta2_model_output("bbb\nccc\n>>>>>>> UPDATED\n", format, &input).unwrap();
7086
7087        assert_eq!(apply_edit(excerpt, &output), excerpt);
7088    }
7089
7090    #[test]
7091    fn test_parse_zeta2_model_output_strips_end_marker() {
7092        let excerpt = "hello\nworld\n";
7093        let input = make_input_with_context_range(excerpt, 0..excerpt.len(), 0..excerpt.len(), 0);
7094
7095        let format = ZetaFormat::V0131GitMergeMarkersPrefix;
7096        let output1 =
7097            parse_zeta2_model_output("new content\n>>>>>>> UPDATED\n", format, &input).unwrap();
7098        let output2 = parse_zeta2_model_output("new content\n", format, &input).unwrap();
7099
7100        assert_eq!(apply_edit(excerpt, &output1), apply_edit(excerpt, &output2));
7101        assert_eq!(apply_edit(excerpt, &output1), "new content\n");
7102    }
7103
7104    #[test]
7105    fn test_parsed_output_to_patch_round_trips_through_udiff_application() {
7106        let excerpt = "before ctx\nctx start\neditable old\nctx end\nafter ctx\n";
7107        let context_start = excerpt.find("ctx start").unwrap();
7108        let context_end = excerpt.find("after ctx").unwrap();
7109        let editable_start = excerpt.find("editable old").unwrap();
7110        let editable_end = editable_start + "editable old\n".len();
7111        let input = make_input_with_context_range(
7112            excerpt,
7113            editable_start..editable_end,
7114            context_start..context_end,
7115            editable_start,
7116        );
7117
7118        let parsed = parse_zeta2_model_output(
7119            "editable new\n>>>>>>> UPDATED\n",
7120            ZetaFormat::V0131GitMergeMarkersPrefix,
7121            &input,
7122        )
7123        .unwrap();
7124        let expected = apply_edit(excerpt, &parsed);
7125        let patch = parsed_output_to_patch(&input, parsed).unwrap();
7126        let patched = udiff::apply_diff_to_string(&patch, excerpt).unwrap();
7127
7128        assert_eq!(patched, expected);
7129    }
7130
7131    #[test]
7132    fn test_special_tokens_not_triggered_by_comment_separator() {
7133        // Regression test for https://github.com/zed-industries/zed/issues/52489
7134        let excerpt = "fn main() {\n    // =======\n    println!(\"hello\");\n}\n";
7135        let input = make_input(excerpt, 0..excerpt.len(), 0, vec![], vec![]);
7136        assert!(
7137            !prompt_input_contains_special_tokens(&input, ZetaFormat::V0131GitMergeMarkersPrefix),
7138            "comment containing ======= should not trigger special token detection"
7139        );
7140    }
7141}
7142
Served at tenant.openagents/omega Member data and write actions are omitted.