Start on adding support for editing via the assistant panel (#14795)

Note that this shouldn't have any visible user-facing behavior yet. The
feature is incomplete but we wanna merge early to avoid a long-running
branch.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2024-07-19 11:13:15 +02:00 committed by GitHub
parent 87457f9ae8
commit 4d177918c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 1999 additions and 968 deletions

View file

@ -1,19 +1,19 @@
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings},
humanize_token_count, parse_next_edit_suggestion,
humanize_token_count,
prompt_library::open_prompt_library,
search::*,
slash_command::{
default_command::DefaultSlashCommand,
docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
SlashCommandCompletionProvider, SlashCommandRegistry,
},
terminal_inline_assistant::TerminalInlineAssistant,
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, Context, ContextEvent, ContextId,
ContextStore, CycleMessageRole, DeployHistory, DeployPromptLibrary, EditSuggestion,
InlineAssist, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector,
PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
ResetKey, Role, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
Assist, CompletionProvider, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore,
CycleMessageRole, DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep,
EditStepOperations, EditSuggestionGroup, InlineAssist, InlineAssistId, InlineAssistant,
InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus,
QuoteSelection, RemoteContextMetadata, ResetKey, Role, SavedContextMetadata, Split,
ToggleFocus, ToggleModelSelector,
};
use anyhow::{anyhow, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
@ -25,29 +25,36 @@ use editor::{
display_map::{
BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, RenderBlock, ToDisplayPoint,
},
scroll::{Autoscroll, AutoscrollStrategy},
Anchor, Editor, EditorEvent, RowExt, ToOffset as _, ToPoint,
scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint,
};
use editor::{display_map::CreaseId, FoldPlaceholder};
use fs::Fs;
use gpui::{
div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext,
AsyncWindowContext, ClipboardItem, DismissEvent, Empty, EventEmitter, FocusHandle,
FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render,
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter,
FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use indexed_docs::IndexedDocsStore;
use language::{
language_settings::SoftWrap, AutoindentMode, Buffer, LanguageRegistry, LspAdapterDelegate,
OffsetRangeExt as _, Point, ToOffset,
language_settings::SoftWrap, Buffer, Capability, LanguageRegistry, LspAdapterDelegate, Point,
ToOffset,
};
use multi_buffer::MultiBufferRow;
use picker::{Picker, PickerDelegate};
use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction};
use project::{Project, ProjectLspAdapterDelegate};
use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::Settings;
use std::{cmp, fmt::Write, ops::Range, path::PathBuf, sync::Arc, time::Duration};
use std::{
cmp::{self, Ordering},
fmt::Write,
ops::Range,
path::PathBuf,
sync::Arc,
time::Duration,
};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use theme::ThemeSettings;
use ui::{
@ -60,7 +67,8 @@ use util::ResultExt;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
item::{self, BreadcrumbText, FollowableItem, Item, ItemHandle},
pane,
notifications::NotifyTaskExt,
pane::{self, SaveIntent},
searchable::{SearchEvent, SearchableItem},
Pane, Save, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
@ -591,6 +599,7 @@ impl AssistantPanel {
make_lsp_adapter_delegate(workspace.project(), cx).log_err()
});
let assistant_panel = cx.view().downgrade();
let editor = cx.new_view(|cx| {
let mut editor = ContextEditor::for_context(
context,
@ -598,6 +607,7 @@ impl AssistantPanel {
workspace.clone(),
self.project.clone(),
lsp_adapter_delegate,
assistant_panel,
cx,
);
editor.insert_default_prompt(cx);
@ -720,6 +730,7 @@ impl AssistantPanel {
cx.spawn(|this, mut cx| async move {
let context = context.await?;
let assistant_panel = this.clone();
this.update(&mut cx, |this, cx| {
let workspace = workspace
.upgrade()
@ -731,6 +742,7 @@ impl AssistantPanel {
workspace,
project,
lsp_adapter_delegate,
assistant_panel,
cx,
)
});
@ -774,6 +786,7 @@ impl AssistantPanel {
cx.spawn(|this, mut cx| async move {
let context = context.await?;
let assistant_panel = this.clone();
this.update(&mut cx, |this, cx| {
let workspace = workspace
.upgrade()
@ -785,6 +798,7 @@ impl AssistantPanel {
workspace,
this.project.clone(),
lsp_adapter_delegate,
assistant_panel,
cx,
)
});
@ -956,10 +970,18 @@ struct ScrollPosition {
cursor: Anchor,
}
struct ActiveEditStep {
start: language::Anchor,
assist_ids: Vec<InlineAssistId>,
editor: Option<WeakView<Editor>>,
_open_editor: Task<Result<()>>,
}
pub struct ContextEditor {
context: Model<Context>,
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
project: Model<Project>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
editor: View<Editor>,
blocks: HashSet<BlockId>,
@ -968,6 +990,8 @@ pub struct ContextEditor {
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
pending_slash_command_blocks: HashMap<Range<language::Anchor>, BlockId>,
_subscriptions: Vec<Subscription>,
active_edit_step: Option<ActiveEditStep>,
assistant_panel: WeakView<AssistantPanel>,
}
impl ContextEditor {
@ -979,6 +1003,7 @@ impl ContextEditor {
workspace: View<Workspace>,
project: Model<Project>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
assistant_panel: WeakView<AssistantPanel>,
cx: &mut ViewContext<Self>,
) -> Self {
let completion_provider = SlashCommandCompletionProvider::new(
@ -996,7 +1021,7 @@ impl ContextEditor {
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_completion_provider(Box::new(completion_provider));
editor.set_collaboration_hub(Box::new(project));
editor.set_collaboration_hub(Box::new(project.clone()));
editor
});
@ -1017,9 +1042,12 @@ impl ContextEditor {
remote_id: None,
fs,
workspace: workspace.downgrade(),
project,
pending_slash_command_creases: HashMap::default(),
pending_slash_command_blocks: HashMap::default(),
_subscriptions,
active_edit_step: None,
assistant_panel,
};
this.update_message_headers(cx);
this.insert_slash_command_output_sections(sections, cx);
@ -1052,31 +1080,37 @@ impl ContextEditor {
}
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
let cursors = self.cursors(cx);
if !self.apply_edit_step(cx) {
self.send_to_model(cx);
}
}
let user_messages = self.context.update(cx, |context, cx| {
let selected_messages = context
.messages_for_offsets(cursors, cx)
.into_iter()
.map(|message| message.id)
.collect();
context.assist(selected_messages, cx)
});
let new_selections = user_messages
.iter()
.map(|message| {
let cursor = message
fn apply_edit_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
if let Some(step) = self.active_edit_step.as_ref() {
InlineAssistant::update_global(cx, |assistant, cx| {
for assist_id in &step.assist_ids {
assistant.start_assist(*assist_id, cx);
}
!step.assist_ids.is_empty()
})
} else {
false
}
}
fn send_to_model(&mut self, cx: &mut ViewContext<Self>) {
if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
let new_selection = {
let cursor = user_message
.start
.to_offset(self.context.read(cx).buffer().read(cx));
cursor..cursor
})
.collect::<Vec<_>>();
if !new_selections.is_empty() {
};
self.editor.update(cx, |editor, cx| {
editor.change_selections(
Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
cx,
|selections| selections.select_ranges(new_selections),
|selections| selections.select_ranges([new_selection]),
);
});
// Avoid scrolling to the new cursor position so the assistant's output is stable.
@ -1093,6 +1127,53 @@ impl ContextEditor {
}
}
fn debug_edit_steps(&mut self, _: &DebugEditSteps, cx: &mut ViewContext<Self>) {
let mut output = String::new();
for (i, step) in self.context.read(cx).edit_steps().iter().enumerate() {
output.push_str(&format!("Step {}:\n", i + 1));
output.push_str(&format!(
"Content: {}\n",
self.context
.read(cx)
.buffer()
.read(cx)
.text_for_range(step.source_range.clone())
.collect::<String>()
));
match &step.operations {
Some(EditStepOperations::Parsed {
operations,
raw_output,
}) => {
output.push_str(&format!("Raw Output:\n{raw_output}\n"));
output.push_str("Parsed Operations:\n");
for op in operations {
output.push_str(&format!(" {:?}\n", op));
}
}
Some(EditStepOperations::Pending(_)) => {
output.push_str("Operations: Pending\n");
}
None => {
output.push_str("Operations: None\n");
}
}
output.push('\n');
}
let editor = self
.workspace
.update(cx, |workspace, cx| Editor::new_in_workspace(workspace, cx));
if let Ok(editor) = editor {
cx.spawn(|_, mut cx| async move {
let editor = editor.await?;
editor.update(&mut cx, |editor, cx| editor.set_text(output, cx))
})
.detach_and_notify_err(cx);
}
}
fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
let cursors = self.cursors(cx);
self.context.update(cx, |context, cx| {
@ -1222,39 +1303,8 @@ impl ContextEditor {
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
});
}
ContextEvent::EditSuggestionsChanged => {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
let excerpt_id = *buffer.as_singleton().unwrap().0;
let context = self.context.read(cx);
let highlighted_rows = context
.edit_suggestions()
.iter()
.map(|suggestion| {
let start = buffer
.anchor_in_excerpt(excerpt_id, suggestion.source_range.start)
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, suggestion.source_range.end)
.unwrap();
start..=end
})
.collect::<Vec<_>>();
editor.clear_row_highlights::<EditSuggestion>();
for range in highlighted_rows {
editor.highlight_rows::<EditSuggestion>(
range,
Some(
cx.theme()
.colors()
.editor_document_highlight_read_background,
),
false,
cx,
);
}
});
ContextEvent::EditStepsChanged => {
cx.notify();
}
ContextEvent::SummaryChanged => {
cx.emit(EditorEvent::TitleChanged);
@ -1515,12 +1565,200 @@ 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 suggestions = new_active_step.edit_suggestions(&self.project, cx);
self.active_edit_step = Some(ActiveEditStep {
start: new_active_step.source_range.start,
assist_ids: Vec::new(),
editor: None,
_open_editor: self.open_editor_for_edit_suggestions(suggestions, cx),
});
}
}
}
_ => {}
}
cx.emit(event.clone());
}
fn open_editor_for_edit_suggestions(
&mut self,
edit_suggestions: Task<HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>>,
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_suggestions = edit_suggestions.await;
let mut assist_ids = Vec::new();
let editor = if edit_suggestions.is_empty() {
return Ok(());
} else 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, suggestion_groups) = edit_suggestions.into_iter().next().unwrap();
let suggestion_group = suggestion_groups.into_iter().next().unwrap();
let editor = workspace.update(&mut cx, |workspace, cx| {
let active_pane = workspace.active_pane().clone();
workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
})?;
cx.update(|cx| {
for suggestion in suggestion_group.suggestions {
let description = suggestion.description.unwrap_or_else(|| "Delete".into());
let range = {
let buffer = editor.read(cx).buffer().read(cx).read(cx);
let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
buffer
.anchor_in_excerpt(excerpt_id, suggestion.range.start)
.unwrap()
..buffer
.anchor_in_excerpt(excerpt_id, suggestion.range.end)
.unwrap()
};
let initial_text = suggestion.prepend_newline.then(|| "\n".into());
InlineAssistant::update_global(cx, |assistant, cx| {
assist_ids.push(assistant.suggest_assist(
&editor,
range,
description,
initial_text,
Some(workspace.clone()),
assistant_panel.upgrade().as_ref(),
cx,
));
});
}
// Scroll the editor to the suggested assist
editor.update(cx, |editor, cx| {
let anchor = {
let buffer = editor.buffer().read(cx).read(cx);
let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
buffer
.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);
for (buffer, suggestion_groups) in edit_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()
};
let initial_text =
suggestion.prepend_newline.then(|| "\n".to_string());
inline_assist_suggestions.push((range, description, initial_text));
}
}
}
multibuffer
})?;
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_text) in inline_assist_suggestions {
assist_ids.push(assistant.suggest_assist(
&editor,
range,
description,
initial_text,
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
};
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());
}
})
})
}
fn handle_editor_search_event(
&mut self,
_: View<Editor>,
@ -1785,173 +2023,6 @@ impl ContextEditor {
});
}
fn apply_edit(&mut self, _: &ApplyEdit, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let project = workspace.read(cx).project().clone();
struct Edit {
old_text: String,
new_text: String,
}
let context = self.context.read(cx);
let context_buffer = context.buffer().read(cx);
let context_buffer_snapshot = context_buffer.snapshot();
let selections = self.editor.read(cx).selections.disjoint_anchors();
let mut selections = selections.iter().peekable();
let selected_suggestions = context
.edit_suggestions()
.iter()
.filter(|suggestion| {
while let Some(selection) = selections.peek() {
if selection
.end
.text_anchor
.cmp(&suggestion.source_range.start, context_buffer)
.is_lt()
{
selections.next();
continue;
}
if selection
.start
.text_anchor
.cmp(&suggestion.source_range.end, context_buffer)
.is_gt()
{
break;
}
return true;
}
false
})
.cloned()
.collect::<Vec<_>>();
let mut opened_buffers: HashMap<PathBuf, Task<Result<Model<Buffer>>>> = HashMap::default();
project.update(cx, |project, cx| {
for suggestion in &selected_suggestions {
opened_buffers
.entry(suggestion.full_path.clone())
.or_insert_with(|| {
project.open_buffer_for_full_path(&suggestion.full_path, cx)
});
}
});
cx.spawn(|this, mut cx| async move {
let mut buffers_by_full_path = HashMap::default();
for (full_path, buffer) in opened_buffers {
if let Some(buffer) = buffer.await.log_err() {
buffers_by_full_path.insert(full_path, buffer);
}
}
let mut suggestions_by_buffer = HashMap::default();
cx.update(|cx| {
for suggestion in selected_suggestions {
if let Some(buffer) = buffers_by_full_path.get(&suggestion.full_path) {
let (_, edits) = suggestions_by_buffer
.entry(buffer.clone())
.or_insert_with(|| (buffer.read(cx).snapshot(), Vec::new()));
let mut lines = context_buffer_snapshot
.as_rope()
.chunks_in_range(
suggestion.source_range.to_offset(&context_buffer_snapshot),
)
.lines();
if let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
let old_text = context_buffer_snapshot
.text_for_range(suggestion.old_text_range)
.collect();
let new_text = context_buffer_snapshot
.text_for_range(suggestion.new_text_range)
.collect();
edits.push(Edit { old_text, new_text });
}
}
}
})?;
let edits_by_buffer = cx
.background_executor()
.spawn(async move {
let mut result = HashMap::default();
for (buffer, (snapshot, suggestions)) in suggestions_by_buffer {
let edits =
result
.entry(buffer)
.or_insert(Vec::<(Range<language::Anchor>, _)>::new());
for suggestion in suggestions {
if let Some(range) =
fuzzy_search_lines(snapshot.as_rope(), &suggestion.old_text)
{
let edit_start = snapshot.anchor_after(range.start);
let edit_end = snapshot.anchor_before(range.end);
if let Err(ix) = edits.binary_search_by(|(range, _)| {
range.start.cmp(&edit_start, &snapshot)
}) {
edits.insert(
ix,
(edit_start..edit_end, suggestion.new_text.clone()),
);
}
} else {
log::info!(
"assistant edit did not match any text in buffer {:?}",
&suggestion.old_text
);
}
}
}
result
})
.await;
let mut project_transaction = ProjectTransaction::default();
let (editor, workspace, title) = this.update(&mut cx, |this, cx| {
for (buffer_handle, edits) in edits_by_buffer {
buffer_handle.update(cx, |buffer, cx| {
buffer.start_transaction();
buffer.edit(
edits,
Some(AutoindentMode::Block {
original_indent_columns: Vec::new(),
}),
cx,
);
buffer.end_transaction(cx);
if let Some(transaction) = buffer.finalize_last_transaction() {
project_transaction
.0
.insert(buffer_handle.clone(), transaction.clone());
}
});
}
(
this.editor.downgrade(),
this.workspace.clone(),
this.title(cx),
)
})?;
Editor::open_project_transaction(
&editor,
workspace,
project_transaction,
format!("Edits from {}", title),
cx,
)
.await
})
.detach_and_log_err(cx);
}
fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
self.context
.update(cx, |context, cx| context.save(None, self.fs.clone(), cx));
@ -1967,6 +2038,14 @@ 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.operations {
Some(EditStepOperations::Pending(_)) => "Computing Changes...",
Some(EditStepOperations::Parsed { .. }) => "Apply Changes",
None => "Send",
},
None => "Send",
};
ButtonLike::new("send_button")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
@ -1974,11 +2053,38 @@ impl ContextEditor {
KeyBinding::for_action_in(&Assist, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
)
.child(Label::new("Send"))
.child(Label::new(button_text))
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Assist, cx);
})
}
fn edit_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a EditStep> {
let newest_cursor = self
.editor
.read(cx)
.selections
.newest_anchor()
.head()
.text_anchor;
let context = self.context.read(cx);
let buffer = context.buffer().read(cx);
let edit_steps = context.edit_steps();
edit_steps
.binary_search_by(|step| {
let step_range = step.source_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() {
Ordering::Less
} else {
Ordering::Equal
}
})
.ok()
.map(|index| &edit_steps[index])
}
}
impl EventEmitter<EditorEvent> for ContextEditor {}
@ -1995,7 +2101,7 @@ impl Render for ContextEditor {
.capture_action(cx.listener(ContextEditor::confirm_command))
.on_action(cx.listener(ContextEditor::assist))
.on_action(cx.listener(ContextEditor::split))
.on_action(cx.listener(ContextEditor::apply_edit))
.on_action(cx.listener(ContextEditor::debug_edit_steps))
.size_full()
.v_flex()
.child(