Give edit steps multibuffer a title (#15625)

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2024-08-01 17:48:56 +02:00 committed by GitHub
parent ed7952f5ef
commit 0b175ac66e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 107 additions and 65 deletions

View file

@ -37,6 +37,7 @@ What are the operations for the step: <step>Add a new method 'calculate_area' to
A (wrong): A (wrong):
{ {
"title": "Add Rectangle methods",
"operations": [ "operations": [
{ {
"kind": "AppendChild", "kind": "AppendChild",
@ -57,6 +58,7 @@ This demonstrates what NOT to do. NEVER append multiple children at the same loc
A (corrected): A (corrected):
{ {
"title": "Add Rectangle methods",
"operations": [ "operations": [
{ {
"kind": "AppendChild", "kind": "AppendChild",
@ -72,6 +74,7 @@ What are the operations for the step: <step>Implement the 'Display' trait for th
A: A:
{ {
"title": "Implement Display for Rectangle",
"operations": [ "operations": [
{ {
"kind": "InsertSiblingAfter", "kind": "InsertSiblingAfter",
@ -110,6 +113,7 @@ What are the operations for the step: <step>Update the 'print_info' method to us
A: A:
{ {
"title": "Use formatted output",
"operations": [ "operations": [
{ {
"kind": "Update", "kind": "Update",
@ -125,13 +129,14 @@ What are the operations for the step: <step>Remove the 'email' field from the Us
A: A:
{ {
"title": "Remove email field",
"operations": [ "operations": [
{ {
"kind": "Delete", "kind": "Delete",
"path": "src/user.rs", "path": "src/user.rs",
"symbol": "struct User email" "symbol": "struct User email"
} }
] ]
} }
Example 3: Example 3:
@ -162,6 +167,7 @@ What are the operations for the step: <step>Add a 'use std::fmt;' statement at t
A: A:
{ {
"title": "Add use std::fmt statement",
"operations": [ "operations": [
{ {
"kind": "PrependChild", "kind": "PrependChild",
@ -176,6 +182,7 @@ What are the operations for the step: <step>Add a new method 'start_engine' in t
A: A:
{ {
"title": "Add start_engine method",
"operations": [ "operations": [
{ {
"kind": "InsertSiblingAfter", "kind": "InsertSiblingAfter",
@ -219,6 +226,7 @@ What are the operations for the step: <step>Make salary an f32</step>
A (wrong): A (wrong):
{ {
"title": "Change salary to f32",
"operations": [ "operations": [
{ {
"kind": "Update", "kind": "Update",
@ -239,6 +247,7 @@ This example demonstrates what not to do. `struct Employee salary` is a child of
A (corrected): A (corrected):
{ {
"title": "Change salary to f32",
"operations": [ "operations": [
{ {
"kind": "Update", "kind": "Update",
@ -254,6 +263,7 @@ What are the correct operations for the step: <step>Remove the 'department' fiel
A: A:
{ {
"title": "Remove department",
"operations": [ "operations": [
{ {
"kind": "Delete", "kind": "Delete",
@ -300,6 +310,7 @@ impl Game {
A: A:
{ {
"title": "Add level field to Player",
"operations": [ "operations": [
{ {
"kind": "InsertSiblingAfter", "kind": "InsertSiblingAfter",
@ -337,6 +348,7 @@ impl Config {
A: A:
{ {
"title": "Add load_from_file method",
"operations": [ "operations": [
{ {
"kind": "PrependChild", "kind": "PrependChild",
@ -376,6 +388,7 @@ impl Database {
A: A:
{ {
"title": "Add error handling to query",
"operations": [ "operations": [
{ {
"kind": "PrependChild", "kind": "PrependChild",

View file

@ -9,8 +9,8 @@ use crate::{
}, },
terminal_inline_assistant::TerminalInlineAssistant, terminal_inline_assistant::TerminalInlineAssistant,
Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole, Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep, EditStepOperations, DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep, EditStepState,
EditSuggestionGroup, InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor, EditStepSuggestions, InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor,
MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
}; };
@ -40,8 +40,7 @@ use gpui::{
}; };
use indexed_docs::IndexedDocsStore; use indexed_docs::IndexedDocsStore;
use language::{ use language::{
language_settings::SoftWrap, Buffer, Capability, LanguageRegistry, LspAdapterDelegate, Point, language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset,
ToOffset,
}; };
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role}; use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role};
use markdown::{Markdown, MarkdownStyle}; use markdown::{Markdown, MarkdownStyle};
@ -1454,18 +1453,19 @@ impl ContextEditor {
.text_for_range(step.source_range.clone()) .text_for_range(step.source_range.clone())
.collect::<String>() .collect::<String>()
)); ));
match &step.operations { match &step.state {
Some(EditStepOperations::Ready(operations)) => { Some(EditStepState::Resolved(resolution)) => {
output.push_str("Parsed Operations:\n"); output.push_str("Resolution:\n");
for op in operations { output.push_str(&format!(" {:?}\n", resolution.step_title));
for op in &resolution.operations {
output.push_str(&format!(" {:?}\n", op)); output.push_str(&format!(" {:?}\n", op));
} }
} }
Some(EditStepOperations::Pending(_)) => { Some(EditStepState::Pending(_)) => {
output.push_str("Operations: Pending\n"); output.push_str("Resolution: Pending\n");
} }
None => { None => {
output.push_str("Operations: None\n"); output.push_str("Resolution: None\n");
} }
} }
output.push('\n'); output.push('\n');
@ -1906,12 +1906,18 @@ impl ContextEditor {
} }
if let Some(new_active_step) = self.edit_step_for_cursor(cx) { if let Some(new_active_step) = self.edit_step_for_cursor(cx) {
let suggestions = new_active_step.edit_suggestions(&self.project, cx); let start = new_active_step.source_range.start;
let open_editor = new_active_step
.edit_suggestions(&self.project, cx)
.map(|suggestions| {
self.open_editor_for_edit_suggestions(suggestions, cx)
})
.unwrap_or_else(|| Task::ready(Ok(())));
self.active_edit_step = Some(ActiveEditStep { self.active_edit_step = Some(ActiveEditStep {
start: new_active_step.source_range.start, start,
assist_ids: Vec::new(), assist_ids: Vec::new(),
editor: None, editor: None,
_open_editor: self.open_editor_for_edit_suggestions(suggestions, cx), _open_editor: open_editor,
}); });
} }
} }
@ -1923,23 +1929,33 @@ impl ContextEditor {
fn open_editor_for_edit_suggestions( fn open_editor_for_edit_suggestions(
&mut self, &mut self,
edit_suggestions: Task<HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>>, edit_step_suggestions: Task<EditStepSuggestions>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
let project = self.project.clone(); let project = self.project.clone();
let assistant_panel = self.assistant_panel.clone(); let assistant_panel = self.assistant_panel.clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let edit_suggestions = edit_suggestions.await; let edit_step_suggestions = edit_step_suggestions.await;
let mut assist_ids = Vec::new(); let mut assist_ids = Vec::new();
let editor = if edit_suggestions.is_empty() { let editor = if edit_step_suggestions.suggestions.is_empty() {
return Ok(()); return Ok(());
} else if edit_suggestions.len() == 1 } else if edit_step_suggestions.suggestions.len() == 1
&& edit_suggestions.values().next().unwrap().len() == 1 && edit_step_suggestions
.suggestions
.values()
.next()
.unwrap()
.len()
== 1
{ {
// If there's only one buffer and one suggestion group, open it directly // If there's only one buffer and one suggestion group, open it directly
let (buffer, suggestion_groups) = edit_suggestions.into_iter().next().unwrap(); let (buffer, suggestion_groups) = edit_step_suggestions
.suggestions
.into_iter()
.next()
.unwrap();
let suggestion_group = suggestion_groups.into_iter().next().unwrap(); let suggestion_group = suggestion_groups.into_iter().next().unwrap();
let editor = workspace.update(&mut cx, |workspace, cx| { let editor = workspace.update(&mut cx, |workspace, cx| {
let active_pane = workspace.active_pane().clone(); let active_pane = workspace.active_pane().clone();
@ -2004,8 +2020,9 @@ impl ContextEditor {
let mut inline_assist_suggestions = Vec::new(); let mut inline_assist_suggestions = Vec::new();
let multibuffer = cx.new_model(|cx| { let multibuffer = cx.new_model(|cx| {
let replica_id = project.read(cx).replica_id(); let replica_id = project.read(cx).replica_id();
let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite); let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite)
for (buffer, suggestion_groups) in edit_suggestions { .with_title(edit_step_suggestions.title);
for (buffer, suggestion_groups) in edit_step_suggestions.suggestions {
let excerpt_ids = multibuffer.push_excerpts( let excerpt_ids = multibuffer.push_excerpts(
buffer, buffer,
suggestion_groups suggestion_groups
@ -2358,9 +2375,9 @@ impl ContextEditor {
fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx).clone(); let focus_handle = self.focus_handle(cx).clone();
let button_text = match self.edit_step_for_cursor(cx) { let button_text = match self.edit_step_for_cursor(cx) {
Some(edit_step) => match &edit_step.operations { Some(edit_step) => match &edit_step.state {
Some(EditStepOperations::Pending(_)) => "Computing Changes...", Some(EditStepState::Pending(_)) => "Computing Changes...",
Some(EditStepOperations::Ready(_)) => "Apply Changes", Some(EditStepState::Resolved(_)) => "Apply Changes",
None => "Send", None => "Send",
}, },
None => "Send", None => "Send",

View file

@ -341,7 +341,7 @@ pub struct SlashCommandId(clock::Lamport);
#[derive(Debug)] #[derive(Debug)]
pub struct EditStep { pub struct EditStep {
pub source_range: Range<language::Anchor>, pub source_range: Range<language::Anchor>,
pub operations: Option<EditStepOperations>, pub state: Option<EditStepState>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -358,22 +358,29 @@ pub struct EditSuggestion {
pub initial_insertion: Option<InitialInsertion>, pub initial_insertion: Option<InitialInsertion>,
} }
pub struct EditStepSuggestions {
pub title: String,
pub suggestions: HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>,
}
impl EditStep { impl EditStep {
pub fn edit_suggestions( pub fn edit_suggestions(
&self, &self,
project: &Model<Project>, project: &Model<Project>,
cx: &AppContext, cx: &AppContext,
) -> Task<HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>> { ) -> Option<Task<EditStepSuggestions>> {
let Some(EditStepOperations::Ready(operations)) = &self.operations else { let Some(EditStepState::Resolved(resolution)) = &self.state else {
return Task::ready(HashMap::default()); return None;
}; };
let suggestion_tasks: Vec<_> = operations let title = resolution.step_title.clone();
let suggestion_tasks: Vec<_> = resolution
.operations
.iter() .iter()
.map(|operation| operation.edit_suggestion(project.clone(), cx)) .map(|operation| operation.edit_suggestion(project.clone(), cx))
.collect(); .collect();
cx.spawn(|mut cx| async move { Some(cx.spawn(|mut cx| async move {
let suggestions = future::join_all(suggestion_tasks) let suggestions = future::join_all(suggestion_tasks)
.await .await
.into_iter() .into_iter()
@ -468,21 +475,24 @@ impl EditStep {
suggestion_groups_by_buffer.insert(buffer, suggestion_groups); suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
} }
suggestion_groups_by_buffer EditStepSuggestions {
}) title,
suggestions: suggestion_groups_by_buffer,
}
}))
} }
} }
pub enum EditStepOperations { pub enum EditStepState {
Pending(Task<Option<()>>), Pending(Task<Option<()>>),
Ready(Vec<EditOperation>), Resolved(EditStepResolution),
} }
impl Debug for EditStepOperations { impl Debug for EditStepState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
EditStepOperations::Pending(_) => write!(f, "EditStepOperations::Pending"), EditStepState::Pending(_) => write!(f, "EditStepOperations::Pending"),
EditStepOperations::Ready(operations) => f EditStepState::Resolved(operations) => f
.debug_struct("EditStepOperations::Parsed") .debug_struct("EditStepOperations::Parsed")
.field("operations", operations) .field("operations", operations)
.finish(), .finish(),
@ -490,6 +500,25 @@ impl Debug for EditStepOperations {
} }
} }
#[derive(Debug, Deserialize, JsonSchema)]
pub struct EditStepResolution {
/// An extremely short title for the edit step represented by these operations.
pub step_title: String,
/// A sequence of operations to apply to the codebase.
/// When multiple operations are required for a step, be sure to include multiple operations in this list.
pub operations: Vec<EditOperation>,
}
impl LanguageModelTool for EditStepResolution {
fn name() -> String {
"edit".into()
}
fn description() -> String {
"suggest edits to one or more locations in the codebase".into()
}
}
/// A description of an operation to apply to one location in the codebase. /// A description of an operation to apply to one location in the codebase.
/// ///
/// This object represents a single edit operation that can be performed on a specific file /// This object represents a single edit operation that can be performed on a specific file
@ -1324,7 +1353,7 @@ impl Context {
ix, ix,
EditStep { EditStep {
source_range, source_range,
operations: None, state: None,
}, },
)); ));
} }
@ -1340,7 +1369,7 @@ impl Context {
// Insert new steps and generate their corresponding tasks // Insert new steps and generate their corresponding tasks
for (index, mut step) in new_edit_steps.into_iter().rev() { for (index, mut step) in new_edit_steps.into_iter().rev() {
let task = self.generate_edit_step_operations(&step, cx); let task = self.generate_edit_step_operations(&step, cx);
step.operations = Some(EditStepOperations::Pending(task)); step.state = Some(EditStepState::Pending(task));
self.edit_steps.insert(index, step); self.edit_steps.insert(index, step);
} }
@ -1353,23 +1382,6 @@ impl Context {
edit_step: &EditStep, edit_step: &EditStep,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Option<()>> { ) -> Task<Option<()>> {
#[derive(Debug, Deserialize, JsonSchema)]
struct EditTool {
/// A sequence of operations to apply to the codebase.
/// When multiple operations are required for a step, be sure to include multiple operations in this list.
operations: Vec<EditOperation>,
}
impl LanguageModelTool for EditTool {
fn name() -> String {
"edit".into()
}
fn description() -> String {
"suggest edits to one or more locations in the codebase".into()
}
}
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else { let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return Task::ready(Err(anyhow!("no active model")).log_err()); return Task::ready(Err(anyhow!("no active model")).log_err());
}; };
@ -1394,7 +1406,7 @@ impl Context {
content: prompt, content: prompt,
}); });
let tool_use = model.use_tool::<EditTool>(request, &cx).await?; let resolution = model.use_tool::<EditStepResolution>(request, &cx).await?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let step_index = this let step_index = this
@ -1405,7 +1417,7 @@ impl Context {
}) })
.map_err(|_| anyhow!("edit step not found"))?; .map_err(|_| anyhow!("edit step not found"))?;
if let Some(edit_step) = this.edit_steps.get_mut(step_index) { if let Some(edit_step) = this.edit_steps.get_mut(step_index) {
edit_step.operations = Some(EditStepOperations::Ready(tool_use.operations)); edit_step.state = Some(EditStepState::Resolved(resolution));
cx.emit(ContextEvent::EditStepsChanged); cx.emit(ContextEvent::EditStepsChanged);
} }
anyhow::Ok(()) anyhow::Ok(())