agent: Remove edit_files
tool (#28041)
Release Notes: - agent: Remove `edit_files` tool in favor of `find_replace`
This commit is contained in:
parent
1bc5618f61
commit
cc9cc12f7b
8 changed files with 0 additions and 2103 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -738,7 +738,6 @@ dependencies = [
|
||||||
"assistant_tool",
|
"assistant_tool",
|
||||||
"chrono",
|
"chrono",
|
||||||
"collections",
|
"collections",
|
||||||
"feature_flags",
|
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"gpui",
|
"gpui",
|
||||||
"html_to_markdown",
|
"html_to_markdown",
|
||||||
|
@ -746,18 +745,14 @@ dependencies = [
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"language",
|
"language",
|
||||||
"language_model",
|
"language_model",
|
||||||
"log",
|
|
||||||
"lsp",
|
"lsp",
|
||||||
"open",
|
"open",
|
||||||
"project",
|
"project",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
"release_channel",
|
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
|
||||||
"theme",
|
|
||||||
"ui",
|
"ui",
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
|
|
|
@ -16,7 +16,6 @@ anyhow.workspace = true
|
||||||
assistant_tool.workspace = true
|
assistant_tool.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
feature_flags.workspace = true
|
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
html_to_markdown.workspace = true
|
html_to_markdown.workspace = true
|
||||||
|
@ -24,19 +23,14 @@ http_client.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
log.workspace = true
|
|
||||||
lsp.workspace = true
|
lsp.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
release_channel.workspace = true
|
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
|
||||||
theme.workspace = true
|
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace.workspace = true
|
|
||||||
worktree.workspace = true
|
worktree.workspace = true
|
||||||
open = { workspace = true }
|
open = { workspace = true }
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
|
@ -7,7 +7,6 @@ mod create_directory_tool;
|
||||||
mod create_file_tool;
|
mod create_file_tool;
|
||||||
mod delete_path_tool;
|
mod delete_path_tool;
|
||||||
mod diagnostics_tool;
|
mod diagnostics_tool;
|
||||||
mod edit_files_tool;
|
|
||||||
mod fetch_tool;
|
mod fetch_tool;
|
||||||
mod find_replace_file_tool;
|
mod find_replace_file_tool;
|
||||||
mod list_directory_tool;
|
mod list_directory_tool;
|
||||||
|
@ -37,7 +36,6 @@ use crate::create_directory_tool::CreateDirectoryTool;
|
||||||
use crate::create_file_tool::CreateFileTool;
|
use crate::create_file_tool::CreateFileTool;
|
||||||
use crate::delete_path_tool::DeletePathTool;
|
use crate::delete_path_tool::DeletePathTool;
|
||||||
use crate::diagnostics_tool::DiagnosticsTool;
|
use crate::diagnostics_tool::DiagnosticsTool;
|
||||||
use crate::edit_files_tool::EditFilesTool;
|
|
||||||
use crate::fetch_tool::FetchTool;
|
use crate::fetch_tool::FetchTool;
|
||||||
use crate::find_replace_file_tool::FindReplaceFileTool;
|
use crate::find_replace_file_tool::FindReplaceFileTool;
|
||||||
use crate::list_directory_tool::ListDirectoryTool;
|
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) {
|
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||||
assistant_tool::init(cx);
|
assistant_tool::init(cx);
|
||||||
crate::edit_files_tool::log::init(cx);
|
|
||||||
|
|
||||||
let registry = ToolRegistry::global(cx);
|
let registry = ToolRegistry::global(cx);
|
||||||
registry.register_tool(BashTool);
|
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(SymbolInfoTool);
|
||||||
registry.register_tool(MovePathTool);
|
registry.register_tool(MovePathTool);
|
||||||
registry.register_tool(DiagnosticsTool);
|
registry.register_tool(DiagnosticsTool);
|
||||||
registry.register_tool(EditFilesTool);
|
|
||||||
registry.register_tool(ListDirectoryTool);
|
registry.register_tool(ListDirectoryTool);
|
||||||
registry.register_tool(NowTool);
|
registry.register_tool(NowTool);
|
||||||
registry.register_tool(OpenTool);
|
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())
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue