| use codex_exec_server::ExecutorFileSystem; |
| use codex_exec_server::FileSystemSandboxContext; |
| use codex_exec_server::ReadFileOptions; |
| use codex_utils_path_uri::PathUri; |
| use similar::TextDiff; |
|
|
| use crate::ApplyPatchError; |
| use crate::ApplyPatchFileUpdateMode; |
| use crate::IoError; |
| use crate::UpdateFileChunk; |
| use crate::seek_sequence; |
| use crate::text_file::Replacement; |
| use crate::text_file::SourceFile; |
|
|
| #[cfg(test)] |
| #[path = "file_update_tests.rs"] |
| mod tests; |
|
|
| pub(crate) struct AppliedPatch { |
| pub(crate) original_contents: String, |
| pub(crate) new_contents: String, |
| } |
|
|
| |
| |
| pub(crate) async fn derive_new_contents_from_chunks( |
| path: &PathUri, |
| chunks: &[UpdateFileChunk], |
| update_file_mode: ApplyPatchFileUpdateMode, |
| fs: &dyn ExecutorFileSystem, |
| follow_symlinks: bool, |
| sandbox: Option<&FileSystemSandboxContext>, |
| ) -> std::result::Result<AppliedPatch, ApplyPatchError> { |
| let original_contents = fs |
| .read_file_text(path, ReadFileOptions { follow_symlinks }, sandbox) |
| .await |
| .map_err(|err| { |
| ApplyPatchError::IoError(IoError { |
| context: format!( |
| "Failed to read file to update {}", |
| path.inferred_native_path_string() |
| ), |
| source: err, |
| }) |
| })?; |
|
|
| let path_text = path.inferred_native_path_string(); |
| let new_contents = match update_file_mode { |
| ApplyPatchFileUpdateMode::NormalizeToLf => { |
| let mut original_lines = original_contents |
| .split('\n') |
| .map(String::from) |
| .collect::<Vec<_>>(); |
|
|
| |
| |
| if original_lines.last().is_some_and(String::is_empty) { |
| original_lines.pop(); |
| } |
|
|
| let replacements = |
| compute_replacements(&original_lines, &path_text, chunks, update_file_mode)?; |
| let mut new_lines = apply_replacements(original_lines, &replacements); |
| if !new_lines.last().is_some_and(String::is_empty) { |
| new_lines.push(String::new()); |
| } |
| new_lines.join("\n") |
| } |
| ApplyPatchFileUpdateMode::PreserveLineEndings => { |
| let mut source_file = SourceFile::parse(&original_contents); |
| let original_lines = source_file.line_texts(); |
| let replacements = |
| compute_replacements(&original_lines, &path_text, chunks, update_file_mode)?; |
| source_file.apply_replacements(&replacements); |
| source_file.into_contents() |
| } |
| }; |
| Ok(AppliedPatch { |
| original_contents, |
| new_contents, |
| }) |
| } |
|
|
| |
| |
| |
| fn compute_replacements( |
| original_lines: &[String], |
| path: &str, |
| chunks: &[UpdateFileChunk], |
| update_file_mode: ApplyPatchFileUpdateMode, |
| ) -> std::result::Result<Vec<Replacement>, ApplyPatchError> { |
| let mut replacements: Vec<Replacement> = Vec::new(); |
| let mut line_index: usize = 0; |
|
|
| for chunk in chunks { |
| |
| |
| if let Some(ctx_line) = &chunk.change_context { |
| if let Some(idx) = seek_sequence::seek_sequence( |
| original_lines, |
| std::slice::from_ref(ctx_line), |
| line_index, |
| false, |
| update_file_mode, |
| ) { |
| line_index = idx + 1; |
| } else { |
| return Err(ApplyPatchError::ComputeReplacements(format!( |
| "Failed to find context '{ctx_line}' in {path}" |
| ))); |
| } |
| } |
|
|
| if chunk.old_lines.is_empty() { |
| |
| |
| |
| let insertion_idx = match update_file_mode { |
| ApplyPatchFileUpdateMode::NormalizeToLf => { |
| if original_lines.last().is_some_and(String::is_empty) { |
| original_lines.len() - 1 |
| } else { |
| original_lines.len() |
| } |
| } |
| ApplyPatchFileUpdateMode::PreserveLineEndings => original_lines.len(), |
| }; |
| replacements.push((insertion_idx, 0, chunk.new_lines.clone())); |
| continue; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| let mut pattern: &[String] = &chunk.old_lines; |
| let mut found = seek_sequence::seek_sequence( |
| original_lines, |
| pattern, |
| line_index, |
| chunk.is_end_of_file, |
| update_file_mode, |
| ); |
|
|
| let mut new_slice: &[String] = &chunk.new_lines; |
|
|
| if found.is_none() && pattern.last().is_some_and(String::is_empty) { |
| |
| |
| pattern = &pattern[..pattern.len() - 1]; |
| if new_slice.last().is_some_and(String::is_empty) { |
| new_slice = &new_slice[..new_slice.len() - 1]; |
| } |
|
|
| found = seek_sequence::seek_sequence( |
| original_lines, |
| pattern, |
| line_index, |
| chunk.is_end_of_file, |
| update_file_mode, |
| ); |
| } |
|
|
| if let Some(start_idx) = found { |
| match update_file_mode { |
| ApplyPatchFileUpdateMode::NormalizeToLf => { |
| replacements.push((start_idx, pattern.len(), new_slice.to_vec())); |
| } |
| ApplyPatchFileUpdateMode::PreserveLineEndings => { |
| |
| |
| |
| let mut old_start = 0; |
| let mut new_start = 0; |
| for &(old_context, new_context) in &chunk.context_line_indices { |
| |
| |
| if old_context >= pattern.len() || new_context >= new_slice.len() { |
| break; |
| } |
| if old_start != old_context || new_start != new_context { |
| replacements.push(( |
| start_idx + old_start, |
| old_context - old_start, |
| new_slice[new_start..new_context].to_vec(), |
| )); |
| } |
| old_start = old_context + 1; |
| new_start = new_context + 1; |
| } |
| if old_start != pattern.len() || new_start != new_slice.len() { |
| replacements.push(( |
| start_idx + old_start, |
| pattern.len() - old_start, |
| new_slice[new_start..].to_vec(), |
| )); |
| } |
| } |
| } |
| line_index = start_idx + pattern.len(); |
| } else { |
| return Err(ApplyPatchError::ComputeReplacements(format!( |
| "Failed to find expected lines in {}:\n{}", |
| path, |
| chunk.old_lines.join("\n"), |
| ))); |
| } |
| } |
|
|
| replacements.sort_by_key(|(index, _, _)| *index); |
|
|
| Ok(replacements) |
| } |
|
|
| |
| |
| fn apply_replacements(mut lines: Vec<String>, replacements: &[Replacement]) -> Vec<String> { |
| |
| |
| for (start_idx, old_len, new_segment) in replacements.iter().rev() { |
| let start_idx = *start_idx; |
| let old_len = *old_len; |
|
|
| |
| for _ in 0..old_len { |
| if start_idx < lines.len() { |
| lines.remove(start_idx); |
| } |
| } |
|
|
| |
| for (offset, new_line) in new_segment.iter().enumerate() { |
| lines.insert(start_idx + offset, new_line.clone()); |
| } |
| } |
|
|
| lines |
| } |
|
|
| |
| #[derive(Debug, Eq, PartialEq)] |
| pub struct ApplyPatchFileUpdate { |
| pub(crate) unified_diff: String, |
| pub(crate) original_content: String, |
| pub(crate) content: String, |
| } |
|
|
| pub async fn unified_diff_from_chunks( |
| path: &PathUri, |
| chunks: &[UpdateFileChunk], |
| fs: &dyn ExecutorFileSystem, |
| sandbox: Option<&FileSystemSandboxContext>, |
| ) -> std::result::Result<ApplyPatchFileUpdate, ApplyPatchError> { |
| unified_diff_from_chunks_with_mode( |
| path, |
| chunks, |
| ApplyPatchFileUpdateMode::default(), |
| fs, |
| sandbox, |
| ) |
| .await |
| } |
|
|
| pub(crate) async fn unified_diff_from_chunks_with_mode( |
| path: &PathUri, |
| chunks: &[UpdateFileChunk], |
| update_file_mode: ApplyPatchFileUpdateMode, |
| fs: &dyn ExecutorFileSystem, |
| sandbox: Option<&FileSystemSandboxContext>, |
| ) -> std::result::Result<ApplyPatchFileUpdate, ApplyPatchError> { |
| unified_diff_from_chunks_with_context_and_mode( |
| path, |
| chunks, |
| 1, |
| update_file_mode, |
| fs, |
| sandbox, |
| ) |
| .await |
| } |
|
|
| pub async fn unified_diff_from_chunks_with_context( |
| path: &PathUri, |
| chunks: &[UpdateFileChunk], |
| context: usize, |
| fs: &dyn ExecutorFileSystem, |
| sandbox: Option<&FileSystemSandboxContext>, |
| ) -> std::result::Result<ApplyPatchFileUpdate, ApplyPatchError> { |
| unified_diff_from_chunks_with_context_and_mode( |
| path, |
| chunks, |
| context, |
| ApplyPatchFileUpdateMode::default(), |
| fs, |
| sandbox, |
| ) |
| .await |
| } |
|
|
| async fn unified_diff_from_chunks_with_context_and_mode( |
| path: &PathUri, |
| chunks: &[UpdateFileChunk], |
| context: usize, |
| update_file_mode: ApplyPatchFileUpdateMode, |
| fs: &dyn ExecutorFileSystem, |
| sandbox: Option<&FileSystemSandboxContext>, |
| ) -> std::result::Result<ApplyPatchFileUpdate, ApplyPatchError> { |
| let AppliedPatch { |
| original_contents, |
| new_contents, |
| } = derive_new_contents_from_chunks( |
| path, |
| chunks, |
| update_file_mode, |
| fs, |
| true, |
| sandbox, |
| ) |
| .await?; |
| let text_diff = TextDiff::from_lines(&original_contents, &new_contents); |
| let unified_diff = text_diff.unified_diff().context_radius(context).to_string(); |
| Ok(ApplyPatchFileUpdate { |
| unified_diff, |
| original_content: original_contents, |
| content: new_contents, |
| }) |
| } |
|
|