Also: - Add two evals - Remove duplicated Example 6 from the teacher prompt Release Notes: - N/A
1002 lines
34 KiB
Rust
1002 lines
34 KiB
Rust
use crate::udiff::DiffLine;
|
|
use anyhow::{Context as _, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::{borrow::Cow, fmt::Write as _, mem, ops::Range, path::Path, sync::Arc};
|
|
use telemetry_events::EditPredictionRating;
|
|
|
|
pub const CURSOR_POSITION_MARKER: &str = "[CURSOR_POSITION]";
|
|
pub const INLINE_CURSOR_MARKER: &str = "<|user_cursor|>";
|
|
|
|
/// Maximum cursor file size to capture (64KB).
|
|
/// Files larger than this will not have their content captured,
|
|
/// falling back to git-based loading.
|
|
pub const MAX_CURSOR_FILE_SIZE: usize = 64 * 1024;
|
|
|
|
/// Encodes a cursor position into a diff patch by adding a comment line with a caret
|
|
/// pointing to the cursor column.
|
|
///
|
|
/// The cursor offset is relative to the start of the new text content (additions and context lines).
|
|
/// Returns the patch with cursor marker comment lines inserted after the relevant addition line.
|
|
pub fn encode_cursor_in_patch(patch: &str, cursor_offset: Option<usize>) -> String {
|
|
let Some(cursor_offset) = cursor_offset else {
|
|
return patch.to_string();
|
|
};
|
|
|
|
let mut result = String::new();
|
|
let mut line_start_offset = 0usize;
|
|
|
|
for line in patch.lines() {
|
|
if !result.is_empty() {
|
|
result.push('\n');
|
|
}
|
|
result.push_str(line);
|
|
|
|
match DiffLine::parse(line) {
|
|
DiffLine::Addition(content) => {
|
|
let line_end_offset = line_start_offset + content.len();
|
|
|
|
if cursor_offset >= line_start_offset && cursor_offset <= line_end_offset {
|
|
let cursor_column = cursor_offset - line_start_offset;
|
|
|
|
result.push('\n');
|
|
result.push('#');
|
|
for _ in 0..cursor_column {
|
|
result.push(' ');
|
|
}
|
|
write!(result, "^{}", CURSOR_POSITION_MARKER).unwrap();
|
|
}
|
|
|
|
line_start_offset = line_end_offset + 1;
|
|
}
|
|
DiffLine::Context(content) => {
|
|
line_start_offset += content.len() + 1;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
if patch.ends_with('\n') {
|
|
result.push('\n');
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
|
|
pub struct ExampleSpec {
|
|
#[serde(default)]
|
|
pub name: String,
|
|
pub repository_url: String,
|
|
pub revision: String,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub tags: Vec<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub reasoning: Option<String>,
|
|
#[serde(default)]
|
|
pub uncommitted_diff: String,
|
|
pub cursor_path: Arc<Path>,
|
|
pub cursor_position: String,
|
|
pub edit_history: String,
|
|
pub expected_patches: Vec<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub rejected_patch: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub captured_prompt_input: Option<CapturedPromptInput>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub telemetry: Option<TelemetrySource>,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub human_feedback: Vec<HumanFeedback>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub rating: Option<EditPredictionRating>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
|
|
pub struct HumanFeedback {
|
|
pub message: String,
|
|
}
|
|
|
|
/// Metadata for examples sourced from production telemetry (rejected predictions).
|
|
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
|
|
pub struct TelemetrySource {
|
|
pub request_id: String,
|
|
pub device_id: String,
|
|
pub time: String,
|
|
pub rejection_reason: String,
|
|
pub was_shown: bool,
|
|
}
|
|
|
|
/// All data needed to run format_prompt without loading the project.
|
|
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
|
|
pub struct CapturedPromptInput {
|
|
pub cursor_file_content: String,
|
|
pub cursor_offset: usize,
|
|
pub cursor_row: u32,
|
|
pub cursor_column: u32,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub excerpt_start_row: Option<u32>,
|
|
pub events: Vec<CapturedEvent>,
|
|
pub related_files: Vec<CapturedRelatedFile>,
|
|
#[serde(default)]
|
|
pub in_open_source_repo: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub zed_version: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
|
|
pub struct CapturedEvent {
|
|
pub path: Arc<Path>,
|
|
pub old_path: Arc<Path>,
|
|
pub diff: String,
|
|
pub predicted: bool,
|
|
#[serde(default)]
|
|
pub in_open_source_repo: bool,
|
|
}
|
|
|
|
impl CapturedEvent {
|
|
pub fn to_event(&self) -> zeta_prompt::Event {
|
|
zeta_prompt::Event::BufferChange {
|
|
path: self.path.clone(),
|
|
old_path: self.old_path.clone(),
|
|
diff: self.diff.clone(),
|
|
predicted: self.predicted,
|
|
in_open_source_repo: self.in_open_source_repo,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
|
|
pub struct CapturedRelatedFile {
|
|
pub path: Arc<Path>,
|
|
pub max_row: u32,
|
|
pub excerpts: Vec<CapturedRelatedExcerpt>,
|
|
}
|
|
|
|
impl CapturedRelatedFile {
|
|
pub fn to_related_file(&self) -> zeta_prompt::RelatedFile {
|
|
zeta_prompt::RelatedFile {
|
|
path: self.path.clone(),
|
|
max_row: self.max_row,
|
|
in_open_source_repo: false,
|
|
excerpts: self
|
|
.excerpts
|
|
.iter()
|
|
.map(|e| zeta_prompt::RelatedExcerpt {
|
|
row_range: e.row_range.clone(),
|
|
text: e.text.clone().into(),
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)]
|
|
pub struct CapturedRelatedExcerpt {
|
|
pub row_range: Range<u32>,
|
|
pub text: String,
|
|
}
|
|
|
|
const REASONING_HEADING: &str = "Reasoning";
|
|
const UNCOMMITTED_DIFF_HEADING: &str = "Uncommitted Diff";
|
|
const EDIT_HISTORY_HEADING: &str = "Edit History";
|
|
const CURSOR_POSITION_HEADING: &str = "Cursor Position";
|
|
const EXPECTED_PATCH_HEADING: &str = "Expected Patch";
|
|
const REJECTED_PATCH_HEADING: &str = "Rejected Patch";
|
|
const ACCEPTED_PREDICTION_MARKER: &str = "// User accepted prediction:";
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct FrontMatter<'a> {
|
|
repository_url: Cow<'a, str>,
|
|
revision: Cow<'a, str>,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
tags: Vec<String>,
|
|
}
|
|
|
|
impl ExampleSpec {
|
|
/// Generate a sanitized filename for this example.
|
|
pub fn filename(&self) -> String {
|
|
self.name
|
|
.chars()
|
|
.map(|c| match c {
|
|
' ' | ':' | '~' | '^' | '?' | '*' | '[' | '\\' | '@' | '{' | '/' | '<' | '>'
|
|
| '|' | '"' => '-',
|
|
c => c,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Format this example spec as markdown.
|
|
pub fn to_markdown(&self) -> String {
|
|
use std::fmt::Write as _;
|
|
|
|
let front_matter = FrontMatter {
|
|
repository_url: Cow::Borrowed(&self.repository_url),
|
|
revision: Cow::Borrowed(&self.revision),
|
|
tags: self.tags.clone(),
|
|
};
|
|
let front_matter_toml =
|
|
toml::to_string_pretty(&front_matter).unwrap_or_else(|_| String::new());
|
|
|
|
let mut markdown = String::new();
|
|
|
|
_ = writeln!(markdown, "+++");
|
|
markdown.push_str(&front_matter_toml);
|
|
if !markdown.ends_with('\n') {
|
|
markdown.push('\n');
|
|
}
|
|
_ = writeln!(markdown, "+++");
|
|
markdown.push('\n');
|
|
|
|
_ = writeln!(markdown, "# {}", self.name);
|
|
markdown.push('\n');
|
|
|
|
if let Some(reasoning) = &self.reasoning {
|
|
_ = writeln!(markdown, "## {}", REASONING_HEADING);
|
|
markdown.push('\n');
|
|
markdown.push_str(reasoning);
|
|
if !markdown.ends_with('\n') {
|
|
markdown.push('\n');
|
|
}
|
|
markdown.push('\n');
|
|
}
|
|
|
|
if !self.uncommitted_diff.is_empty() {
|
|
_ = writeln!(markdown, "## {}", UNCOMMITTED_DIFF_HEADING);
|
|
_ = writeln!(markdown);
|
|
_ = writeln!(markdown, "```diff");
|
|
markdown.push_str(&self.uncommitted_diff);
|
|
if !markdown.ends_with('\n') {
|
|
markdown.push('\n');
|
|
}
|
|
_ = writeln!(markdown, "```");
|
|
markdown.push('\n');
|
|
}
|
|
|
|
_ = writeln!(markdown, "## {}", EDIT_HISTORY_HEADING);
|
|
_ = writeln!(markdown);
|
|
|
|
if self.edit_history.is_empty() {
|
|
_ = writeln!(markdown, "(No edit history)");
|
|
_ = writeln!(markdown);
|
|
} else {
|
|
_ = writeln!(markdown, "```diff");
|
|
markdown.push_str(&self.edit_history);
|
|
if !markdown.ends_with('\n') {
|
|
markdown.push('\n');
|
|
}
|
|
_ = writeln!(markdown, "```");
|
|
markdown.push('\n');
|
|
}
|
|
|
|
_ = writeln!(markdown, "## {}", CURSOR_POSITION_HEADING);
|
|
_ = writeln!(markdown);
|
|
_ = writeln!(markdown, "```{}", self.cursor_path.to_string_lossy());
|
|
markdown.push_str(&self.cursor_position);
|
|
if !markdown.ends_with('\n') {
|
|
markdown.push('\n');
|
|
}
|
|
_ = writeln!(markdown, "```");
|
|
markdown.push('\n');
|
|
|
|
_ = writeln!(markdown, "## {}", EXPECTED_PATCH_HEADING);
|
|
markdown.push('\n');
|
|
for patch in &self.expected_patches {
|
|
_ = writeln!(markdown, "```diff");
|
|
markdown.push_str(patch);
|
|
if !markdown.ends_with('\n') {
|
|
markdown.push('\n');
|
|
}
|
|
_ = writeln!(markdown, "```");
|
|
markdown.push('\n');
|
|
}
|
|
|
|
if let Some(rejected_patch) = &self.rejected_patch {
|
|
_ = writeln!(markdown, "## {}", REJECTED_PATCH_HEADING);
|
|
markdown.push('\n');
|
|
_ = writeln!(markdown, "```diff");
|
|
markdown.push_str(rejected_patch);
|
|
if !markdown.ends_with('\n') {
|
|
markdown.push('\n');
|
|
}
|
|
_ = writeln!(markdown, "```");
|
|
markdown.push('\n');
|
|
}
|
|
|
|
markdown
|
|
}
|
|
|
|
/// Parse an example spec from markdown.
|
|
pub fn from_markdown(mut input: &str) -> anyhow::Result<Self> {
|
|
use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Parser, Tag, TagEnd};
|
|
|
|
let mut spec = ExampleSpec {
|
|
name: String::new(),
|
|
repository_url: String::new(),
|
|
revision: String::new(),
|
|
tags: Vec::new(),
|
|
reasoning: None,
|
|
uncommitted_diff: String::new(),
|
|
cursor_path: Path::new("").into(),
|
|
cursor_position: String::new(),
|
|
edit_history: String::new(),
|
|
expected_patches: Vec::new(),
|
|
rejected_patch: None,
|
|
captured_prompt_input: None,
|
|
telemetry: None,
|
|
human_feedback: Vec::new(),
|
|
rating: None,
|
|
};
|
|
|
|
if let Some(rest) = input.strip_prefix("+++\n")
|
|
&& let Some((front_matter, rest)) = rest.split_once("+++\n")
|
|
{
|
|
if let Ok(data) = toml::from_str::<FrontMatter<'_>>(front_matter) {
|
|
spec.repository_url = data.repository_url.into_owned();
|
|
spec.revision = data.revision.into_owned();
|
|
spec.tags = data.tags;
|
|
}
|
|
input = rest.trim_start();
|
|
}
|
|
|
|
let parser = Parser::new(input);
|
|
let mut text = String::new();
|
|
let mut block_info: CowStr = "".into();
|
|
|
|
#[derive(PartialEq)]
|
|
enum Section {
|
|
Start,
|
|
UncommittedDiff,
|
|
EditHistory,
|
|
CursorPosition,
|
|
ExpectedPatch,
|
|
RejectedPatch,
|
|
Other,
|
|
}
|
|
|
|
let mut current_section = Section::Start;
|
|
let mut next_edit_predicted = false;
|
|
|
|
for event in parser {
|
|
match event {
|
|
Event::Text(line) => {
|
|
text.push_str(&line);
|
|
}
|
|
Event::End(TagEnd::Heading(HeadingLevel::H1)) => {
|
|
spec.name = mem::take(&mut text);
|
|
}
|
|
Event::End(TagEnd::Heading(HeadingLevel::H2)) => {
|
|
let title = mem::take(&mut text);
|
|
current_section = if title.eq_ignore_ascii_case(UNCOMMITTED_DIFF_HEADING) {
|
|
Section::UncommittedDiff
|
|
} else if title.eq_ignore_ascii_case(EDIT_HISTORY_HEADING) {
|
|
Section::EditHistory
|
|
} else if title.eq_ignore_ascii_case(CURSOR_POSITION_HEADING) {
|
|
Section::CursorPosition
|
|
} else if title.eq_ignore_ascii_case(EXPECTED_PATCH_HEADING) {
|
|
Section::ExpectedPatch
|
|
} else if title.eq_ignore_ascii_case(REJECTED_PATCH_HEADING) {
|
|
Section::RejectedPatch
|
|
} else {
|
|
Section::Other
|
|
};
|
|
}
|
|
Event::End(TagEnd::Heading(HeadingLevel::H3)) => {
|
|
mem::take(&mut text);
|
|
}
|
|
Event::End(TagEnd::Heading(HeadingLevel::H4)) => {
|
|
mem::take(&mut text);
|
|
}
|
|
Event::End(TagEnd::Heading(level)) => {
|
|
anyhow::bail!("Unexpected heading level: {level}");
|
|
}
|
|
Event::Start(Tag::CodeBlock(kind)) => {
|
|
if current_section == Section::EditHistory
|
|
&& text.trim() == ACCEPTED_PREDICTION_MARKER
|
|
{
|
|
next_edit_predicted = true;
|
|
}
|
|
text.clear();
|
|
match kind {
|
|
CodeBlockKind::Fenced(info) => {
|
|
block_info = info;
|
|
}
|
|
CodeBlockKind::Indented => {
|
|
anyhow::bail!("Unexpected indented codeblock");
|
|
}
|
|
};
|
|
}
|
|
Event::Start(_) => {
|
|
text.clear();
|
|
block_info = "".into();
|
|
}
|
|
Event::End(TagEnd::CodeBlock) => {
|
|
let block_info = block_info.trim();
|
|
match current_section {
|
|
Section::UncommittedDiff => {
|
|
spec.uncommitted_diff = mem::take(&mut text);
|
|
}
|
|
Section::EditHistory => {
|
|
if next_edit_predicted {
|
|
spec.edit_history
|
|
.push_str(&format!("{}\n", ACCEPTED_PREDICTION_MARKER));
|
|
next_edit_predicted = false;
|
|
}
|
|
spec.edit_history.push_str(&mem::take(&mut text));
|
|
}
|
|
Section::CursorPosition => {
|
|
spec.cursor_path = Path::new(block_info).into();
|
|
spec.cursor_position = mem::take(&mut text);
|
|
}
|
|
Section::ExpectedPatch => {
|
|
spec.expected_patches.push(mem::take(&mut text));
|
|
}
|
|
Section::RejectedPatch => {
|
|
spec.rejected_patch = Some(mem::take(&mut text));
|
|
}
|
|
Section::Start | Section::Other => {}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
if spec.cursor_path.as_ref() == Path::new("") || spec.cursor_position.is_empty() {
|
|
anyhow::bail!("Missing cursor position codeblock");
|
|
}
|
|
|
|
Ok(spec)
|
|
}
|
|
|
|
/// Returns the excerpt of text around the cursor, and the offset of the cursor within that
|
|
/// excerpt.
|
|
///
|
|
/// The cursor's position is marked with a special comment that appears
|
|
/// below the cursor line, which contains the string `[CURSOR_POSITION]`,
|
|
/// preceded by an arrow marking the cursor's column. The arrow can be
|
|
/// either:
|
|
/// - `^` - The cursor column is at the position of the `^` character (pointing up to the cursor)
|
|
/// - `<` - The cursor column is at the first non-whitespace character on that line.
|
|
pub fn cursor_excerpt(&self) -> Result<(String, usize)> {
|
|
let input = &self.cursor_position;
|
|
|
|
// Check for inline cursor marker first
|
|
if let Some(inline_offset) = input.find(INLINE_CURSOR_MARKER) {
|
|
let excerpt = input[..inline_offset].to_string()
|
|
+ &input[inline_offset + INLINE_CURSOR_MARKER.len()..];
|
|
return Ok((excerpt, inline_offset));
|
|
}
|
|
|
|
let marker_offset = input
|
|
.find(CURSOR_POSITION_MARKER)
|
|
.context("missing [CURSOR_POSITION] marker")?;
|
|
let marker_line_start = input[..marker_offset]
|
|
.rfind('\n')
|
|
.map(|pos| pos + 1)
|
|
.unwrap_or(0);
|
|
let marker_line_end = input[marker_line_start..]
|
|
.find('\n')
|
|
.map(|pos| marker_line_start + pos + 1)
|
|
.unwrap_or(input.len());
|
|
let marker_line = &input[marker_line_start..marker_line_end].trim_end_matches('\n');
|
|
|
|
let cursor_column = if let Some(cursor_offset) = marker_line.find('^') {
|
|
cursor_offset
|
|
} else if let Some(less_than_pos) = marker_line.find('<') {
|
|
marker_line
|
|
.find(|c: char| !c.is_whitespace())
|
|
.unwrap_or(less_than_pos)
|
|
} else {
|
|
anyhow::bail!(
|
|
"cursor position marker line must contain '^' or '<' before [CURSOR_POSITION]"
|
|
);
|
|
};
|
|
|
|
let mut excerpt = input[..marker_line_start].to_string() + &input[marker_line_end..];
|
|
excerpt.truncate(excerpt.trim_end_matches('\n').len());
|
|
|
|
// The cursor is on the line above the marker line.
|
|
let cursor_line_end = marker_line_start.saturating_sub(1);
|
|
let cursor_line_start = excerpt[..cursor_line_end]
|
|
.rfind('\n')
|
|
.map(|pos| pos + 1)
|
|
.unwrap_or(0);
|
|
let cursor_offset = cursor_line_start + cursor_column;
|
|
|
|
Ok((excerpt, cursor_offset))
|
|
}
|
|
|
|
/// Sets the cursor position excerpt from a plain excerpt and cursor byte offset.
|
|
///
|
|
/// The `line_comment_prefix` is used to format the marker line as a comment.
|
|
/// If the cursor column is less than the comment prefix length, the `<` format is used.
|
|
/// Otherwise, the `^` format is used.
|
|
pub fn set_cursor_excerpt(
|
|
&mut self,
|
|
excerpt: &str,
|
|
cursor_offset: usize,
|
|
line_comment_prefix: &str,
|
|
) {
|
|
// Find which line the cursor is on and its column
|
|
let cursor_line_start = excerpt[..cursor_offset]
|
|
.rfind('\n')
|
|
.map(|pos| pos + 1)
|
|
.unwrap_or(0);
|
|
let cursor_line_end = excerpt[cursor_line_start..]
|
|
.find('\n')
|
|
.map(|pos| cursor_line_start + pos + 1)
|
|
.unwrap_or(excerpt.len());
|
|
let cursor_line = &excerpt[cursor_line_start..cursor_line_end];
|
|
let cursor_line_indent = &cursor_line[..cursor_line.len() - cursor_line.trim_start().len()];
|
|
let cursor_column = cursor_offset - cursor_line_start;
|
|
|
|
// Build the marker line
|
|
let mut marker_line = String::new();
|
|
if cursor_column < line_comment_prefix.len() {
|
|
for _ in 0..cursor_column {
|
|
marker_line.push(' ');
|
|
}
|
|
marker_line.push_str(line_comment_prefix);
|
|
write!(marker_line, " <{}", CURSOR_POSITION_MARKER).unwrap();
|
|
} else {
|
|
if cursor_column >= cursor_line_indent.len() + line_comment_prefix.len() {
|
|
marker_line.push_str(cursor_line_indent);
|
|
}
|
|
marker_line.push_str(line_comment_prefix);
|
|
while marker_line.len() < cursor_column {
|
|
marker_line.push(' ');
|
|
}
|
|
write!(marker_line, "^{}", CURSOR_POSITION_MARKER).unwrap();
|
|
}
|
|
|
|
// Build the final cursor_position string
|
|
let mut result = String::with_capacity(excerpt.len() + marker_line.len() + 2);
|
|
result.push_str(&excerpt[..cursor_line_end]);
|
|
if !result.ends_with('\n') {
|
|
result.push('\n');
|
|
}
|
|
result.push_str(&marker_line);
|
|
if cursor_line_end < excerpt.len() {
|
|
result.push('\n');
|
|
result.push_str(&excerpt[cursor_line_end..]);
|
|
}
|
|
|
|
self.cursor_position = result;
|
|
}
|
|
|
|
/// Returns all of the possible expected patches for this example, each with an optional
|
|
/// cursor offset.
|
|
///
|
|
/// The cursor offset is an offset within the new text (after applying the patch), relative
|
|
/// to the start of the hunk.
|
|
///
|
|
/// In the serialized representation of this example, the cursor position is represented
|
|
/// using a comment line in the diff, beginning with `#`, and containing a `[CURSOR_POSITION]`
|
|
/// marker with the same format as the [`Self::cursor_excerpt`].
|
|
pub fn expected_patches_with_cursor_positions(&self) -> Vec<(String, Option<usize>)> {
|
|
self.expected_patches
|
|
.iter()
|
|
.map(|patch| {
|
|
let mut clean_patch = String::new();
|
|
let mut cursor_offset: Option<usize> = None;
|
|
let mut line_start_offset = 0usize;
|
|
let mut prev_line_start_offset = 0usize;
|
|
|
|
for line in patch.lines() {
|
|
let diff_line = DiffLine::parse(line);
|
|
|
|
match &diff_line {
|
|
DiffLine::Garbage(content)
|
|
if content.starts_with('#')
|
|
&& content.contains(CURSOR_POSITION_MARKER) =>
|
|
{
|
|
let caret_column = if let Some(caret_pos) = content.find('^') {
|
|
caret_pos
|
|
} else if let Some(_) = content.find('<') {
|
|
0
|
|
} else {
|
|
continue;
|
|
};
|
|
let cursor_column = caret_column.saturating_sub('#'.len_utf8());
|
|
cursor_offset = Some(prev_line_start_offset + cursor_column);
|
|
}
|
|
_ => {
|
|
if !clean_patch.is_empty() {
|
|
clean_patch.push('\n');
|
|
}
|
|
clean_patch.push_str(line);
|
|
|
|
match diff_line {
|
|
DiffLine::Addition(content) | DiffLine::Context(content) => {
|
|
prev_line_start_offset = line_start_offset;
|
|
line_start_offset += content.len() + 1;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if patch.ends_with('\n') && !clean_patch.is_empty() {
|
|
clean_patch.push('\n');
|
|
}
|
|
|
|
(clean_patch, cursor_offset)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn set_expected_patches_with_cursor_positions(
|
|
&mut self,
|
|
patches: Vec<(String, Option<usize>)>,
|
|
) {
|
|
self.expected_patches = patches
|
|
.into_iter()
|
|
.map(|(patch, cursor_offset)| encode_cursor_in_patch(&patch, cursor_offset))
|
|
.collect();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use indoc::indoc;
|
|
|
|
#[test]
|
|
fn test_cursor_excerpt_with_caret() {
|
|
let mut spec = ExampleSpec {
|
|
name: String::new(),
|
|
repository_url: String::new(),
|
|
revision: String::new(),
|
|
tags: Vec::new(),
|
|
reasoning: None,
|
|
uncommitted_diff: String::new(),
|
|
cursor_path: Path::new("test.rs").into(),
|
|
cursor_position: String::new(),
|
|
edit_history: String::new(),
|
|
expected_patches: Vec::new(),
|
|
rejected_patch: None,
|
|
captured_prompt_input: None,
|
|
telemetry: None,
|
|
human_feedback: Vec::new(),
|
|
rating: None,
|
|
};
|
|
|
|
// Cursor before `42`
|
|
let excerpt = indoc! {"
|
|
fn main() {
|
|
let x = 42;
|
|
println!(\"{}\", x);
|
|
}"
|
|
};
|
|
let offset = excerpt.find("42").unwrap();
|
|
let position_string = indoc! {"
|
|
fn main() {
|
|
let x = 42;
|
|
// ^[CURSOR_POSITION]
|
|
println!(\"{}\", x);
|
|
}"
|
|
}
|
|
.to_string();
|
|
|
|
spec.set_cursor_excerpt(excerpt, offset, "//");
|
|
assert_eq!(spec.cursor_position, position_string);
|
|
assert_eq!(
|
|
spec.cursor_excerpt().unwrap(),
|
|
(excerpt.to_string(), offset)
|
|
);
|
|
|
|
// Cursor after `l` in `let`
|
|
let offset = excerpt.find("et x").unwrap();
|
|
let position_string = indoc! {"
|
|
fn main() {
|
|
let x = 42;
|
|
// ^[CURSOR_POSITION]
|
|
println!(\"{}\", x);
|
|
}"
|
|
}
|
|
.to_string();
|
|
|
|
spec.set_cursor_excerpt(excerpt, offset, "//");
|
|
assert_eq!(spec.cursor_position, position_string);
|
|
assert_eq!(
|
|
spec.cursor_excerpt().unwrap(),
|
|
(excerpt.to_string(), offset)
|
|
);
|
|
|
|
// Cursor before `let`
|
|
let offset = excerpt.find("let").unwrap();
|
|
let position_string = indoc! {"
|
|
fn main() {
|
|
let x = 42;
|
|
// ^[CURSOR_POSITION]
|
|
println!(\"{}\", x);
|
|
}"
|
|
}
|
|
.to_string();
|
|
|
|
spec.set_cursor_excerpt(excerpt, offset, "//");
|
|
assert_eq!(spec.cursor_position, position_string);
|
|
assert_eq!(
|
|
spec.cursor_excerpt().unwrap(),
|
|
(excerpt.to_string(), offset)
|
|
);
|
|
|
|
// Cursor at beginning of the line with `let`
|
|
let offset = excerpt.find(" let").unwrap();
|
|
let position_string = indoc! {"
|
|
fn main() {
|
|
let x = 42;
|
|
// <[CURSOR_POSITION]
|
|
println!(\"{}\", x);
|
|
}"
|
|
}
|
|
.to_string();
|
|
|
|
spec.set_cursor_excerpt(excerpt, offset, "//");
|
|
assert_eq!(spec.cursor_position, position_string);
|
|
assert_eq!(
|
|
spec.cursor_excerpt().unwrap(),
|
|
(excerpt.to_string(), offset)
|
|
);
|
|
|
|
// Cursor at end of line, after the semicolon
|
|
let offset = excerpt.find(';').unwrap() + 1;
|
|
let position_string = indoc! {"
|
|
fn main() {
|
|
let x = 42;
|
|
// ^[CURSOR_POSITION]
|
|
println!(\"{}\", x);
|
|
}"
|
|
}
|
|
.to_string();
|
|
|
|
spec.set_cursor_excerpt(excerpt, offset, "//");
|
|
assert_eq!(spec.cursor_position, position_string);
|
|
assert_eq!(
|
|
spec.cursor_excerpt().unwrap(),
|
|
(excerpt.to_string(), offset)
|
|
);
|
|
|
|
// Caret at end of file (no trailing newline)
|
|
let excerpt = indoc! {"
|
|
fn main() {
|
|
let x = 42;"
|
|
};
|
|
let offset = excerpt.find(';').unwrap() + 1;
|
|
let position_string = indoc! {"
|
|
fn main() {
|
|
let x = 42;
|
|
// ^[CURSOR_POSITION]"
|
|
}
|
|
.to_string();
|
|
|
|
spec.set_cursor_excerpt(excerpt, offset, "//");
|
|
assert_eq!(spec.cursor_position, position_string);
|
|
assert_eq!(
|
|
spec.cursor_excerpt().unwrap(),
|
|
(excerpt.to_string(), offset)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_cursor_excerpt_with_inline_marker() {
|
|
let mut spec = ExampleSpec {
|
|
name: String::new(),
|
|
repository_url: String::new(),
|
|
revision: String::new(),
|
|
tags: Vec::new(),
|
|
reasoning: None,
|
|
uncommitted_diff: String::new(),
|
|
cursor_path: Path::new("test.rs").into(),
|
|
cursor_position: String::new(),
|
|
edit_history: String::new(),
|
|
expected_patches: Vec::new(),
|
|
rejected_patch: None,
|
|
captured_prompt_input: None,
|
|
telemetry: None,
|
|
human_feedback: Vec::new(),
|
|
rating: None,
|
|
};
|
|
|
|
// Cursor before `42` using inline marker
|
|
spec.cursor_position = indoc! {"
|
|
fn main() {
|
|
let x = <|user_cursor|>42;
|
|
println!(\"{}\", x);
|
|
}"
|
|
}
|
|
.to_string();
|
|
|
|
let expected_excerpt = indoc! {"
|
|
fn main() {
|
|
let x = 42;
|
|
println!(\"{}\", x);
|
|
}"
|
|
};
|
|
let expected_offset = expected_excerpt.find("42").unwrap();
|
|
|
|
assert_eq!(
|
|
spec.cursor_excerpt().unwrap(),
|
|
(expected_excerpt.to_string(), expected_offset)
|
|
);
|
|
|
|
// Cursor at beginning of line
|
|
spec.cursor_position = indoc! {"
|
|
fn main() {
|
|
<|user_cursor|> let x = 42;
|
|
}"
|
|
}
|
|
.to_string();
|
|
|
|
let expected_excerpt = indoc! {"
|
|
fn main() {
|
|
let x = 42;
|
|
}"
|
|
};
|
|
let expected_offset = expected_excerpt.find(" let").unwrap();
|
|
|
|
assert_eq!(
|
|
spec.cursor_excerpt().unwrap(),
|
|
(expected_excerpt.to_string(), expected_offset)
|
|
);
|
|
|
|
// Cursor at end of file
|
|
spec.cursor_position = "fn main() {}<|user_cursor|>".to_string();
|
|
let expected_excerpt = "fn main() {}";
|
|
let expected_offset = expected_excerpt.len();
|
|
|
|
assert_eq!(
|
|
spec.cursor_excerpt().unwrap(),
|
|
(expected_excerpt.to_string(), expected_offset)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_expected_patches_with_cursor_positions() {
|
|
let mut spec = ExampleSpec {
|
|
name: String::new(),
|
|
repository_url: String::new(),
|
|
revision: String::new(),
|
|
tags: Vec::new(),
|
|
reasoning: None,
|
|
uncommitted_diff: String::new(),
|
|
cursor_path: Path::new("test.rs").into(),
|
|
cursor_position: String::new(),
|
|
edit_history: String::new(),
|
|
expected_patches: Vec::new(),
|
|
rejected_patch: None,
|
|
captured_prompt_input: None,
|
|
telemetry: None,
|
|
human_feedback: Vec::new(),
|
|
rating: None,
|
|
};
|
|
|
|
let new_content = indoc! {r#"
|
|
// prints a greeting
|
|
fn main() {
|
|
println!("hello, {}", );
|
|
let x = 42;
|
|
}
|
|
"#};
|
|
let cursor_offset = new_content.find(");").unwrap();
|
|
|
|
let clean_patch = indoc! {r#"
|
|
--- a/test.rs
|
|
+++ b/test.rs
|
|
@@ -1,3 +1,4 @@
|
|
+// prints a greeting
|
|
fn main() {
|
|
- println!("hi");
|
|
+ println!("hello, {}", );
|
|
let x = 42;
|
|
}
|
|
"#}
|
|
.to_string();
|
|
|
|
let encoded_patch = indoc! {r#"
|
|
--- a/test.rs
|
|
+++ b/test.rs
|
|
@@ -1,3 +1,4 @@
|
|
+// prints a greeting
|
|
fn main() {
|
|
- println!("hi");
|
|
+ println!("hello, {}", );
|
|
# ^[CURSOR_POSITION]
|
|
let x = 42;
|
|
}
|
|
"#}
|
|
.to_string();
|
|
|
|
spec.set_expected_patches_with_cursor_positions(vec![(
|
|
clean_patch.clone(),
|
|
Some(cursor_offset),
|
|
)]);
|
|
assert_eq!(spec.expected_patches, vec![encoded_patch]);
|
|
|
|
let results = spec.expected_patches_with_cursor_positions();
|
|
assert_eq!(results, vec![(clean_patch.clone(), Some(cursor_offset))]);
|
|
|
|
spec.set_expected_patches_with_cursor_positions(vec![(clean_patch.clone(), None)]);
|
|
assert_eq!(spec.expected_patches, vec![clean_patch.clone()]);
|
|
|
|
let results = spec.expected_patches_with_cursor_positions();
|
|
assert_eq!(results, vec![(clean_patch, None)]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_from_markdown_accepted_prediction_marker() {
|
|
let markdown = indoc! {r#"
|
|
+++
|
|
repository_url = "https://github.com/example/repo"
|
|
revision = "abc123"
|
|
+++
|
|
|
|
## Edit History
|
|
|
|
```diff
|
|
--- a/src/main.rs
|
|
+++ b/src/main.rs
|
|
@@ -1,3 +1,3 @@
|
|
-fn hello() {}
|
|
+fn hello_world() {}
|
|
```
|
|
|
|
// User accepted prediction:
|
|
```diff
|
|
--- a/src/main.rs
|
|
+++ b/src/main.rs
|
|
@@ -1,3 +1,3 @@
|
|
-fn hello_world() {}
|
|
+fn hello_world() { println!("hi"); }
|
|
```
|
|
|
|
```diff
|
|
--- a/src/main.rs
|
|
+++ b/src/main.rs
|
|
@@ -1,3 +1,3 @@
|
|
-fn hello_world() { println!("hi"); }
|
|
+fn hello_world() { println!("hello"); }
|
|
```
|
|
|
|
## Cursor Position
|
|
|
|
```src/main.rs
|
|
fn hello_world() { println!("hello"); }
|
|
# ^[CURSOR_POSITION]
|
|
```
|
|
|
|
## Expected Patch
|
|
|
|
```diff
|
|
--- a/src/main.rs
|
|
+++ b/src/main.rs
|
|
@@ -1,3 +1,3 @@
|
|
-fn hello_world() { println!("hello"); }
|
|
+fn hello_world() { println!("hello, world!"); }
|
|
```
|
|
"#};
|
|
|
|
let spec = ExampleSpec::from_markdown(markdown).unwrap();
|
|
|
|
// The first diff should NOT have the marker
|
|
assert!(spec.edit_history.starts_with("--- a/src/main.rs"));
|
|
|
|
// The second diff should be preceded by the accepted prediction marker
|
|
assert!(
|
|
spec.edit_history
|
|
.contains("// User accepted prediction:\n--- a/src/main.rs")
|
|
);
|
|
|
|
// Count occurrences of the marker - should be exactly one
|
|
let marker_count = spec
|
|
.edit_history
|
|
.matches("// User accepted prediction:")
|
|
.count();
|
|
assert_eq!(marker_count, 1);
|
|
|
|
// The third diff should NOT have the marker
|
|
// Verify all three diffs are present
|
|
let diff_count = spec.edit_history.matches("--- a/src/main.rs").count();
|
|
assert_eq!(diff_count, 3);
|
|
}
|
|
}
|