1269 lines · 41.4 KB · Rust
RawDownload
mod reindent;mod streaming_fuzzy_matcher;mod streaming_parser; use super::tool_permissions::resolve_creatable_global_skill_path;use crate::{Thread, ToolCallEventStream};use acp_thread::Diff;use action_log::ActionLog;use agent_client_protocol::schema::v1::{self as acp, ToolCallLocation, ToolCallUpdateFields};use anyhow::Result;use collections::HashSet;use futures::{FutureExt, channel::oneshot};use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};use language::language_settings::{self, FormatOnSave};use language::{Buffer, BufferEditSource, BufferEvent, LanguageRegistry};use language_model::LanguageModelToolResultContent;use project::lsp_store::{FormatTrigger, LspFormatTarget};use project::{AgentLocation, Project, ProjectPath};use reindent::{Reindenter, compute_indent_delta, compute_rest_indent_delta};use schemars::JsonSchema;use serde::{Deserialize, Serialize, de::DeserializeOwned};use std::ops::Range;use std::path::PathBuf;use std::sync::Arc;use streaming_diff::{CharOperation, StreamingDiff};use streaming_fuzzy_matcher::StreamingFuzzyMatcher;use streaming_parser::{EditEvent, StreamingParser, WriteEvent};use text::ToOffset;use ui::SharedString;use util::rel_path::RelPath;use util::{Deferred, ResultExt}; /// Operating mode used internally by `EditSession`/`Pipeline` to choose between/// applying granular edits (the `edit_file` tool) or replacing/creating the/// entire file content (the `write_file` tool).#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub(crate) enum EditSessionMode {    Write,    Edit,} /// A single edit operation that replaces old text with new text/// Properly escape all text fields as valid JSON strings./// Remember to escape special characters like newlines (`\n`) and quotes (`"`) in JSON strings.#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]pub struct Edit {    /// The exact text to find in the file. This will be matched using fuzzy matching    /// to handle minor differences in whitespace or formatting.    ///    /// Be minimal with replacements:    /// - For unique lines, include only those lines    /// - For non-unique lines, include enough context to identify them    pub old_text: String,    /// The text to replace it with    pub new_text: String,} #[derive(Clone, Default, Debug, Deserialize)]pub struct PartialEdit {    #[serde(default)]    pub old_text: Option<String>,    #[serde(default)]    pub new_text: Option<String>,} #[derive(Debug, Serialize, Deserialize)]#[serde(untagged)]pub enum EditSessionOutput {    Success {        #[serde(alias = "original_path")]        input_path: PathBuf,        new_text: String,        old_text: Arc<String>,        #[serde(default)]        diff: String,    },    Error {        error: String,        #[serde(default, skip_serializing_if = "Option::is_none")]        input_path: Option<PathBuf>,        #[serde(default, skip_serializing_if = "String::is_empty")]        diff: String,    },} impl EditSessionOutput {    pub fn error(error: impl Into<String>) -> Self {        Self::Error {            error: error.into(),            input_path: None,            diff: String::new(),        }    }} impl std::fmt::Display for EditSessionOutput {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        match self {            EditSessionOutput::Success {                diff, input_path, ..            } => {                if diff.is_empty() {                    write!(f, "No edits were made.")                } else {                    write!(f, "Edited {} successfully", input_path.display())                }            }            EditSessionOutput::Error {                error,                diff,                input_path,            } => {                write!(f, "{error}\n")?;                if let Some(input_path) = input_path                    && !diff.is_empty()                {                    write!(                        f,                        "Edited {}:\n\n```diff\n{diff}\n```",                        input_path.display()                    )                } else {                    write!(f, "No edits were made.")                }            }        }    }} impl From<EditSessionOutput> for LanguageModelToolResultContent {    fn from(output: EditSessionOutput) -> Self {        output.to_string().into()    }} pub(crate) struct EditSessionContext {    project: Entity<Project>,    thread: WeakEntity<Thread>,    action_log: Entity<ActionLog>,    language_registry: Arc<LanguageRegistry>,} impl EditSessionContext {    pub(crate) fn new(        project: Entity<Project>,        thread: WeakEntity<Thread>,        action_log: Entity<ActionLog>,        language_registry: Arc<LanguageRegistry>,    ) -> Self {        Self {            project,            thread,            action_log,            language_registry,        }    }     pub(crate) fn authorize(        &self,        tool_name: &str,        path: &PathBuf,        event_stream: &ToolCallEventStream,        cx: &mut App,    ) -> Task<Result<()>> {        super::tool_permissions::authorize_file_edit(            tool_name,            path,            &self.thread,            event_stream,            cx,        )    }     fn set_agent_location(&self, buffer: WeakEntity<Buffer>, position: text::Anchor, cx: &mut App) {        let should_update_agent_location = self            .thread            .read_with(cx, |thread, _cx| !thread.is_subagent())            .unwrap_or_default();        if should_update_agent_location {            self.project.update(cx, |project, cx| {                project.set_agent_location(Some(AgentLocation { buffer, position }), cx);            });        }    }     async fn ensure_buffer_saved(&self, buffer: &Entity<Buffer>, cx: &mut AsyncApp) {        let format_on_save_enabled = buffer.read_with(cx, |buffer, cx| {            let settings = language_settings::LanguageSettings::for_buffer(buffer, cx);            settings.format_on_save != FormatOnSave::Off        });         if format_on_save_enabled {            self.project                .update(cx, |project, cx| {                    project.format(                        HashSet::from_iter([buffer.clone()]),                        LspFormatTarget::Buffers,                        false,                        FormatTrigger::Save,                        cx,                    )                })                .await                .log_err();        }         self.project            .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))            .await            .log_err();         self.action_log.update(cx, |log, cx| {            log.buffer_edited(buffer.clone(), cx);        });    }     pub(crate) fn initial_title_from_path(        &self,        path: &std::path::Path,        default: &str,        cx: &App,    ) -> SharedString {        let project = self.project.read(cx);        if let Some(project_path) = project.find_project_path(path, cx)            && let Some(short) = project.short_full_path_for_project_path(&project_path, cx)        {            return short.into();        }         let display = path.to_string_lossy();        if display.is_empty() {            default.into()        } else {            display.into_owned().into()        }    }     pub(crate) fn replay_output(        &self,        output: EditSessionOutput,        event_stream: ToolCallEventStream,        cx: &mut App,    ) -> Result<()> {        match output {            EditSessionOutput::Success {                input_path,                old_text,                new_text,                ..            } => {                event_stream.update_diff(cx.new(|cx| {                    Diff::finalized(                        input_path.to_string_lossy().into_owned(),                        Some(old_text.to_string()),                        new_text,                        self.language_registry.clone(),                        cx,                    )                }));                Ok(())            }            EditSessionOutput::Error { .. } => Ok(()),        }    }} pub(crate) enum EditSessionResult {    Completed(EditSession),    Failed {        error: String,        session: Option<EditSession>,    },} pub(crate) async fn run_session(    result: EditSessionResult,    event_stream: &ToolCallEventStream,    cx: &mut AsyncApp,) -> Result<EditSessionOutput, EditSessionOutput> {    match result {        EditSessionResult::Completed(session) => {            session                .context                .ensure_buffer_saved(&session.buffer, cx)                .await;            let (new_text, diff) = session.compute_new_text_and_diff(cx).await;            Ok(EditSessionOutput::Success {                old_text: session.old_text.clone(),                new_text,                input_path: session.input_path,                diff,            })        }        EditSessionResult::Failed {            error,            session: Some(session),        } => {            session                .context                .ensure_buffer_saved(&session.buffer, cx)                .await;            let (_new_text, diff) = session.compute_new_text_and_diff(cx).await;            if diff.is_empty() {                event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![                    acp::ToolCallContent::Content(acp::Content::new(error.clone())),                ]));            }            Err(EditSessionOutput::Error {                error,                input_path: Some(session.input_path),                diff,            })        }        EditSessionResult::Failed {            error,            session: None,        } => {            event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![                acp::ToolCallContent::Content(acp::Content::new(error.clone())),            ]));            Err(EditSessionOutput::Error {                error,                input_path: None,                diff: String::new(),            })        }    }} pub(crate) fn initial_title_from_partial_path<P>(    context: &EditSessionContext,    raw_input: serde_json::Value,    extract_path: impl FnOnce(&P) -> Option<String>,    default: &str,    cx: &App,) -> SharedStringwhere    P: DeserializeOwned,{    if let Ok(partial) = serde_json::from_value::<P>(raw_input)        && let Some(raw_path) = extract_path(&partial)    {        let trimmed = raw_path.trim();        if !trimmed.is_empty() {            return context.initial_title_from_path(std::path::Path::new(trimmed), default, cx);        }    }    default.into()} pub(crate) struct EditSession {    abs_path: PathBuf,    pub(crate) input_path: PathBuf,    pub(crate) buffer: Entity<Buffer>,    pub(crate) old_text: Arc<String>,    diff: Entity<Diff>,    parser: StreamingParser,    pipeline: Pipeline,    context: Arc<EditSessionContext>,    _finalize_diff_guard: Deferred<Box<dyn FnOnce()>>,} /// The destination of an edit session, identified by its absolute path on/// disk. `project_path` is `Some` for files that live inside one of the/// project's worktrees (i.e. that the standard project-path machinery can/// resolve), and `None` for global skill files reached through the/// `~/.agents/skills` allowlist.struct EditSessionTarget {    abs_path: PathBuf,    project_path: Option<ProjectPath>,} enum Pipeline {    Write(WritePipeline),    Edit(EditPipeline),} struct WritePipeline {    content_written: bool,} struct EditPipeline {    current_edit: Option<EditPipelineEntry>,    file_changed_since_last_read: bool,} enum EditPipelineEntry {    ResolvingOldText {        matcher: StreamingFuzzyMatcher,    },    StreamingNewText {        streaming_diff: StreamingDiff,        edit_cursor: usize,        reindenter: Reindenter,        original_snapshot: text::BufferSnapshot,    },} impl Pipeline {    fn new(mode: EditSessionMode, file_changed_since_last_read: bool) -> Self {        match mode {            EditSessionMode::Write => Self::Write(WritePipeline {                content_written: false,            }),            EditSessionMode::Edit => Self::Edit(EditPipeline {                current_edit: None,                file_changed_since_last_read,            }),        }    }} impl WritePipeline {    fn process_event(        &mut self,        event: &WriteEvent,        buffer: &Entity<Buffer>,        context: &EditSessionContext,        cx: &mut AsyncApp,    ) {        let WriteEvent::ContentChunk { chunk } = event;         let (buffer_id, buffer_len) =            buffer.read_with(cx, |buffer, _cx| (buffer.remote_id(), buffer.len()));        let edit_range = if self.content_written {            buffer_len..buffer_len        } else {            0..buffer_len        };         agent_edit_buffer(            buffer,            [(edit_range, chunk.as_str())],            &context.action_log,            cx,        );        cx.update(|cx| {            context.set_agent_location(                buffer.downgrade(),                text::Anchor::max_for_buffer(buffer_id),                cx,            );        });        self.content_written = true;    }} impl EditPipeline {    fn ensure_resolving_old_text(&mut self, buffer: &Entity<Buffer>, cx: &mut AsyncApp) {        if self.current_edit.is_none() {            let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.text_snapshot());            self.current_edit = Some(EditPipelineEntry::ResolvingOldText {                matcher: StreamingFuzzyMatcher::new(snapshot),            });        }    }     fn process_event(        &mut self,        event: &EditEvent,        buffer: &Entity<Buffer>,        diff: &Entity<Diff>,        abs_path: &PathBuf,        context: &EditSessionContext,        event_stream: &ToolCallEventStream,        cx: &mut AsyncApp,    ) -> Result<(), String> {        match event {            EditEvent::OldTextChunk {                chunk, done: false, ..            } => {                log::debug!("old_text_chunk: done=false, chunk='{}'", chunk);                self.ensure_resolving_old_text(buffer, cx);                 if let Some(EditPipelineEntry::ResolvingOldText { matcher }) =                    &mut self.current_edit                    && !chunk.is_empty()                {                    if let Some(match_range) = matcher.push(chunk, None) {                        let anchor_range = buffer.read_with(cx, |buffer, _cx| {                            buffer.anchor_range_outside(match_range.clone())                        });                        diff.update(cx, |diff, cx| diff.reveal_range(anchor_range, cx));                         cx.update(|cx| {                            let position = buffer.read(cx).anchor_before(match_range.end);                            context.set_agent_location(buffer.downgrade(), position, cx);                        });                    }                }            }            EditEvent::OldTextChunk {                edit_index,                chunk,                done: true,            } => {                log::debug!("old_text_chunk: done=true, chunk='{}'", chunk);                 self.ensure_resolving_old_text(buffer, cx); 

Lines 1–500 of 1269.