Detailed changes
@@ -738,7 +738,6 @@ dependencies = [
"assistant_tool",
"chrono",
"collections",
- "feature_flags",
"futures 0.3.31",
"gpui",
"html_to_markdown",
@@ -746,18 +745,14 @@ dependencies = [
"itertools 0.14.0",
"language",
"language_model",
- "log",
"lsp",
"open",
"project",
"rand 0.8.5",
"regex",
- "release_channel",
"schemars",
"serde",
"serde_json",
- "settings",
- "theme",
"ui",
"unindent",
"util",
@@ -16,7 +16,6 @@ anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
collections.workspace = true
-feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
@@ -24,19 +23,14 @@ http_client.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
-log.workspace = true
lsp.workspace = true
project.workspace = true
regex.workspace = true
-release_channel.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
-settings.workspace = true
-theme.workspace = true
ui.workspace = true
util.workspace = true
-workspace.workspace = true
worktree.workspace = true
open = { workspace = true }
workspace-hack.workspace = true
@@ -7,7 +7,6 @@ mod create_directory_tool;
mod create_file_tool;
mod delete_path_tool;
mod diagnostics_tool;
-mod edit_files_tool;
mod fetch_tool;
mod find_replace_file_tool;
mod list_directory_tool;
@@ -37,7 +36,6 @@ use crate::create_directory_tool::CreateDirectoryTool;
use crate::create_file_tool::CreateFileTool;
use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
-use crate::edit_files_tool::EditFilesTool;
use crate::fetch_tool::FetchTool;
use crate::find_replace_file_tool::FindReplaceFileTool;
use crate::list_directory_tool::ListDirectoryTool;
@@ -51,7 +49,6 @@ use crate::thinking_tool::ThinkingTool;
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
assistant_tool::init(cx);
- crate::edit_files_tool::log::init(cx);
let registry = ToolRegistry::global(cx);
registry.register_tool(BashTool);
@@ -64,7 +61,6 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(SymbolInfoTool);
registry.register_tool(MovePathTool);
registry.register_tool(DiagnosticsTool);
- registry.register_tool(EditFilesTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(NowTool);
registry.register_tool(OpenTool);
@@ -1,559 +0,0 @@
-mod edit_action;
-pub mod log;
-
-use crate::replace::{replace_exact, replace_with_flexible_indent};
-use crate::schema::json_schema_for;
-use anyhow::{Context, Result, anyhow};
-use assistant_tool::{ActionLog, Tool};
-use collections::HashSet;
-use edit_action::{EditAction, EditActionParser, edit_model_prompt};
-use futures::{SinkExt, StreamExt, channel::mpsc};
-use gpui::{App, AppContext, AsyncApp, Entity, Task};
-use language_model::{ConfiguredModel, LanguageModelToolSchemaFormat};
-use language_model::{
- LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role,
-};
-use log::{EditToolLog, EditToolRequestId};
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::fmt::Write;
-use std::sync::Arc;
-use ui::IconName;
-use util::ResultExt;
-
-#[derive(Debug, Serialize, Deserialize, JsonSchema)]
-pub struct EditFilesToolInput {
- /// High-level edit instructions. These will be interpreted by a smaller
- /// model, so explain the changes you want that model to make and which
- /// file paths need changing. The description should be concise and clear.
- ///
- /// WARNING: When specifying which file paths need changing, you MUST
- /// start each path with one of the project's root directories.
- ///
- /// WARNING: NEVER include code blocks or snippets in edit instructions.
- /// Only provide natural language descriptions of the changes needed! The tool will
- /// reject any instructions that contain code blocks or snippets.
- ///
- /// The following examples assume we have two root directories in the project:
- /// - root-1
- /// - root-2
- ///
- /// <example>
- /// If you want to introduce a new quit function to kill the process, your
- /// instructions should be: "Add a new `quit` function to
- /// `root-1/src/main.rs` to kill the process".
- ///
- /// Notice how the file path starts with root-1. Without that, the path
- /// would be ambiguous and the call would fail!
- /// </example>
- ///
- /// <example>
- /// If you want to change documentation to always start with a capital
- /// letter, your instructions should be: "In `root-2/db.js`,
- /// `root-2/inMemory.js` and `root-2/sql.js`, change all the documentation
- /// to start with a capital letter".
- ///
- /// Notice how we never specify code snippets in the instructions!
- /// </example>
- pub edit_instructions: String,
-
- /// A user-friendly description of what changes are being made.
- /// This will be shown to the user in the UI to describe the edit operation. The screen real estate for this UI will be extremely
- /// constrained, so make the description extremely terse.
- ///
- /// <example>
- /// For fixing a broken authentication system:
- /// "Fix auth bug in login flow"
- /// </example>
- ///
- /// <example>
- /// For adding unit tests to a module:
- /// "Add tests for user profile logic"
- /// </example>
- pub display_description: String,
-}
-
-pub struct EditFilesTool;
-
-impl Tool for EditFilesTool {
- fn name(&self) -> String {
- "edit_files".into()
- }
-
- fn needs_confirmation(&self) -> bool {
- false
- }
-
- fn description(&self) -> String {
- include_str!("./edit_files_tool/description.md").into()
- }
-
- fn icon(&self) -> IconName {
- IconName::Pencil
- }
-
- fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
- json_schema_for::<EditFilesToolInput>(format)
- }
-
- fn ui_text(&self, input: &serde_json::Value) -> String {
- match serde_json::from_value::<EditFilesToolInput>(input.clone()) {
- Ok(input) => input.display_description,
- Err(_) => "Edit files".to_string(),
- }
- }
-
- fn run(
- self: Arc<Self>,
- input: serde_json::Value,
- messages: &[LanguageModelRequestMessage],
- project: Entity<Project>,
- action_log: Entity<ActionLog>,
- cx: &mut App,
- ) -> Task<Result<String>> {
- let input = match serde_json::from_value::<EditFilesToolInput>(input) {
- Ok(input) => input,
- Err(err) => return Task::ready(Err(anyhow!(err))),
- };
-
- match EditToolLog::try_global(cx) {
- Some(log) => {
- let req_id = log.update(cx, |log, cx| {
- log.new_request(input.edit_instructions.clone(), cx)
- });
-
- let task = EditToolRequest::new(
- input,
- messages,
- project,
- action_log,
- Some((log.clone(), req_id)),
- cx,
- );
-
- cx.spawn(async move |cx| {
- let result = task.await;
-
- let str_result = match &result {
- Ok(out) => Ok(out.clone()),
- Err(err) => Err(err.to_string()),
- };
-
- log.update(cx, |log, cx| log.set_tool_output(req_id, str_result, cx))
- .log_err();
-
- result
- })
- }
-
- None => EditToolRequest::new(input, messages, project, action_log, None, cx),
- }
- }
-}
-
-struct EditToolRequest {
- parser: EditActionParser,
- editor_response: EditorResponse,
- project: Entity<Project>,
- action_log: Entity<ActionLog>,
- tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
-}
-
-enum EditorResponse {
- /// The editor model hasn't produced any actions yet.
- /// If we don't have any by the end, we'll return its message to the architect model.
- Message(String),
- /// The editor model produced at least one action.
- Actions {
- applied: Vec<AppliedAction>,
- search_errors: Vec<SearchError>,
- },
-}
-
-struct AppliedAction {
- source: String,
- buffer: Entity<language::Buffer>,
-}
-
-#[derive(Debug)]
-enum DiffResult {
- Diff(language::Diff),
- SearchError(SearchError),
-}
-
-#[derive(Debug)]
-enum SearchError {
- NoMatch {
- file_path: String,
- search: String,
- },
- EmptyBuffer {
- file_path: String,
- search: String,
- exists: bool,
- },
-}
-
-impl EditToolRequest {
- fn new(
- input: EditFilesToolInput,
- messages: &[LanguageModelRequestMessage],
- project: Entity<Project>,
- action_log: Entity<ActionLog>,
- tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
- cx: &mut App,
- ) -> Task<Result<String>> {
- let model_registry = LanguageModelRegistry::read_global(cx);
- let Some(ConfiguredModel { model, .. }) = model_registry.default_model() else {
- return Task::ready(Err(anyhow!("No model configured")));
- };
-
- let mut messages = messages.to_vec();
- // Remove the last tool use (this run) to prevent an invalid request
- 'outer: for message in messages.iter_mut().rev() {
- for (index, content) in message.content.iter().enumerate().rev() {
- match content {
- MessageContent::ToolUse(_) => {
- message.content.remove(index);
- break 'outer;
- }
- MessageContent::ToolResult(_) => {
- // If we find any tool results before a tool use, the request is already valid
- break 'outer;
- }
- MessageContent::Text(_) | MessageContent::Image(_) => {}
- }
- }
- }
-
- messages.push(LanguageModelRequestMessage {
- role: Role::User,
- content: vec![edit_model_prompt().into(), input.edit_instructions.into()],
- cache: false,
- });
-
- cx.spawn(async move |cx| {
- let llm_request = LanguageModelRequest {
- messages,
- tools: vec![],
- stop: vec![],
- temperature: Some(0.0),
- };
-
- let (mut tx, mut rx) = mpsc::channel::<String>(32);
- let stream = model.stream_completion_text(llm_request, &cx);
- let reader_task = cx.background_spawn(async move {
- let mut chunks = stream.await?;
-
- while let Some(chunk) = chunks.stream.next().await {
- if let Some(chunk) = chunk.log_err() {
- // we don't process here because the API fails
- // if we take too long between reads
- tx.send(chunk).await?
- }
- }
- tx.close().await?;
- anyhow::Ok(())
- });
-
- let mut request = Self {
- parser: EditActionParser::new(),
- editor_response: EditorResponse::Message(String::with_capacity(256)),
- action_log,
- project,
- tool_log,
- };
-
- while let Some(chunk) = rx.next().await {
- request.process_response_chunk(&chunk, cx).await?;
- }
-
- reader_task.await?;
-
- request.finalize(cx).await
- })
- }
-
- async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
- let new_actions = self.parser.parse_chunk(chunk);
-
- if let EditorResponse::Message(ref mut message) = self.editor_response {
- if new_actions.is_empty() {
- message.push_str(chunk);
- }
- }
-
- if let Some((ref log, req_id)) = self.tool_log {
- log.update(cx, |log, cx| {
- log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
- })
- .log_err();
- }
-
- for action in new_actions {
- self.apply_action(action, cx).await?;
- }
-
- Ok(())
- }
-
- async fn apply_action(
- &mut self,
- (action, source): (EditAction, String),
- cx: &mut AsyncApp,
- ) -> Result<()> {
- let project_path = self.project.read_with(cx, |project, cx| {
- project
- .find_project_path(action.file_path(), cx)
- .context("Path not found in project")
- })??;
-
- let buffer = self
- .project
- .update(cx, |project, cx| project.open_buffer(project_path, cx))?
- .await?;
-
- let result = match action {
- EditAction::Replace {
- old,
- new,
- file_path,
- } => {
- let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
-
- cx.background_executor()
- .spawn(Self::replace_diff(old, new, file_path, snapshot))
- .await
- }
- EditAction::Write { content, .. } => Ok(DiffResult::Diff(
- buffer
- .read_with(cx, |buffer, cx| buffer.diff(content, cx))?
- .await,
- )),
- }?;
-
- match result {
- DiffResult::SearchError(error) => {
- self.push_search_error(error);
- }
- DiffResult::Diff(diff) => {
- cx.update(|cx| {
- self.action_log
- .update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
- buffer.update(cx, |buffer, cx| {
- buffer.finalize_last_transaction();
- buffer.apply_diff(diff, cx);
- buffer.finalize_last_transaction();
- });
- self.action_log
- .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
- })?;
-
- self.push_applied_action(AppliedAction { source, buffer });
- }
- }
-
- anyhow::Ok(())
- }
-
- fn push_search_error(&mut self, error: SearchError) {
- match &mut self.editor_response {
- EditorResponse::Message(_) => {
- self.editor_response = EditorResponse::Actions {
- applied: Vec::new(),
- search_errors: vec![error],
- };
- }
- EditorResponse::Actions { search_errors, .. } => {
- search_errors.push(error);
- }
- }
- }
-
- fn push_applied_action(&mut self, action: AppliedAction) {
- match &mut self.editor_response {
- EditorResponse::Message(_) => {
- self.editor_response = EditorResponse::Actions {
- applied: vec![action],
- search_errors: Vec::new(),
- };
- }
- EditorResponse::Actions { applied, .. } => {
- applied.push(action);
- }
- }
- }
-
- async fn replace_diff(
- old: String,
- new: String,
- file_path: std::path::PathBuf,
- snapshot: language::BufferSnapshot,
- ) -> Result<DiffResult> {
- if snapshot.is_empty() {
- let exists = snapshot
- .file()
- .map_or(false, |file| file.disk_state().exists());
-
- let error = SearchError::EmptyBuffer {
- file_path: file_path.display().to_string(),
- exists,
- search: old,
- };
-
- return Ok(DiffResult::SearchError(error));
- }
-
- let replace_result =
- // Try to match exactly
- replace_exact(&old, &new, &snapshot)
- .await
- // If that fails, try being flexible about indentation
- .or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
-
- let Some(diff) = replace_result else {
- let error = SearchError::NoMatch {
- search: old,
- file_path: file_path.display().to_string(),
- };
-
- return Ok(DiffResult::SearchError(error));
- };
-
- Ok(DiffResult::Diff(diff))
- }
-
- async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
- match self.editor_response {
- EditorResponse::Message(message) => Err(anyhow!(
- "No edits were applied! You might need to provide more context.\n\n{}",
- message
- )),
- EditorResponse::Actions {
- applied,
- search_errors,
- } => {
- let mut output = String::with_capacity(1024);
-
- let parse_errors = self.parser.errors();
- let has_errors = !search_errors.is_empty() || !parse_errors.is_empty();
-
- if has_errors {
- let error_count = search_errors.len() + parse_errors.len();
-
- if applied.is_empty() {
- writeln!(
- &mut output,
- "{} errors occurred! No edits were applied.",
- error_count,
- )?;
- } else {
- writeln!(
- &mut output,
- "{} errors occurred, but {} edits were correctly applied.",
- error_count,
- applied.len(),
- )?;
-
- writeln!(
- &mut output,
- "# {} SEARCH/REPLACE block(s) applied:\n\nDo not re-send these since they are already applied!\n",
- applied.len()
- )?;
- }
- } else {
- write!(
- &mut output,
- "Successfully applied! Here's a list of applied edits:"
- )?;
- }
-
- let mut changed_buffers = HashSet::default();
-
- for action in applied {
- changed_buffers.insert(action.buffer.clone());
- write!(&mut output, "\n\n{}", action.source)?;
- }
-
- for buffer in &changed_buffers {
- self.project
- .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
- .await?;
- }
-
- if !search_errors.is_empty() {
- writeln!(
- &mut output,
- "\n\n## {} SEARCH/REPLACE block(s) failed to match:\n",
- search_errors.len()
- )?;
-
- for error in search_errors {
- match error {
- SearchError::NoMatch { file_path, search } => {
- writeln!(
- &mut output,
- "### No exact match in: `{}`\n```\n{}\n```\n",
- file_path, search,
- )?;
- }
- SearchError::EmptyBuffer {
- file_path,
- exists: true,
- search,
- } => {
- writeln!(
- &mut output,
- "### No match because `{}` is empty:\n```\n{}\n```\n",
- file_path, search,
- )?;
- }
- SearchError::EmptyBuffer {
- file_path,
- exists: false,
- search,
- } => {
- writeln!(
- &mut output,
- "### No match because `{}` does not exist:\n```\n{}\n```\n",
- file_path, search,
- )?;
- }
- }
- }
-
- write!(
- &mut output,
- "The SEARCH section must exactly match an existing block of lines including all white \
- space, comments, indentation, docstrings, etc."
- )?;
- }
-
- if !parse_errors.is_empty() {
- writeln!(
- &mut output,
- "\n\n## {} SEARCH/REPLACE blocks failed to parse:",
- parse_errors.len()
- )?;
-
- for error in parse_errors {
- writeln!(&mut output, "- {}", error)?;
- }
- }
-
- if has_errors {
- writeln!(
- &mut output,
- "\n\nYou can fix errors by running the tool again. You can include instructions, \
- but errors are part of the conversation so you don't need to repeat them.",
- )?;
-
- Err(anyhow!(output))
- } else {
- Ok(output)
- }
- }
- }
- }
-}
@@ -1,11 +0,0 @@
-Edit files in the current project by specifying instructions in natural language.
-
-IMPORTANT NOTE: If there is a find-replace tool, use that instead of this tool! This tool is only to be used as a fallback in case that tool is unavailable. Always prefer that tool if it is available.
-
-When using this tool, you should suggest one coherent edit that can be made to the codebase.
-
-When the set of edits you want to make is large or complex, feel free to invoke this tool multiple times, each time focusing on a specific change you wanna make.
-
-You should use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents, and you absolutely must never use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
-
-DO NOT call this tool until the code to be edited appears in the conversation! You must use the `read-files` tool or ask the user to add it to context first.
@@ -1,967 +0,0 @@
-use std::{
- mem::take,
- ops::Range,
- path::{Path, PathBuf},
-};
-use util::ResultExt;
-
-/// Represents an edit action to be performed on a file.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum EditAction {
- /// Replace specific content in a file with new content
- Replace {
- file_path: PathBuf,
- old: String,
- new: String,
- },
- /// Write content to a file (create or overwrite)
- Write { file_path: PathBuf, content: String },
-}
-
-impl EditAction {
- pub fn file_path(&self) -> &Path {
- match self {
- EditAction::Replace { file_path, .. } => file_path,
- EditAction::Write { file_path, .. } => file_path,
- }
- }
-}
-
-/// Parses edit actions from an LLM response.
-/// See system.md for more details on the format.
-#[derive(Debug)]
-pub struct EditActionParser {
- state: State,
- line: usize,
- column: usize,
- marker_ix: usize,
- action_source: Vec<u8>,
- fence_start_offset: usize,
- block_range: Range<usize>,
- old_range: Range<usize>,
- new_range: Range<usize>,
- errors: Vec<ParseError>,
-}
-
-#[derive(Debug, PartialEq, Eq)]
-enum State {
- /// Anywhere outside an action
- Default,
- /// After opening ```, in optional language tag
- OpenFence,
- /// In SEARCH marker
- SearchMarker,
- /// In search block or divider
- SearchBlock,
- /// In replace block or REPLACE marker
- ReplaceBlock,
- /// In closing ```
- CloseFence,
-}
-
-/// used to avoid having source code that looks like git-conflict markers
-macro_rules! marker_sym {
- ($char:expr) => {
- concat!($char, $char, $char, $char, $char, $char, $char)
- };
-}
-
-const SEARCH_MARKER: &str = concat!(marker_sym!('<'), " SEARCH");
-const DIVIDER: &str = marker_sym!('=');
-const NL_DIVIDER: &str = concat!("\n", marker_sym!('='));
-const REPLACE_MARKER: &str = concat!(marker_sym!('>'), " REPLACE");
-const NL_REPLACE_MARKER: &str = concat!("\n", marker_sym!('>'), " REPLACE");
-const FENCE: &str = "```";
-
-impl EditActionParser {
- /// Creates a new `EditActionParser`
- pub fn new() -> Self {
- Self {
- state: State::Default,
- line: 1,
- column: 0,
- action_source: Vec::new(),
- fence_start_offset: 0,
- marker_ix: 0,
- block_range: Range::default(),
- old_range: Range::default(),
- new_range: Range::default(),
- errors: Vec::new(),
- }
- }
-
- /// Processes a chunk of input text and returns any completed edit actions.
- ///
- /// This method can be called repeatedly with fragments of input. The parser
- /// maintains its state between calls, allowing you to process streaming input
- /// as it becomes available. Actions are only inserted once they are fully parsed.
- ///
- /// If a block fails to parse, it will simply be skipped and an error will be recorded.
- /// All errors can be accessed through the `EditActionsParser::errors` method.
- pub fn parse_chunk(&mut self, input: &str) -> Vec<(EditAction, String)> {
- use State::*;
-
- let mut actions = Vec::new();
-
- for byte in input.bytes() {
- // Update line and column tracking
- if byte == b'\n' {
- self.line += 1;
- self.column = 0;
- } else {
- self.column += 1;
- }
-
- let action_offset = self.action_source.len();
-
- match &self.state {
- Default => match self.match_marker(byte, FENCE, false) {
- MarkerMatch::Complete => {
- self.fence_start_offset = action_offset + 1 - FENCE.len();
- self.to_state(OpenFence);
- }
- MarkerMatch::Partial => {}
- MarkerMatch::None => {
- if self.marker_ix > 0 {
- self.marker_ix = 0;
- } else if self.action_source.ends_with(b"\n") {
- self.action_source.clear();
- }
- }
- },
- OpenFence => {
- // skip language tag
- if byte == b'\n' {
- self.to_state(SearchMarker);
- }
- }
- SearchMarker => {
- if self.expect_marker(byte, SEARCH_MARKER, true) {
- self.to_state(SearchBlock);
- }
- }
- SearchBlock => {
- if self.extend_block_range(byte, DIVIDER, NL_DIVIDER) {
- self.old_range = take(&mut self.block_range);
- self.to_state(ReplaceBlock);
- }
- }
- ReplaceBlock => {
- if self.extend_block_range(byte, REPLACE_MARKER, NL_REPLACE_MARKER) {
- self.new_range = take(&mut self.block_range);
- self.to_state(CloseFence);
- }
- }
- CloseFence => {
- if self.expect_marker(byte, FENCE, false) {
- self.action_source.push(byte);
-
- if let Some(action) = self.action() {
- actions.push(action);
- }
-
- self.errors();
- self.reset();
-
- continue;
- }
- }
- };
-
- self.action_source.push(byte);
- }
-
- actions
- }
-
- /// Returns a reference to the errors encountered during parsing.
- pub fn errors(&self) -> &[ParseError] {
- &self.errors
- }
-
- fn action(&mut self) -> Option<(EditAction, String)> {
- let old_range = take(&mut self.old_range);
- let new_range = take(&mut self.new_range);
-
- let action_source = take(&mut self.action_source);
- let action_source = String::from_utf8(action_source).log_err()?;
-
- let mut file_path_bytes = action_source[..self.fence_start_offset].to_owned();
-
- if file_path_bytes.ends_with("\n") {
- file_path_bytes.pop();
- if file_path_bytes.ends_with("\r") {
- file_path_bytes.pop();
- }
- }
-
- let file_path = PathBuf::from(file_path_bytes);
-
- if old_range.is_empty() {
- return Some((
- EditAction::Write {
- file_path,
- content: action_source[new_range].to_owned(),
- },
- action_source,
- ));
- }
-
- let old = action_source[old_range].to_owned();
- let new = action_source[new_range].to_owned();
-
- let action = EditAction::Replace {
- file_path,
- old,
- new,
- };
-
- Some((action, action_source))
- }
-
- fn to_state(&mut self, state: State) {
- self.state = state;
- self.marker_ix = 0;
- }
-
- fn reset(&mut self) {
- self.action_source.clear();
- self.block_range = Range::default();
- self.old_range = Range::default();
- self.new_range = Range::default();
- self.fence_start_offset = 0;
- self.marker_ix = 0;
- self.to_state(State::Default);
- }
-
- fn expect_marker(&mut self, byte: u8, marker: &'static str, trailing_newline: bool) -> bool {
- match self.match_marker(byte, marker, trailing_newline) {
- MarkerMatch::Complete => true,
- MarkerMatch::Partial => false,
- MarkerMatch::None => {
- self.errors.push(ParseError {
- line: self.line,
- column: self.column,
- expected: marker,
- found: byte,
- });
-
- self.reset();
- false
- }
- }
- }
-
- fn extend_block_range(&mut self, byte: u8, marker: &str, nl_marker: &str) -> bool {
- let marker = if self.block_range.is_empty() {
- // do not require another newline if block is empty
- marker
- } else {
- nl_marker
- };
-
- let offset = self.action_source.len();
-
- match self.match_marker(byte, marker, true) {
- MarkerMatch::Complete => {
- if self.action_source[self.block_range.clone()].ends_with(b"\r") {
- self.block_range.end -= 1;
- }
-
- true
- }
- MarkerMatch::Partial => false,
- MarkerMatch::None => {
- if self.marker_ix > 0 {
- self.marker_ix = 0;
- self.block_range.end = offset;
-
- // The beginning of marker might match current byte
- match self.match_marker(byte, marker, true) {
- MarkerMatch::Complete => return true,
- MarkerMatch::Partial => return false,
- MarkerMatch::None => { /* no match, keep collecting */ }
- }
- }
-
- if self.block_range.is_empty() {
- self.block_range.start = offset;
- }
- self.block_range.end = offset + 1;
-
- false
- }
- }
- }
-
- fn match_marker(&mut self, byte: u8, marker: &str, trailing_newline: bool) -> MarkerMatch {
- if trailing_newline && self.marker_ix >= marker.len() {
- if byte == b'\n' {
- MarkerMatch::Complete
- } else if byte == b'\r' {
- MarkerMatch::Partial
- } else {
- MarkerMatch::None
- }
- } else if byte == marker.as_bytes()[self.marker_ix] {
- self.marker_ix += 1;
-
- if self.marker_ix < marker.len() || trailing_newline {
- MarkerMatch::Partial
- } else {
- MarkerMatch::Complete
- }
- } else {
- MarkerMatch::None
- }
- }
-}
-
-#[derive(Debug)]
-enum MarkerMatch {
- None,
- Partial,
- Complete,
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub struct ParseError {
- line: usize,
- column: usize,
- expected: &'static str,
- found: u8,
-}
-
-impl std::fmt::Display for ParseError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "input:{}:{}: Expected marker {:?}, found {:?}",
- self.line, self.column, self.expected, self.found as char
- )
- }
-}
-
-pub fn edit_model_prompt() -> String {
- include_str!("edit_prompt.md")
- .to_string()
- .replace("{{SEARCH_MARKER}}", SEARCH_MARKER)
- .replace("{{DIVIDER}}", DIVIDER)
- .replace("{{REPLACE_MARKER}}", REPLACE_MARKER)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use rand::prelude::*;
- use util::line_endings;
-
- const WRONG_MARKER: &str = concat!(marker_sym!('<'), " WRONG_MARKER");
-
- #[test]
- fn test_simple_edit_action() {
- // Construct test input using format with multiline string literals
- let input = format!(
- r#"src/main.rs
-```
-{}
-fn original() {{}}
-{}
-fn replacement() {{}}
-{}
-```
-"#,
- SEARCH_MARKER, DIVIDER, REPLACE_MARKER
- );
-
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&input);
-
- assert_no_errors(&parser);
- assert_eq!(actions.len(), 1);
- assert_eq!(
- actions[0].0,
- EditAction::Replace {
- file_path: PathBuf::from("src/main.rs"),
- old: "fn original() {}".to_string(),
- new: "fn replacement() {}".to_string(),
- }
- );
- }
-
- #[test]
- fn test_with_language_tag() {
- // Construct test input using format with multiline string literals
- let input = format!(
- r#"src/main.rs
-```rust
-{}
-fn original() {{}}
-{}
-fn replacement() {{}}
-{}
-```
-"#,
- SEARCH_MARKER, DIVIDER, REPLACE_MARKER
- );
-
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&input);
-
- assert_no_errors(&parser);
- assert_eq!(actions.len(), 1);
- assert_eq!(
- actions[0].0,
- EditAction::Replace {
- file_path: PathBuf::from("src/main.rs"),
- old: "fn original() {}".to_string(),
- new: "fn replacement() {}".to_string(),
- }
- );
- }
-
- #[test]
- fn test_with_surrounding_text() {
- // Construct test input using format with multiline string literals
- let input = format!(
- r#"Here's a modification I'd like to make to the file:
-
-src/main.rs
-```rust
-{}
-fn original() {{}}
-{}
-fn replacement() {{}}
-{}
-```
-
-This change makes the function better.
-"#,
- SEARCH_MARKER, DIVIDER, REPLACE_MARKER
- );
-
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&input);
-
- assert_no_errors(&parser);
- assert_eq!(actions.len(), 1);
- assert_eq!(
- actions[0].0,
- EditAction::Replace {
- file_path: PathBuf::from("src/main.rs"),
- old: "fn original() {}".to_string(),
- new: "fn replacement() {}".to_string(),
- }
- );
- }
-
- #[test]
- fn test_multiple_edit_actions() {
- // Construct test input using format with multiline string literals
- let input = format!(
- r#"First change:
-src/main.rs
-```
-{}
-fn original() {{}}
-{}
-fn replacement() {{}}
-{}
-```
-
-Second change:
-src/utils.rs
-```rust
-{}
-fn old_util() -> bool {{ false }}
-{}
-fn new_util() -> bool {{ true }}
-{}
-```
-"#,
- SEARCH_MARKER, DIVIDER, REPLACE_MARKER, SEARCH_MARKER, DIVIDER, REPLACE_MARKER
- );
-
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&input);
-
- assert_no_errors(&parser);
- assert_eq!(actions.len(), 2);
-
- let (action, _) = &actions[0];
- assert_eq!(
- action,
- &EditAction::Replace {
- file_path: PathBuf::from("src/main.rs"),
- old: "fn original() {}".to_string(),
- new: "fn replacement() {}".to_string(),
- }
- );
- let (action2, _) = &actions[1];
- assert_eq!(
- action2,
- &EditAction::Replace {
- file_path: PathBuf::from("src/utils.rs"),
- old: "fn old_util() -> bool { false }".to_string(),
- new: "fn new_util() -> bool { true }".to_string(),
- }
- );
- }
-
- #[test]
- fn test_multiline() {
- // Construct test input using format with multiline string literals
- let input = format!(
- r#"src/main.rs
-```rust
-{}
-fn original() {{
- println!("This is the original function");
- let x = 42;
- if x > 0 {{
- println!("Positive number");
- }}
-}}
-{}
-fn replacement() {{
- println!("This is the replacement function");
- let x = 100;
- if x > 50 {{
- println!("Large number");
- }} else {{
- println!("Small number");
- }}
-}}
-{}
-```
-"#,
- SEARCH_MARKER, DIVIDER, REPLACE_MARKER
- );
-
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&input);
-
- assert_no_errors(&parser);
- assert_eq!(actions.len(), 1);
-
- let (action, _) = &actions[0];
- assert_eq!(
- action,
- &EditAction::Replace {
- file_path: PathBuf::from("src/main.rs"),
- old: "fn original() {\n println!(\"This is the original function\");\n let x = 42;\n if x > 0 {\n println!(\"Positive number\");\n }\n}".to_string(),
- new: "fn replacement() {\n println!(\"This is the replacement function\");\n let x = 100;\n if x > 50 {\n println!(\"Large number\");\n } else {\n println!(\"Small number\");\n }\n}".to_string(),
- }
- );
- }
-
- #[test]
- fn test_write_action() {
- // Construct test input using format with multiline string literals
- let input = format!(
- r#"Create a new main.rs file:
-
-src/main.rs
-```rust
-{}
-{}
-fn new_function() {{
- println!("This function is being added");
-}}
-{}
-```
-"#,
- SEARCH_MARKER, DIVIDER, REPLACE_MARKER
- );
-
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&input);
-
- assert_no_errors(&parser);
- assert_eq!(actions.len(), 1);
- assert_eq!(
- actions[0].0,
- EditAction::Write {
- file_path: PathBuf::from("src/main.rs"),
- content: "fn new_function() {\n println!(\"This function is being added\");\n}"
- .to_string(),
- }
- );
- }
-
- #[test]
- fn test_empty_replace() {
- // Construct test input using format with multiline string literals
- let input = format!(
- r#"src/main.rs
-```rust
-{}
-fn this_will_be_deleted() {{
- println!("Deleting this function");
-}}
-{}
-{}
-```
-"#,
- SEARCH_MARKER, DIVIDER, REPLACE_MARKER
- );
-
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&input);
-
- assert_no_errors(&parser);
- assert_eq!(actions.len(), 1);
- assert_eq!(
- actions[0].0,
- EditAction::Replace {
- file_path: PathBuf::from("src/main.rs"),
- old: "fn this_will_be_deleted() {\n println!(\"Deleting this function\");\n}"
- .to_string(),
- new: "".to_string(),
- }
- );
-
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&input.replace("\n", "\r\n"));
- assert_no_errors(&parser);
- assert_eq!(actions.len(), 1);
- assert_eq!(
- actions[0].0,
- EditAction::Replace {
- file_path: PathBuf::from("src/main.rs"),
- old:
- "fn this_will_be_deleted() {\r\n println!(\"Deleting this function\");\r\n}"
- .to_string(),
- new: "".to_string(),
- }
- );
- }
-
- #[test]
- fn test_empty_both() {
- // Construct test input using format with multiline string literals
- let input = format!(
- r#"src/main.rs
-```rust
-{}
-{}
-{}
-```
-"#,
- SEARCH_MARKER, DIVIDER, REPLACE_MARKER
- );
-
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&input);
-
- assert_eq!(actions.len(), 1);
- assert_eq!(
- actions[0].0,
- EditAction::Write {
- file_path: PathBuf::from("src/main.rs"),
- content: String::new(),
- }
- );
- assert_no_errors(&parser);
- }
-
- #[test]
- fn test_resumability() {
- // Construct test input using format with multiline string literals
- let input_part1 = format!("src/main.rs\n```rust\n{}\nfn ori", SEARCH_MARKER);
-
- let input_part2 = format!("ginal() {{}}\n{}\nfn replacement() {{}}", DIVIDER);
-
- let input_part3 = format!("\n{}\n```\n", REPLACE_MARKER);
-
- let mut parser = EditActionParser::new();
- let actions1 = parser.parse_chunk(&input_part1);
- assert_no_errors(&parser);
- assert_eq!(actions1.len(), 0);
-
- let actions2 = parser.parse_chunk(&input_part2);
- // No actions should be complete yet
- assert_no_errors(&parser);
- assert_eq!(actions2.len(), 0);
-
- let actions3 = parser.parse_chunk(&input_part3);
- // The third chunk should complete the action
- assert_no_errors(&parser);
- assert_eq!(actions3.len(), 1);
- let (action, _) = &actions3[0];
- assert_eq!(
- action,
- &EditAction::Replace {
- file_path: PathBuf::from("src/main.rs"),
- old: "fn original() {}".to_string(),
- new: "fn replacement() {}".to_string(),
- }
- );
- }
-
- #[test]
- fn test_parser_state_preservation() {
- let mut parser = EditActionParser::new();
- let first_chunk = format!("src/main.rs\n```rust\n{}\n", SEARCH_MARKER);
- let actions1 = parser.parse_chunk(&first_chunk);
-
- // Check parser is in the correct state
- assert_no_errors(&parser);
- assert_eq!(parser.state, State::SearchBlock);
- assert_eq!(parser.action_source, first_chunk.as_bytes());
-
- // Continue parsing
- let second_chunk = format!("original code\n{}\n", DIVIDER);
- let actions2 = parser.parse_chunk(&second_chunk);
-
- assert_no_errors(&parser);
- assert_eq!(parser.state, State::ReplaceBlock);
- assert_eq!(
- &parser.action_source[parser.old_range.clone()],
- b"original code"
- );
-
- let third_chunk = format!("replacement code\n{}\n```\n", REPLACE_MARKER);
- let actions3 = parser.parse_chunk(&third_chunk);
-
- // After complete parsing, state should reset
- assert_no_errors(&parser);
- assert_eq!(parser.state, State::Default);
- assert_eq!(parser.action_source, b"\n");
- assert!(parser.old_range.is_empty());
- assert!(parser.new_range.is_empty());
-
- assert_eq!(actions1.len(), 0);
- assert_eq!(actions2.len(), 0);
- assert_eq!(actions3.len(), 1);
- }
-
- #[test]
- fn test_invalid_search_marker() {
- let input = format!(
- r#"src/main.rs
-```rust
-{}
-fn original() {{}}
-{}
-fn replacement() {{}}
-{}
-```
-"#,
- WRONG_MARKER, DIVIDER, REPLACE_MARKER
- );
-
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&input);
- assert_eq!(actions.len(), 0);
-
- assert_eq!(parser.errors().len(), 1);
- let error = &parser.errors()[0];
-
- assert_eq!(
- error.to_string(),
- format!(
- "input:3:9: Expected marker \"{}\", found 'W'",
- SEARCH_MARKER
- )
- );
- }
-
- #[test]
- fn test_missing_closing_fence() {
- // Construct test input using format with multiline string literals
- let input = format!(
- r#"src/main.rs
-```rust
-{}
-fn original() {{}}
-{}
-fn replacement() {{}}
-{}
-<!-- Missing closing fence -->
-
-src/utils.rs
-```rust
-{}
-fn utils_func() {{}}
-{}
-fn new_utils_func() {{}}
-{}
-```
-"#,
- SEARCH_MARKER, DIVIDER, REPLACE_MARKER, SEARCH_MARKER, DIVIDER, REPLACE_MARKER
- );
-
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&input);
-
- // Only the second block should be parsed
- assert_eq!(actions.len(), 1);
- let (action, _) = &actions[0];
- assert_eq!(
- action,
- &EditAction::Replace {
- file_path: PathBuf::from("src/utils.rs"),
- old: "fn utils_func() {}".to_string(),
- new: "fn new_utils_func() {}".to_string(),
- }
- );
- assert_eq!(parser.errors().len(), 1);
- assert_eq!(
- parser.errors()[0].to_string(),
- "input:8:1: Expected marker \"```\", found '<'"
- );
-
- // The parser should continue after an error
- assert_eq!(parser.state, State::Default);
- }
-
- #[test]
- fn test_parse_examples_in_edit_prompt() {
- let mut parser = EditActionParser::new();
- let actions = parser.parse_chunk(&edit_model_prompt());
- assert_examples_in_edit_prompt(&actions, parser.errors());
- }
-
- #[gpui::test(iterations = 10)]
- fn test_random_chunking_of_edit_prompt(mut rng: StdRng) {
- let mut parser = EditActionParser::new();
- let mut remaining: &str = &edit_model_prompt();
- let mut actions = Vec::with_capacity(5);
-
- while !remaining.is_empty() {
- let chunk_size = rng.gen_range(1..=std::cmp::min(remaining.len(), 100));
-
- let (chunk, rest) = remaining.split_at(chunk_size);
-
- let chunk_actions = parser.parse_chunk(chunk);
- actions.extend(chunk_actions);
- remaining = rest;
- }
-
- assert_examples_in_edit_prompt(&actions, parser.errors());
- }
-
- fn assert_examples_in_edit_prompt(actions: &[(EditAction, String)], errors: &[ParseError]) {
- assert_eq!(actions.len(), 5);
-
- assert_eq!(
- actions[0].0,
- EditAction::Replace {
- file_path: PathBuf::from("mathweb/flask/app.py"),
- old: "from flask import Flask".to_string(),
- new: line_endings!("import math\nfrom flask import Flask").to_string(),
- },
- );
-
- assert_eq!(
- actions[1].0,
- EditAction::Replace {
- file_path: PathBuf::from("mathweb/flask/app.py"),
- old: line_endings!("def factorial(n):\n \"compute factorial\"\n\n if n == 0:\n return 1\n else:\n return n * factorial(n-1)\n").to_string(),
- new: "".to_string(),
- }
- );
-
- assert_eq!(
- actions[2].0,
- EditAction::Replace {
- file_path: PathBuf::from("mathweb/flask/app.py"),
- old: " return str(factorial(n))".to_string(),
- new: " return str(math.factorial(n))".to_string(),
- },
- );
-
- assert_eq!(
- actions[3].0,
- EditAction::Write {
- file_path: PathBuf::from("hello.py"),
- content: line_endings!(
- "def hello():\n \"print a greeting\"\n\n print(\"hello\")"
- )
- .to_string(),
- },
- );
-
- assert_eq!(
- actions[4].0,
- EditAction::Replace {
- file_path: PathBuf::from("main.py"),
- old: line_endings!(
- "def hello():\n \"print a greeting\"\n\n print(\"hello\")"
- )
- .to_string(),
- new: "from hello import hello".to_string(),
- },
- );
-
- // The system prompt includes some text that would produce errors
- assert_eq!(
- errors[0].to_string(),
- format!(
- "input:102:1: Expected marker \"{}\", found '3'",
- SEARCH_MARKER
- )
- );
- #[cfg(not(windows))]
- assert_eq!(
- errors[1].to_string(),
- format!(
- "input:109:0: Expected marker \"{}\", found '\\n'",
- SEARCH_MARKER
- )
- );
- #[cfg(windows)]
- assert_eq!(
- errors[1].to_string(),
- format!(
- "input:108:1: Expected marker \"{}\", found '\\r'",
- SEARCH_MARKER
- )
- );
- }
-
- #[test]
- fn test_print_error() {
- let input = format!(
- r#"src/main.rs
-```rust
-{}
-fn original() {{}}
-{}
-fn replacement() {{}}
-{}
-```
-"#,
- WRONG_MARKER, DIVIDER, REPLACE_MARKER
- );
-
- let mut parser = EditActionParser::new();
- parser.parse_chunk(&input);
-
- assert_eq!(parser.errors().len(), 1);
- let error = &parser.errors()[0];
- let expected_error = format!(
- r#"input:3:9: Expected marker "{}", found 'W'"#,
- SEARCH_MARKER
- );
-
- assert_eq!(format!("{}", error), expected_error);
- }
-
- // helpers
-
- fn assert_no_errors(parser: &EditActionParser) {
- let errors = parser.errors();
-
- assert!(
- errors.is_empty(),
- "Expected no errors, but found:\n\n{}",
- errors
- .iter()
- .map(|e| e.to_string())
- .collect::<Vec<String>>()
- .join("\n")
- );
- }
-}
@@ -1,134 +0,0 @@
-Act as an expert software developer.
-Always use best practices when coding.
-Respect and use existing conventions, libraries, etc that are already present in the code base.
-
-Take requests for changes to the supplied code.
-Always reply to the user in the same language they are using.
-
-Once you understand the request, you MUST:
-
-1. Decide if you need to propose *SEARCH/REPLACE* edits to any files that haven't been added to the chat. You can create new files without asking!
-
-2. Think step-by-step and explain the needed changes in a few short sentences.
-
-3. Describe each change with a *SEARCH/REPLACE block* per the examples below.
-
-All changes to files must use this *SEARCH/REPLACE block* format.
-ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
-
-# Example conversations:
-
-## USER: Change get_factorial() to use math.factorial
-
-## ASSISTANT: To make this change we need to modify `mathweb/flask/app.py` to:
-
-1. Import the math package.
-2. Remove the existing factorial() function.
-3. Update get_factorial() to call math.factorial instead.
-
-Here are the *SEARCH/REPLACE* blocks:
-
-mathweb/flask/app.py
-```python
-{{SEARCH_MARKER}}
-from flask import Flask
-{{DIVIDER}}
-import math
-from flask import Flask
-{{REPLACE_MARKER}}
-```
-
-mathweb/flask/app.py
-```python
-{{SEARCH_MARKER}}
-def factorial(n):
- "compute factorial"
-
- if n == 0:
- return 1
- else:
- return n * factorial(n-1)
-
-{{DIVIDER}}
-{{REPLACE_MARKER}}
-```
-
-mathweb/flask/app.py
-```python
-{{SEARCH_MARKER}}
- return str(factorial(n))
-{{DIVIDER}}
- return str(math.factorial(n))
-{{REPLACE_MARKER}}
-```
-
-
-## USER: Refactor hello() into its own file.
-
-## ASSISTANT: To make this change we need to modify `main.py` and make a new file `hello.py`:
-
-1. Make a new hello.py file with hello() in it.
-2. Remove hello() from main.py and replace it with an import.
-
-Here are the *SEARCH/REPLACE* blocks:
-
-hello.py
-```python
-{{SEARCH_MARKER}}
-{{DIVIDER}}
-def hello():
- "print a greeting"
-
- print("hello")
-{{REPLACE_MARKER}}
-```
-
-main.py
-```python
-{{SEARCH_MARKER}}
-def hello():
- "print a greeting"
-
- print("hello")
-{{DIVIDER}}
-from hello import hello
-{{REPLACE_MARKER}}
-```
-# *SEARCH/REPLACE block* Rules:
-
-Every *SEARCH/REPLACE block* must use this format:
-1. The *FULL* file path alone on a line, verbatim. No bold asterisks, no quotes around it, no escaping of characters, etc.
-2. The opening fence and code language, eg: ```python
-3. The start of search block: {{SEARCH_MARKER}}
-4. A contiguous chunk of lines to search for in the existing source code
-5. The dividing line: {{DIVIDER}}
-6. The lines to replace into the source code
-7. The end of the replace block: {{REPLACE_MARKER}}
-8. The closing fence: ```
-
-Use the *FULL* file path, as shown to you by the user. Make sure to include the project's root directory name at the start of the path. *NEVER* specify the absolute path of the file!
-
-Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc.
-If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup.
-
-*SEARCH/REPLACE* blocks will *only* replace the first match occurrence.
-Including multiple unique *SEARCH/REPLACE* blocks if needed.
-Include enough lines in each SEARCH section to uniquely match each set of lines that need to change.
-
-Keep *SEARCH/REPLACE* blocks concise.
-Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each change a small portion of the file.
-Include just the changing lines, and a few surrounding lines if needed for uniqueness.
-Do not include long runs of unchanging lines in *SEARCH/REPLACE* blocks.
-
-Only create *SEARCH/REPLACE* blocks for files that have been read! Even though the conversation includes `read-file` tool results, you *CANNOT* issue your own reads. If the conversation doesn't include the code you need to edit, ask for it to be read explicitly.
-
-To move code within a file, use 2 *SEARCH/REPLACE* blocks: 1 to delete it from its current location, 1 to insert it in the new location.
-
-Pay attention to which filenames the user wants you to edit, especially if they are asking you to create a new file.
-
-If you want to put code in a new file, use a *SEARCH/REPLACE block* with:
-- A new file path, including dir name if needed
-- An empty `SEARCH` section
-- The new file's contents in the `REPLACE` section
-
-ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
@@ -1,417 +0,0 @@
-use std::path::Path;
-
-use collections::HashSet;
-use feature_flags::FeatureFlagAppExt;
-use gpui::{
- App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
- SharedString, Subscription, Window, actions, list, prelude::*,
-};
-use release_channel::ReleaseChannel;
-use settings::Settings;
-use ui::prelude::*;
-use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent};
-
-use super::edit_action::EditAction;
-
-actions!(debug, [EditTool]);
-
-pub fn init(cx: &mut App) {
- if cx.is_staff() || ReleaseChannel::global(cx) == ReleaseChannel::Dev {
- // Track events even before opening the log
- EditToolLog::global(cx);
- }
-
- cx.observe_new(|workspace: &mut Workspace, _, _| {
- workspace.register_action(|workspace, _: &EditTool, window, cx| {
- let viewer = cx.new(EditToolLogViewer::new);
- workspace.add_item_to_active_pane(Box::new(viewer), None, true, window, cx)
- });
- })
- .detach();
-}
-
-pub struct GlobalEditToolLog(Entity<EditToolLog>);
-
-impl Global for GlobalEditToolLog {}
-
-#[derive(Default)]
-pub struct EditToolLog {
- requests: Vec<EditToolRequest>,
-}
-
-#[derive(Clone, Copy, Hash, Eq, PartialEq)]
-pub struct EditToolRequestId(u32);
-
-impl EditToolLog {
- pub fn global(cx: &mut App) -> Entity<Self> {
- match Self::try_global(cx) {
- Some(entity) => entity,
- None => {
- let entity = cx.new(|_cx| Self::default());
- cx.set_global(GlobalEditToolLog(entity.clone()));
- entity
- }
- }
- }
-
- pub fn try_global(cx: &App) -> Option<Entity<Self>> {
- cx.try_global::<GlobalEditToolLog>()
- .map(|log| log.0.clone())
- }
-
- pub fn new_request(
- &mut self,
- instructions: String,
- cx: &mut Context<Self>,
- ) -> EditToolRequestId {
- let id = EditToolRequestId(self.requests.len() as u32);
- self.requests.push(EditToolRequest {
- id,
- instructions,
- editor_response: None,
- tool_output: None,
- parsed_edits: Vec::new(),
- });
- cx.emit(EditToolLogEvent::Inserted);
- id
- }
-
- pub fn push_editor_response_chunk(
- &mut self,
- id: EditToolRequestId,
- chunk: &str,
- new_actions: &[(EditAction, String)],
- cx: &mut Context<Self>,
- ) {
- if let Some(request) = self.requests.get_mut(id.0 as usize) {
- match &mut request.editor_response {
- None => {
- request.editor_response = Some(chunk.to_string());
- }
- Some(response) => {
- response.push_str(chunk);
- }
- }
- request
- .parsed_edits
- .extend(new_actions.iter().cloned().map(|(action, _)| action));
-
- cx.emit(EditToolLogEvent::Updated);
- }
- }
-
- pub fn set_tool_output(
- &mut self,
- id: EditToolRequestId,
- tool_output: Result<String, String>,
- cx: &mut Context<Self>,
- ) {
- if let Some(request) = self.requests.get_mut(id.0 as usize) {
- request.tool_output = Some(tool_output);
- cx.emit(EditToolLogEvent::Updated);
- }
- }
-}
-
-enum EditToolLogEvent {
- Inserted,
- Updated,
-}
-
-impl EventEmitter<EditToolLogEvent> for EditToolLog {}
-
-pub struct EditToolRequest {
- id: EditToolRequestId,
- instructions: String,
- // we don't use a result here because the error might have occurred after we got a response
- editor_response: Option<String>,
- parsed_edits: Vec<EditAction>,
- tool_output: Option<Result<String, String>>,
-}
-
-pub struct EditToolLogViewer {
- focus_handle: FocusHandle,
- log: Entity<EditToolLog>,
- list_state: ListState,
- expanded_edits: HashSet<(EditToolRequestId, usize)>,
- _subscription: Subscription,
-}
-
-impl EditToolLogViewer {
- pub fn new(cx: &mut Context<Self>) -> Self {
- let log = EditToolLog::global(cx);
-
- let subscription = cx.subscribe(&log, Self::handle_log_event);
-
- Self {
- focus_handle: cx.focus_handle(),
- log: log.clone(),
- list_state: ListState::new(
- log.read(cx).requests.len(),
- ListAlignment::Bottom,
- px(1024.),
- {
- let this = cx.entity().downgrade();
- move |ix, window: &mut Window, cx: &mut App| {
- this.update(cx, |this, cx| this.render_request(ix, window, cx))
- .unwrap()
- }
- },
- ),
- expanded_edits: HashSet::default(),
- _subscription: subscription,
- }
- }
-
- fn handle_log_event(
- &mut self,
- _: Entity<EditToolLog>,
- event: &EditToolLogEvent,
- cx: &mut Context<Self>,
- ) {
- match event {
- EditToolLogEvent::Inserted => {
- let count = self.list_state.item_count();
- self.list_state.splice(count..count, 1);
- }
- EditToolLogEvent::Updated => {}
- }
-
- cx.notify();
- }
-
- fn render_request(
- &self,
- index: usize,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) -> AnyElement {
- let requests = &self.log.read(cx).requests;
- let request = &requests[index];
-
- v_flex()
- .gap_3()
- .child(Self::render_section(IconName::ArrowRight, "Tool Input"))
- .child(request.instructions.clone())
- .py_5()
- .when(index + 1 < requests.len(), |element| {
- element
- .border_b_1()
- .border_color(cx.theme().colors().border)
- })
- .map(|parent| match &request.editor_response {
- None => {
- if request.tool_output.is_none() {
- parent.child("...")
- } else {
- parent
- }
- }
- Some(response) => parent
- .child(Self::render_section(
- IconName::ZedAssistant,
- "Editor Response",
- ))
- .child(Label::new(response.clone()).buffer_font(cx)),
- })
- .when(!request.parsed_edits.is_empty(), |parent| {
- parent
- .child(Self::render_section(IconName::Microscope, "Parsed Edits"))
- .child(
- v_flex()
- .gap_2()
- .children(request.parsed_edits.iter().enumerate().map(
- |(index, edit)| {
- self.render_edit_action(edit, request.id, index, cx)
- },
- )),
- )
- })
- .when_some(request.tool_output.as_ref(), |parent, output| {
- parent
- .child(Self::render_section(IconName::ArrowLeft, "Tool Output"))
- .child(match output {
- Ok(output) => Label::new(output.clone()).color(Color::Success),
- Err(error) => Label::new(error.clone()).color(Color::Error),
- })
- })
- .into_any()
- }
-
- fn render_section(icon: IconName, title: &'static str) -> AnyElement {
- h_flex()
- .gap_1()
- .child(Icon::new(icon).color(Color::Muted))
- .child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
- .into_any()
- }
-
- fn render_edit_action(
- &self,
- edit_action: &EditAction,
- request_id: EditToolRequestId,
- index: usize,
- cx: &Context<Self>,
- ) -> AnyElement {
- let expanded_id = (request_id, index);
-
- match edit_action {
- EditAction::Replace {
- file_path,
- old,
- new,
- } => self
- .render_edit_action_container(
- expanded_id,
- &file_path,
- [
- Self::render_block(IconName::MagnifyingGlass, "Search", old.clone(), cx)
- .border_r_1()
- .border_color(cx.theme().colors().border)
- .into_any(),
- Self::render_block(IconName::Replace, "Replace", new.clone(), cx)
- .into_any(),
- ],
- cx,
- )
- .into_any(),
- EditAction::Write { file_path, content } => self
- .render_edit_action_container(
- expanded_id,
- &file_path,
- [
- Self::render_block(IconName::Pencil, "Write", content.clone(), cx)
- .into_any(),
- ],
- cx,
- )
- .into_any(),
- }
- }
-
- fn render_edit_action_container(
- &self,
- expanded_id: (EditToolRequestId, usize),
- file_path: &Path,
- content: impl IntoIterator<Item = AnyElement>,
- cx: &Context<Self>,
- ) -> AnyElement {
- let is_expanded = self.expanded_edits.contains(&expanded_id);
-
- v_flex()
- .child(
- h_flex()
- .bg(cx.theme().colors().element_background)
- .border_1()
- .border_color(cx.theme().colors().border)
- .rounded_t_md()
- .when(!is_expanded, |el| el.rounded_b_md())
- .py_1()
- .px_2()
- .gap_1()
- .child(
- ui::Disclosure::new(ElementId::Integer(expanded_id.1), is_expanded)
- .on_click(cx.listener(move |this, _ev, _window, cx| {
- if is_expanded {
- this.expanded_edits.remove(&expanded_id);
- } else {
- this.expanded_edits.insert(expanded_id);
- }
-
- cx.notify();
- })),
- )
- .child(Label::new(file_path.display().to_string()).size(LabelSize::Small)),
- )
- .child(if is_expanded {
- h_flex()
- .border_1()
- .border_t_0()
- .border_color(cx.theme().colors().border)
- .rounded_b_md()
- .children(content)
- .into_any()
- } else {
- Empty.into_any()
- })
- .into_any()
- }
-
- fn render_block(icon: IconName, title: &'static str, content: String, cx: &App) -> Div {
- v_flex()
- .p_1()
- .gap_1()
- .flex_1()
- .h_full()
- .child(
- h_flex()
- .gap_1()
- .child(Icon::new(icon).color(Color::Muted))
- .child(Label::new(title).size(LabelSize::Small).color(Color::Muted)),
- )
- .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
- .text_sm()
- .child(content)
- .child(div().flex_1())
- }
-}
-
-impl EventEmitter<()> for EditToolLogViewer {}
-
-impl Focusable for EditToolLogViewer {
- fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl Item for EditToolLogViewer {
- type Event = ();
-
- fn to_item_events(_: &Self::Event, _: impl FnMut(ItemEvent)) {}
-
- fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
- Some("Edit Tool Log".into())
- }
-
- fn telemetry_event_text(&self) -> Option<&'static str> {
- None
- }
-
- fn clone_on_split(
- &self,
- _workspace_id: Option<WorkspaceId>,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Option<Entity<Self>>
- where
- Self: Sized,
- {
- Some(cx.new(Self::new))
- }
-}
-
-impl Render for EditToolLogViewer {
- fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- if self.list_state.item_count() == 0 {
- return v_flex()
- .justify_center()
- .size_full()
- .gap_1()
- .bg(cx.theme().colors().editor_background)
- .text_center()
- .text_lg()
- .child("No requests yet")
- .child(
- div()
- .text_ui(cx)
- .child("Go ask the assistant to perform some edits"),
- );
- }
-
- v_flex()
- .p_4()
- .bg(cx.theme().colors().editor_background)
- .size_full()
- .child(list(self.list_state.clone()).flex_grow())
- }
-}