Restructure workflow step resolution and fix inserting newlines (#15720)

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2024-08-05 09:18:06 +02:00 committed by GitHub
parent 49e736d8ef
commit 0ec29d6866
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1316 additions and 815 deletions

View file

@ -9,10 +9,11 @@ use crate::{
},
terminal_inline_assistant::TerminalInlineAssistant,
Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep, EditStepState,
EditStepSuggestions, InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor,
MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
DebugEditSteps, DeployHistory, DeployPromptLibrary, EditSuggestionGroup, InlineAssist,
InlineAssistId, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector,
PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, WorkflowStep,
WorkflowStepEditSuggestions,
};
use crate::{ContextStoreEvent, ShowConfiguration};
use anyhow::{anyhow, Result};
@ -39,7 +40,8 @@ use gpui::{
};
use indexed_docs::IndexedDocsStore;
use language::{
language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset,
language_settings::SoftWrap, Buffer, Capability, LanguageRegistry, LspAdapterDelegate, Point,
ToOffset,
};
use language_model::{
provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
@ -1284,7 +1286,6 @@ struct ActiveEditStep {
start: language::Anchor,
assist_ids: Vec<InlineAssistId>,
editor: Option<WeakView<Editor>>,
_open_editor: Task<Result<()>>,
}
pub struct ContextEditor {
@ -1452,23 +1453,21 @@ impl ContextEditor {
.read(cx)
.buffer()
.read(cx)
.text_for_range(step.source_range.clone())
.text_for_range(step.tagged_range.clone())
.collect::<String>()
));
match &step.state {
Some(EditStepState::Resolved(resolution)) => {
match &step.edit_suggestions {
WorkflowStepEditSuggestions::Resolved {
title,
edit_suggestions,
} => {
output.push_str("Resolution:\n");
output.push_str(&format!(" {:?}\n", resolution.step_title));
for op in &resolution.operations {
output.push_str(&format!(" {:?}\n", op));
}
output.push_str(&format!(" {:?}\n", title));
output.push_str(&format!(" {:?}\n", edit_suggestions));
}
Some(EditStepState::Pending(_)) => {
WorkflowStepEditSuggestions::Pending(_) => {
output.push_str("Resolution: Pending\n");
}
None => {
output.push_str("Resolution: None\n");
}
}
output.push('\n');
}
@ -1875,222 +1874,165 @@ impl ContextEditor {
}
EditorEvent::SelectionsChanged { .. } => {
self.scroll_position = self.cursor_scroll_position(cx);
if self
.edit_step_for_cursor(cx)
.map(|step| step.source_range.start)
!= self.active_edit_step.as_ref().map(|step| step.start)
{
if let Some(old_active_edit_step) = self.active_edit_step.take() {
if let Some(editor) = old_active_edit_step
.editor
.and_then(|editor| editor.upgrade())
{
self.workspace
.update(cx, |workspace, cx| {
if let Some(pane) = workspace.pane_for(&editor) {
pane.update(cx, |pane, cx| {
let item_id = editor.entity_id();
if pane.is_active_preview_item(item_id) {
pane.close_item_by_id(
item_id,
SaveIntent::Skip,
cx,
)
.detach_and_log_err(cx);
}
});
}
})
.ok();
}
}
if let Some(new_active_step) = self.edit_step_for_cursor(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 {
start,
assist_ids: Vec::new(),
editor: None,
_open_editor: open_editor,
});
}
}
self.update_active_workflow_step(cx);
}
_ => {}
}
cx.emit(event.clone());
}
fn open_editor_for_edit_suggestions(
&mut self,
edit_step_suggestions: Task<EditStepSuggestions>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let workspace = self.workspace.clone();
let project = self.project.clone();
let assistant_panel = self.assistant_panel.clone();
cx.spawn(|this, mut cx| async move {
let edit_step_suggestions = edit_step_suggestions.await;
fn update_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) {
if self
.workflow_step_for_cursor(cx)
.map(|step| step.tagged_range.start)
!= self.active_edit_step.as_ref().map(|step| step.start)
{
if let Some(old_active_edit_step) = self.active_edit_step.take() {
if let Some(editor) = old_active_edit_step
.editor
.and_then(|editor| editor.upgrade())
{
self.workspace
.update(cx, |workspace, cx| {
if let Some(pane) = workspace.pane_for(&editor) {
pane.update(cx, |pane, cx| {
let item_id = editor.entity_id();
if pane.is_active_preview_item(item_id) {
pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
.detach_and_log_err(cx);
}
});
}
})
.ok();
}
}
let mut assist_ids = Vec::new();
let editor = if edit_step_suggestions.suggestions.is_empty() {
return Ok(());
} else if edit_step_suggestions.suggestions.len() == 1
&& edit_step_suggestions
.suggestions
.values()
.next()
.unwrap()
.len()
== 1
{
// If there's only one buffer and one suggestion group, open it directly
let (buffer, suggestion_groups) = edit_step_suggestions
.suggestions
.into_iter()
.next()
.unwrap();
let suggestion_group = suggestion_groups.into_iter().next().unwrap();
let editor = workspace.update(&mut cx, |workspace, cx| {
if let Some(new_active_step) = self.workflow_step_for_cursor(cx) {
let start = new_active_step.tagged_range.start;
let mut editor = None;
let mut assist_ids = Vec::new();
if let WorkflowStepEditSuggestions::Resolved {
title,
edit_suggestions,
} = &new_active_step.edit_suggestions
{
if let Some((opened_editor, inline_assist_ids)) =
self.suggest_edits(title.clone(), edit_suggestions.clone(), cx)
{
editor = Some(opened_editor.downgrade());
assist_ids = inline_assist_ids;
}
}
self.active_edit_step = Some(ActiveEditStep {
start,
assist_ids,
editor,
});
}
}
}
fn suggest_edits(
&mut self,
title: String,
edit_suggestions: HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>,
cx: &mut ViewContext<Self>,
) -> Option<(View<Editor>, Vec<InlineAssistId>)> {
let assistant_panel = self.assistant_panel.upgrade()?;
if edit_suggestions.is_empty() {
return None;
}
let editor;
let mut suggestion_groups = Vec::new();
if edit_suggestions.len() == 1 && edit_suggestions.values().next().unwrap().len() == 1 {
// If there's only one buffer and one suggestion group, open it directly
let (buffer, groups) = edit_suggestions.into_iter().next().unwrap();
let group = groups.into_iter().next().unwrap();
editor = self
.workspace
.update(cx, |workspace, cx| {
let active_pane = workspace.active_pane().clone();
workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
})?;
})
.log_err()?;
cx.update(|cx| {
for suggestion in suggestion_group.suggestions {
let description = suggestion.description.unwrap_or_else(|| "Delete".into());
let (&excerpt_id, _, _) = editor
.read(cx)
.buffer()
.read(cx)
.read(cx)
.as_singleton()
.unwrap();
let range = {
let multibuffer = editor.read(cx).buffer().read(cx).read(cx);
let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
multibuffer
.anchor_in_excerpt(excerpt_id, suggestion.range.start)
.unwrap()
..multibuffer
.anchor_in_excerpt(excerpt_id, suggestion.range.end)
.unwrap()
};
InlineAssistant::update_global(cx, |assistant, cx| {
let suggestion_id = assistant.suggest_assist(
&editor,
range,
description,
suggestion.initial_insertion,
Some(workspace.clone()),
assistant_panel.upgrade().as_ref(),
cx,
);
assist_ids.push(suggestion_id);
});
}
// Scroll the editor to the suggested assist
editor.update(cx, |editor, cx| {
let multibuffer = editor.buffer().read(cx).snapshot(cx);
let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
let anchor = if suggestion_group.context_range.start.to_offset(buffer) == 0
{
Anchor::min()
} else {
multibuffer
.anchor_in_excerpt(excerpt_id, suggestion_group.context_range.start)
.unwrap()
};
editor.set_scroll_anchor(
ScrollAnchor {
offset: gpui::Point::default(),
anchor,
},
cx,
);
});
})?;
editor
} else {
// If there are multiple buffers or suggestion groups, create a multibuffer
let mut inline_assist_suggestions = Vec::new();
let multibuffer = cx.new_model(|cx| {
let replica_id = project.read(cx).replica_id();
let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite)
.with_title(edit_step_suggestions.title);
for (buffer, suggestion_groups) in edit_step_suggestions.suggestions {
let excerpt_ids = multibuffer.push_excerpts(
buffer,
suggestion_groups
.iter()
.map(|suggestion_group| ExcerptRange {
context: suggestion_group.context_range.clone(),
primary: None,
}),
cx,
);
for (excerpt_id, suggestion_group) in
excerpt_ids.into_iter().zip(suggestion_groups)
{
for suggestion in suggestion_group.suggestions {
let description =
suggestion.description.unwrap_or_else(|| "Delete".into());
let range = {
let multibuffer = multibuffer.read(cx);
multibuffer
.anchor_in_excerpt(excerpt_id, suggestion.range.start)
.unwrap()
..multibuffer
.anchor_in_excerpt(excerpt_id, suggestion.range.end)
.unwrap()
};
inline_assist_suggestions.push((
range,
description,
suggestion.initial_insertion,
));
}
}
}
// Scroll the editor to the suggested assist
editor.update(cx, |editor, cx| {
let multibuffer = editor.buffer().read(cx).snapshot(cx);
let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
let anchor = if group.context_range.start.to_offset(buffer) == 0 {
Anchor::min()
} else {
multibuffer
})?;
.anchor_in_excerpt(excerpt_id, group.context_range.start)
.unwrap()
};
let editor = cx
.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), true, cx))?;
cx.update(|cx| {
InlineAssistant::update_global(cx, |assistant, cx| {
for (range, description, initial_insertion) in inline_assist_suggestions {
assist_ids.push(assistant.suggest_assist(
&editor,
range,
description,
initial_insertion,
Some(workspace.clone()),
assistant_panel.upgrade().as_ref(),
cx,
));
}
})
})?;
workspace.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
})?;
editor.set_scroll_anchor(
ScrollAnchor {
offset: gpui::Point::default(),
anchor,
},
cx,
);
});
editor
};
this.update(&mut cx, |this, _cx| {
if let Some(step) = this.active_edit_step.as_mut() {
step.assist_ids = assist_ids;
step.editor = Some(editor.downgrade());
suggestion_groups.push((excerpt_id, group));
} else {
// If there are multiple buffers or suggestion groups, create a multibuffer
let multibuffer = cx.new_model(|cx| {
let replica_id = self.project.read(cx).replica_id();
let mut multibuffer =
MultiBuffer::new(replica_id, Capability::ReadWrite).with_title(title);
for (buffer, groups) in edit_suggestions {
let excerpt_ids = multibuffer.push_excerpts(
buffer,
groups.iter().map(|suggestion_group| ExcerptRange {
context: suggestion_group.context_range.clone(),
primary: None,
}),
cx,
);
suggestion_groups.extend(excerpt_ids.into_iter().zip(groups));
}
})
})
multibuffer
});
editor = cx.new_view(|cx| {
Editor::for_multibuffer(multibuffer, Some(self.project.clone()), true, cx)
});
self.workspace
.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
})
.log_err()?;
}
let mut assist_ids = Vec::new();
for (excerpt_id, suggestion_group) in suggestion_groups {
for suggestion in suggestion_group.suggestions {
assist_ids.extend(suggestion.show(
&editor,
excerpt_id,
&self.workspace,
&assistant_panel,
cx,
));
}
}
Some((editor, assist_ids))
}
fn handle_editor_search_event(
@ -2374,11 +2316,10 @@ impl ContextEditor {
fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx).clone();
let button_text = match self.edit_step_for_cursor(cx) {
Some(edit_step) => match &edit_step.state {
Some(EditStepState::Pending(_)) => "Computing Changes...",
Some(EditStepState::Resolved(_)) => "Apply Changes",
None => "Send",
let button_text = match self.workflow_step_for_cursor(cx) {
Some(edit_step) => match &edit_step.edit_suggestions {
WorkflowStepEditSuggestions::Pending(_) => "Computing Changes...",
WorkflowStepEditSuggestions::Resolved { .. } => "Apply Changes",
},
None => "Send",
};
@ -2421,7 +2362,7 @@ impl ContextEditor {
})
}
fn edit_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a EditStep> {
fn workflow_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a WorkflowStep> {
let newest_cursor = self
.editor
.read(cx)
@ -2435,7 +2376,7 @@ impl ContextEditor {
let edit_steps = context.edit_steps();
edit_steps
.binary_search_by(|step| {
let step_range = step.source_range.clone();
let step_range = step.tagged_range.clone();
if newest_cursor.cmp(&step_range.start, buffer).is_lt() {
Ordering::Greater
} else if newest_cursor.cmp(&step_range.end, buffer).is_gt() {

File diff suppressed because it is too large Load diff

View file

@ -330,7 +330,12 @@ impl ContextStore {
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
let context = cx.new_model(|cx| {
Context::local(self.languages.clone(), Some(self.telemetry.clone()), cx)
Context::local(
self.languages.clone(),
Some(self.project.clone()),
Some(self.telemetry.clone()),
cx,
)
});
self.register_context(&context, cx);
context
@ -351,6 +356,7 @@ impl ContextStore {
let replica_id = project.replica_id();
let capability = project.capability();
let language_registry = self.languages.clone();
let project = self.project.clone();
let telemetry = self.telemetry.clone();
let request = self.client.request(proto::CreateContext { project_id });
cx.spawn(|this, mut cx| async move {
@ -363,6 +369,7 @@ impl ContextStore {
replica_id,
capability,
language_registry,
Some(project),
Some(telemetry),
cx,
)
@ -401,6 +408,7 @@ impl ContextStore {
let fs = self.fs.clone();
let languages = self.languages.clone();
let project = self.project.clone();
let telemetry = self.telemetry.clone();
let load = cx.background_executor().spawn({
let path = path.clone();
@ -413,7 +421,14 @@ impl ContextStore {
cx.spawn(|this, mut cx| async move {
let saved_context = load.await?;
let context = cx.new_model(|cx| {
Context::deserialize(saved_context, path.clone(), languages, Some(telemetry), cx)
Context::deserialize(
saved_context,
path.clone(),
languages,
Some(project),
Some(telemetry),
cx,
)
})?;
this.update(&mut cx, |this, cx| {
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
@ -472,6 +487,7 @@ impl ContextStore {
let replica_id = project.replica_id();
let capability = project.capability();
let language_registry = self.languages.clone();
let project = self.project.clone();
let telemetry = self.telemetry.clone();
let request = self.client.request(proto::OpenContext {
project_id,
@ -486,6 +502,7 @@ impl ContextStore {
replica_id,
capability,
language_registry,
Some(project),
Some(telemetry),
cx,
)

View file

@ -237,7 +237,7 @@ impl InlineAssistant {
editor: &View<Editor>,
mut range: Range<Anchor>,
initial_prompt: String,
initial_insertion: Option<InitialInsertion>,
initial_transaction_id: Option<TransactionId>,
workspace: Option<WeakView<Workspace>>,
assistant_panel: Option<&View<AssistantPanel>>,
cx: &mut WindowContext,
@ -251,28 +251,15 @@ impl InlineAssistant {
let buffer = editor.read(cx).buffer().clone();
{
let snapshot = buffer.read(cx).read(cx);
let mut point_range = range.to_point(&snapshot);
if point_range.is_empty() {
point_range.start.column = 0;
point_range.end.column = 0;
} else {
point_range.start.column = 0;
if point_range.end.row > point_range.start.row && point_range.end.column == 0 {
point_range.end.row -= 1;
}
point_range.end.column = snapshot.line_len(MultiBufferRow(point_range.end.row));
}
range.start = snapshot.anchor_before(point_range.start);
range.end = snapshot.anchor_after(point_range.end);
range.start = range.start.bias_left(&snapshot);
range.end = range.end.bias_right(&snapshot);
}
let codegen = cx.new_model(|cx| {
Codegen::new(
editor.read(cx).buffer().clone(),
range.clone(),
initial_insertion,
initial_transaction_id,
self.telemetry.clone(),
cx,
)
@ -873,13 +860,20 @@ impl InlineAssistant {
for assist_id in assist_ids {
if let Some(assist) = self.assists.get(assist_id) {
let codegen = assist.codegen.read(cx);
let buffer = codegen.buffer.read(cx).read(cx);
foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
gutter_pending_ranges
.push(codegen.edit_position.unwrap_or(assist.range.start)..assist.range.end);
let pending_range =
codegen.edit_position.unwrap_or(assist.range.start)..assist.range.end;
if pending_range.end.to_offset(&buffer) > pending_range.start.to_offset(&buffer) {
gutter_pending_ranges.push(pending_range);
}
if let Some(edit_position) = codegen.edit_position {
gutter_transformed_ranges.push(assist.range.start..edit_position);
let edited_range = assist.range.start..edit_position;
if edited_range.end.to_offset(&buffer) > edited_range.start.to_offset(&buffer) {
gutter_transformed_ranges.push(edited_range);
}
}
if assist.decorations.is_some() {
@ -1997,13 +1991,13 @@ pub struct Codegen {
snapshot: MultiBufferSnapshot,
edit_position: Option<Anchor>,
last_equal_ranges: Vec<Range<Anchor>>,
transaction_id: Option<TransactionId>,
initial_transaction_id: Option<TransactionId>,
transformation_transaction_id: Option<TransactionId>,
status: CodegenStatus,
generation: Task<()>,
diff: Diff,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
initial_insertion: Option<InitialInsertion>,
}
enum CodegenStatus {
@ -2027,7 +2021,7 @@ impl Codegen {
pub fn new(
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
initial_insertion: Option<InitialInsertion>,
initial_transaction_id: Option<TransactionId>,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
) -> Self {
@ -2059,13 +2053,13 @@ impl Codegen {
edit_position: None,
snapshot,
last_equal_ranges: Default::default(),
transaction_id: None,
transformation_transaction_id: None,
status: CodegenStatus::Idle,
generation: Task::ready(()),
diff: Diff::default(),
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
initial_insertion,
initial_transaction_id,
}
}
@ -2076,8 +2070,8 @@ impl Codegen {
cx: &mut ModelContext<Self>,
) {
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
if self.transaction_id == Some(*transaction_id) {
self.transaction_id = None;
if self.transformation_transaction_id == Some(*transaction_id) {
self.transformation_transaction_id = None;
self.generation = Task::ready(());
cx.emit(CodegenEvent::Undone);
}
@ -2105,7 +2099,7 @@ impl Codegen {
pub fn start(
&mut self,
mut edit_range: Range<Anchor>,
edit_range: Range<Anchor>,
user_prompt: String,
assistant_panel_context: Option<LanguageModelRequest>,
cx: &mut ModelContext<Self>,
@ -2114,34 +2108,13 @@ impl Codegen {
.active_model()
.context("no active model")?;
self.undo(cx);
// Handle initial insertion
self.transaction_id = if let Some(initial_insertion) = self.initial_insertion {
if let Some(transformation_transaction_id) = self.transformation_transaction_id.take() {
self.buffer.update(cx, |buffer, cx| {
buffer.start_transaction(cx);
let offset = edit_range.start.to_offset(&self.snapshot);
let edit_position;
match initial_insertion {
InitialInsertion::NewlineBefore => {
buffer.edit([(offset..offset, "\n\n")], None, cx);
self.snapshot = buffer.snapshot(cx);
edit_position = self.snapshot.anchor_after(offset + 1);
}
InitialInsertion::NewlineAfter => {
buffer.edit([(offset..offset, "\n")], None, cx);
self.snapshot = buffer.snapshot(cx);
edit_position = self.snapshot.anchor_after(offset);
}
}
self.edit_position = Some(edit_position);
edit_range = edit_position.bias_left(&self.snapshot)..edit_position;
buffer.end_transaction(cx)
})
} else {
self.edit_position = Some(edit_range.start.bias_right(&self.snapshot));
None
};
buffer.undo_transaction(transformation_transaction_id, cx)
});
}
self.edit_position = Some(edit_range.start.bias_right(&self.snapshot));
let telemetry_id = model.telemetry_id();
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> = if user_prompt
@ -2406,7 +2379,8 @@ impl Codegen {
});
if let Some(transaction) = transaction {
if let Some(first_transaction) = this.transaction_id {
if let Some(first_transaction) = this.transformation_transaction_id
{
// Group all assistant edits into the first transaction.
this.buffer.update(cx, |buffer, cx| {
buffer.merge_transactions(
@ -2416,7 +2390,7 @@ impl Codegen {
)
});
} else {
this.transaction_id = Some(transaction);
this.transformation_transaction_id = Some(transaction);
this.buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction(cx)
});
@ -2459,10 +2433,15 @@ impl Codegen {
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
if let Some(transaction_id) = self.transaction_id.take() {
self.buffer
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
}
self.buffer.update(cx, |buffer, cx| {
if let Some(transaction_id) = self.transformation_transaction_id.take() {
buffer.undo_transaction(transaction_id, cx);
}
if let Some(transaction_id) = self.initial_transaction_id.take() {
buffer.undo_transaction(transaction_id, cx);
}
});
}
fn update_diff(&mut self, edit_range: Range<Anchor>, cx: &mut ModelContext<Self>) {