Detailed changes
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>
@@ -53,7 +53,7 @@ actions!(
DeployPromptLibrary,
ConfirmCommand,
ToggleModelSelector,
- DebugEditSteps
+ DebugWorkflowSteps
]
);
@@ -9,23 +9,22 @@ use crate::{
SlashCommandCompletionProvider, SlashCommandRegistry,
},
terminal_inline_assistant::TerminalInlineAssistant,
- Assist, CodegenStatus, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore,
- CycleMessageRole, DebugEditSteps, DeployHistory, DeployPromptLibrary, InlineAssist,
- InlineAssistId, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector,
- PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
- ResolvedWorkflowStep, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
- WorkflowStepStatus,
+ Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
+ DebugWorkflowSteps, DeployHistory, DeployPromptLibrary, InlineAssist, InlineAssistId,
+ InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand,
+ PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, ResolvedWorkflowStep,
+ SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
};
use crate::{ContextStoreEvent, ShowConfiguration};
-use anyhow::{anyhow, Context as _, Result};
+use anyhow::{anyhow, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
use client::{proto, Client, Status};
use collections::{BTreeSet, HashMap, HashSet};
use editor::{
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
display_map::{
- BlockDisposition, BlockProperties, BlockStyle, Crease, CustomBlockId, RenderBlock,
- ToDisplayPoint,
+ BlockContext, BlockDisposition, BlockProperties, BlockStyle, Crease, CustomBlockId,
+ RenderBlock, ToDisplayPoint,
},
scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint,
@@ -34,10 +33,11 @@ use editor::{display_map::CreaseId, FoldPlaceholder};
use fs::Fs;
use gpui::{
div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext,
- AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter,
- FocusHandle, FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement,
- Pixels, ReadGlobal, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
- Task, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
+ AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EntityId,
+ EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement, IntoElement, Model,
+ ParentElement, Pixels, ReadGlobal, Render, SharedString, StatefulInteractiveElement, Styled,
+ Subscription, Task, Transformation, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
+ WindowContext,
};
use indexed_docs::IndexedDocsStore;
use language::{
@@ -63,6 +63,7 @@ use std::{
time::Duration,
};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
+use text::OffsetRangeExt;
use ui::TintColor;
use ui::{
prelude::*,
@@ -1328,10 +1329,275 @@ struct ScrollPosition {
cursor: Anchor,
}
+struct WorkflowStep {
+ range: Range<language::Anchor>,
+ header_block_id: CustomBlockId,
+ footer_block_id: CustomBlockId,
+ resolved_step: Option<Result<ResolvedWorkflowStep, Arc<anyhow::Error>>>,
+ assist: Option<WorkflowAssist>,
+}
+
+impl WorkflowStep {
+ fn status(&self, cx: &AppContext) -> WorkflowStepStatus {
+ match self.resolved_step.as_ref() {
+ Some(Ok(_)) => {
+ if let Some(assist) = self.assist.as_ref() {
+ let assistant = InlineAssistant::global(cx);
+ if assist
+ .assist_ids
+ .iter()
+ .any(|assist_id| assistant.assist_status(*assist_id, cx).is_pending())
+ {
+ WorkflowStepStatus::Pending
+ } else if assist
+ .assist_ids
+ .iter()
+ .all(|assist_id| assistant.assist_status(*assist_id, cx).is_confirmed())
+ {
+ WorkflowStepStatus::Confirmed
+ } else if assist
+ .assist_ids
+ .iter()
+ .all(|assist_id| assistant.assist_status(*assist_id, cx).is_done())
+ {
+ WorkflowStepStatus::Done
+ } else {
+ WorkflowStepStatus::Idle
+ }
+ } else {
+ WorkflowStepStatus::Idle
+ }
+ }
+ Some(Err(error)) => WorkflowStepStatus::Error(error.clone()),
+ None => WorkflowStepStatus::Resolving,
+ }
+ }
+}
+
+enum WorkflowStepStatus {
+ Resolving,
+ Error(Arc<anyhow::Error>),
+ Idle,
+ Pending,
+ Done,
+ Confirmed,
+}
+
+impl WorkflowStepStatus {
+ pub(crate) fn is_confirmed(&self) -> bool {
+ matches!(self, Self::Confirmed)
+ }
+
+ pub(crate) fn into_element(
+ &self,
+ step_range: Range<language::Anchor>,
+ focus_handle: FocusHandle,
+ editor: WeakView<ContextEditor>,
+ cx: &mut BlockContext<'_, '_>,
+ ) -> AnyElement {
+ let id = EntityId::from(cx.block_id);
+ match self {
+ WorkflowStepStatus::Resolving => Icon::new(IconName::ArrowCircle)
+ .size(IconSize::Small)
+ .with_animation(
+ ("resolving-suggestion-label", id),
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+ )
+ .into_any_element(),
+
+ WorkflowStepStatus::Error(error) => {
+ let error = error.clone();
+ h_flex()
+ .gap_2()
+ .child(
+ div()
+ .id("step-resolution-failure")
+ .child(
+ Label::new("Step Resolution Failed")
+ .size(LabelSize::Small)
+ .color(Color::Error),
+ )
+ .tooltip(move |cx| Tooltip::text(error.to_string(), cx)),
+ )
+ .child(
+ Button::new(("transform", id), "Retry")
+ .icon(IconName::Update)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .label_size(LabelSize::Small)
+ .on_click({
+ let editor = editor.clone();
+ let step_range = step_range.clone();
+ move |_, cx| {
+ editor
+ .update(cx, |this, cx| {
+ this.resolve_workflow_step(step_range.clone(), cx)
+ })
+ .ok();
+ }
+ }),
+ )
+ .into_any()
+ }
+
+ WorkflowStepStatus::Idle => Button::new(("transform", id), "Transform")
+ .icon(IconName::Sparkle)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .label_size(LabelSize::Small)
+ .style(ButtonStyle::Tinted(TintColor::Accent))
+ .tooltip(move |cx| {
+ cx.new_view(|cx| {
+ Tooltip::new("Transform").key_binding(KeyBinding::for_action_in(
+ &Assist,
+ &focus_handle,
+ cx,
+ ))
+ })
+ .into()
+ })
+ .on_click({
+ let editor = editor.clone();
+ let step_range = step_range.clone();
+ move |_, cx| {
+ editor
+ .update(cx, |this, cx| {
+ this.apply_workflow_step(step_range.clone(), cx)
+ })
+ .ok();
+ }
+ })
+ .into_any_element(),
+ WorkflowStepStatus::Pending => Button::new(("stop-transformation", id), "Stop")
+ .icon(IconName::Stop)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .label_size(LabelSize::Small)
+ .style(ButtonStyle::Tinted(TintColor::Negative))
+ .tooltip(move |cx| {
+ cx.new_view(|cx| {
+ Tooltip::new("Stop Transformation").key_binding(KeyBinding::for_action_in(
+ &editor::actions::Cancel,
+ &focus_handle,
+ cx,
+ ))
+ })
+ .into()
+ })
+ .on_click({
+ let editor = editor.clone();
+ let step_range = step_range.clone();
+ move |_, cx| {
+ editor
+ .update(cx, |this, cx| {
+ this.stop_workflow_step(step_range.clone(), cx)
+ })
+ .ok();
+ }
+ })
+ .into_any_element(),
+ WorkflowStepStatus::Done => h_flex()
+ .gap_1()
+ .child(
+ Button::new(("stop-transformation", id), "Reject")
+ .icon(IconName::Close)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .label_size(LabelSize::Small)
+ .style(ButtonStyle::Tinted(TintColor::Negative))
+ .tooltip({
+ let focus_handle = focus_handle.clone();
+ move |cx| {
+ cx.new_view(|cx| {
+ Tooltip::new("Reject Transformation").key_binding(
+ KeyBinding::for_action_in(
+ &editor::actions::Cancel,
+ &focus_handle,
+ cx,
+ ),
+ )
+ })
+ .into()
+ }
+ })
+ .on_click({
+ let editor = editor.clone();
+ let step_range = step_range.clone();
+ move |_, cx| {
+ editor
+ .update(cx, |this, cx| {
+ this.reject_workflow_step(step_range.clone(), cx);
+ })
+ .ok();
+ }
+ }),
+ )
+ .child(
+ Button::new(("confirm-workflow-step", id), "Accept")
+ .icon(IconName::Check)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .label_size(LabelSize::Small)
+ .style(ButtonStyle::Tinted(TintColor::Positive))
+ .tooltip(move |cx| {
+ cx.new_view(|cx| {
+ Tooltip::new("Accept Transformation").key_binding(
+ KeyBinding::for_action_in(&Assist, &focus_handle, cx),
+ )
+ })
+ .into()
+ })
+ .on_click({
+ let editor = editor.clone();
+ let step_range = step_range.clone();
+ move |_, cx| {
+ editor
+ .update(cx, |this, cx| {
+ this.confirm_workflow_step(step_range.clone(), cx);
+ })
+ .ok();
+ }
+ }),
+ )
+ .into_any_element(),
+ WorkflowStepStatus::Confirmed => h_flex()
+ .child(
+ Button::new(("revert-workflow-step", id), "Undo")
+ .style(ButtonStyle::Filled)
+ .icon(Some(IconName::Undo))
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .label_size(LabelSize::Small)
+ .tooltip(|cx| Tooltip::text("Undo Transformation", cx))
+ .on_click({
+ let editor = editor.clone();
+ let step_range = step_range.clone();
+ move |_, cx| {
+ editor
+ .update(cx, |this, cx| {
+ this.undo_workflow_step(step_range.clone(), cx);
+ })
+ .ok();
+ }
+ }),
+ )
+ .into_any_element(),
+ }
+ }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+struct ActiveWorkflowStep {
+ range: Range<language::Anchor>,
+ resolved: bool,
+}
+
struct WorkflowAssist {
editor: WeakView<Editor>,
editor_was_open: bool,
assist_ids: Vec<InlineAssistId>,
+ _observe_assist_status: Task<()>,
}
pub struct ContextEditor {
@@ -1346,9 +1612,9 @@ pub struct ContextEditor {
remote_id: Option<workspace::ViewId>,
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
- workflow_assists: HashMap<Range<language::Anchor>, WorkflowAssist>,
- active_workflow_step_range: Option<Range<language::Anchor>>,
_subscriptions: Vec<Subscription>,
+ workflow_steps: HashMap<Range<language::Anchor>, WorkflowStep>,
+ active_workflow_step: Option<ActiveWorkflowStep>,
assistant_panel: WeakView<AssistantPanel>,
error_message: Option<SharedString>,
}
@@ -1406,8 +1672,8 @@ impl ContextEditor {
pending_slash_command_creases: HashMap::default(),
pending_slash_command_blocks: HashMap::default(),
_subscriptions,
- workflow_assists: HashMap::default(),
- active_workflow_step_range: None,
+ workflow_steps: HashMap::default(),
+ active_workflow_step: None,
assistant_panel,
error_message: None,
};
@@ -1442,17 +1708,19 @@ impl ContextEditor {
}
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
- if !self.apply_workflow_step(cx) {
+ if !self.apply_active_workflow_step(cx) {
self.error_message = None;
self.send_to_model(cx);
cx.notify();
}
}
- fn apply_workflow_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
- if let Some(step_range) = self.active_workflow_step_range.as_ref() {
- if let Some(assists) = self.workflow_assists.get(&step_range) {
- let assist_ids = assists.assist_ids.clone();
+ fn apply_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
+ self.show_workflow_step(range.clone(), cx);
+
+ if let Some(workflow_step) = self.workflow_steps.get(&range) {
+ if let Some(assist) = workflow_step.assist.as_ref() {
+ let assist_ids = assist.assist_ids.clone();
cx.window_context().defer(|cx| {
InlineAssistant::update_global(cx, |assistant, cx| {
for assist_id in assist_ids {
@@ -1460,13 +1728,103 @@ impl ContextEditor {
}
})
});
+ }
+ }
+ }
- !assists.assist_ids.is_empty()
- } else {
- false
+ fn apply_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ let Some(step) = self.active_workflow_step() else {
+ return false;
+ };
+
+ let range = step.range.clone();
+ match step.status(cx) {
+ WorkflowStepStatus::Resolving | WorkflowStepStatus::Pending => true,
+ WorkflowStepStatus::Idle => {
+ self.apply_workflow_step(range, cx);
+ true
+ }
+ WorkflowStepStatus::Done => {
+ self.confirm_workflow_step(range, cx);
+ true
+ }
+ WorkflowStepStatus::Error(_) => {
+ self.resolve_workflow_step(range, cx);
+ true
+ }
+ WorkflowStepStatus::Confirmed => false,
+ }
+ }
+
+ fn resolve_workflow_step(
+ &mut self,
+ range: Range<language::Anchor>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.context.update(cx, |context, cx| {
+ context.resolve_workflow_step(range, self.project.clone(), cx)
+ });
+ }
+
+ fn stop_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
+ if let Some(workflow_step) = self.workflow_steps.get(&range) {
+ if let Some(assist) = workflow_step.assist.as_ref() {
+ let assist_ids = assist.assist_ids.clone();
+ cx.window_context().defer(|cx| {
+ InlineAssistant::update_global(cx, |assistant, cx| {
+ for assist_id in assist_ids {
+ assistant.stop_assist(assist_id, cx);
+ }
+ })
+ });
+ }
+ }
+ }
+
+ fn undo_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
+ if let Some(workflow_step) = self.workflow_steps.get_mut(&range) {
+ if let Some(assist) = workflow_step.assist.take() {
+ cx.window_context().defer(|cx| {
+ InlineAssistant::update_global(cx, |assistant, cx| {
+ for assist_id in assist.assist_ids {
+ assistant.undo_assist(assist_id, cx);
+ }
+ })
+ });
+ }
+ }
+ }
+
+ fn confirm_workflow_step(
+ &mut self,
+ range: Range<language::Anchor>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(workflow_step) = self.workflow_steps.get(&range) {
+ if let Some(assist) = workflow_step.assist.as_ref() {
+ let assist_ids = assist.assist_ids.clone();
+ cx.window_context().defer(move |cx| {
+ InlineAssistant::update_global(cx, |assistant, cx| {
+ for assist_id in assist_ids {
+ assistant.finish_assist(assist_id, false, cx);
+ }
+ })
+ });
+ }
+ }
+ }
+
+ fn reject_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
+ if let Some(workflow_step) = self.workflow_steps.get_mut(&range) {
+ if let Some(assist) = workflow_step.assist.take() {
+ cx.window_context().defer(move |cx| {
+ InlineAssistant::update_global(cx, |assistant, cx| {
+ for assist_id in assist.assist_ids {
+ assistant.finish_assist(assist_id, true, cx);
+ }
+ })
+ });
}
- } else {
- false
}
}
@@ -1490,16 +1848,31 @@ impl ContextEditor {
}
}
- fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
- if !self
+ fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
+ if self
.context
.update(cx, |context, _| context.cancel_last_assist())
{
- cx.propagate();
+ return;
}
+
+ if let Some(active_step) = self.active_workflow_step() {
+ match active_step.status(cx) {
+ WorkflowStepStatus::Pending => {
+ self.stop_workflow_step(active_step.range.clone(), cx);
+ return;
+ }
+ WorkflowStepStatus::Done => {
+ self.reject_workflow_step(active_step.range.clone(), cx);
+ return;
+ }
+ _ => {}
+ }
+ }
+ cx.propagate();
}
- fn debug_edit_steps(&mut self, _: &DebugEditSteps, cx: &mut ViewContext<Self>) {
+ fn debug_workflow_steps(&mut self, _: &DebugWorkflowSteps, cx: &mut ViewContext<Self>) {
let mut output = String::new();
for (i, step) in self.context.read(cx).workflow_steps().iter().enumerate() {
output.push_str(&format!("Step {}:\n", i + 1));
@@ -1513,14 +1886,20 @@ impl ContextEditor {
.collect::<String>()
));
match &step.status {
- WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => {
+ crate::WorkflowStepStatus::Resolved(ResolvedWorkflowStep {
+ title,
+ suggestions,
+ }) => {
output.push_str("Resolution:\n");
output.push_str(&format!(" {:?}\n", title));
output.push_str(&format!(" {:?}\n", suggestions));
}
- WorkflowStepStatus::Pending(_) => {
+ crate::WorkflowStepStatus::Pending(_) => {
output.push_str("Resolution: Pending\n");
}
+ crate::WorkflowStepStatus::Error(error) => {
+ writeln!(output, "Resolution: Error\n{:?}", error).unwrap();
+ }
}
output.push('\n');
}
@@ -1665,8 +2044,12 @@ impl ContextEditor {
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
});
}
- ContextEvent::WorkflowStepsChanged => {
- self.update_active_workflow_step_from_cursor(cx);
+ ContextEvent::WorkflowStepsRemoved(removed) => {
+ self.remove_workflow_steps(removed, cx);
+ cx.notify();
+ }
+ ContextEvent::WorkflowStepUpdated(updated) => {
+ self.update_workflow_step(updated.clone(), cx);
cx.notify();
}
ContextEvent::SummaryChanged => {
@@ -1797,6 +2180,7 @@ impl ContextEditor {
height: 1,
disposition: BlockDisposition::Below,
render: slash_command_error_block_renderer(error_message),
+ priority: 0,
}),
None,
cx,
@@ -1931,31 +2315,186 @@ impl ContextEditor {
}
EditorEvent::SelectionsChanged { .. } => {
self.scroll_position = self.cursor_scroll_position(cx);
- self.update_active_workflow_step_from_cursor(cx);
+ self.update_active_workflow_step(cx);
}
_ => {}
}
cx.emit(event.clone());
}
- fn update_active_workflow_step_from_cursor(&mut self, cx: &mut ViewContext<Self>) {
- let new_step = self
- .workflow_step_range_for_cursor(cx)
- .as_ref()
- .and_then(|step_range| {
- let workflow_step = self
- .context
- .read(cx)
- .workflow_step_for_range(step_range.clone())?;
- Some(workflow_step.tagged_range.clone())
+ fn active_workflow_step(&self) -> Option<&WorkflowStep> {
+ let step = self.active_workflow_step.as_ref()?;
+ self.workflow_steps.get(&step.range)
+ }
+
+ fn remove_workflow_steps(
+ &mut self,
+ removed_steps: &[Range<language::Anchor>],
+ cx: &mut ViewContext<Self>,
+ ) {
+ let mut blocks_to_remove = HashSet::default();
+ for step_range in removed_steps {
+ self.hide_workflow_step(step_range.clone(), cx);
+ if let Some(step) = self.workflow_steps.remove(step_range) {
+ blocks_to_remove.insert(step.header_block_id);
+ blocks_to_remove.insert(step.footer_block_id);
+ }
+ }
+ self.editor.update(cx, |editor, cx| {
+ editor.remove_blocks(blocks_to_remove, None, cx)
+ });
+ self.update_active_workflow_step(cx);
+ }
+
+ fn update_workflow_step(
+ &mut self,
+ step_range: Range<language::Anchor>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let buffer_snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
+ let (&excerpt_id, _, _) = buffer_snapshot.as_singleton().unwrap();
+
+ let Some(step) = self
+ .context
+ .read(cx)
+ .workflow_step_for_range(step_range.clone())
+ else {
+ return;
+ };
+
+ let resolved_step = step.status.into_resolved();
+ if let Some(existing_step) = self.workflow_steps.get_mut(&step_range) {
+ existing_step.resolved_step = resolved_step;
+ } else {
+ let start = buffer_snapshot
+ .anchor_in_excerpt(excerpt_id, step_range.start)
+ .unwrap();
+ let end = buffer_snapshot
+ .anchor_in_excerpt(excerpt_id, step_range.end)
+ .unwrap();
+ let weak_self = cx.view().downgrade();
+ let block_ids = self.editor.update(cx, |editor, cx| {
+ let step_range = step_range.clone();
+ let editor_focus_handle = editor.focus_handle(cx);
+ editor.insert_blocks(
+ vec![
+ BlockProperties {
+ position: start,
+ height: 1,
+ style: BlockStyle::Sticky,
+ render: Box::new({
+ let weak_self = weak_self.clone();
+ let step_range = step_range.clone();
+ move |cx| {
+ let current_status = weak_self
+ .update(&mut **cx, |context_editor, cx| {
+ let step =
+ context_editor.workflow_steps.get(&step_range)?;
+ Some(step.status(cx))
+ })
+ .ok()
+ .flatten();
+
+ let theme = cx.theme().status();
+ let border_color = if current_status
+ .as_ref()
+ .map_or(false, |status| status.is_confirmed())
+ {
+ theme.ignored_border
+ } else {
+ theme.info_border
+ };
+
+ div()
+ .w_full()
+ .px(cx.gutter_dimensions.full_width())
+ .child(
+ h_flex()
+ .w_full()
+ .border_b_1()
+ .border_color(border_color)
+ .pb_1()
+ .justify_end()
+ .gap_2()
+ .children(current_status.as_ref().map(|status| {
+ status.into_element(
+ step_range.clone(),
+ editor_focus_handle.clone(),
+ weak_self.clone(),
+ cx,
+ )
+ })),
+ )
+ .into_any()
+ }
+ }),
+ disposition: BlockDisposition::Above,
+ priority: 0,
+ },
+ BlockProperties {
+ position: end,
+ height: 0,
+ style: BlockStyle::Sticky,
+ render: Box::new(move |cx| {
+ let current_status = weak_self
+ .update(&mut **cx, |context_editor, cx| {
+ let step =
+ context_editor.workflow_steps.get(&step_range)?;
+ Some(step.status(cx))
+ })
+ .ok()
+ .flatten();
+ let theme = cx.theme().status();
+ let border_color = if current_status
+ .as_ref()
+ .map_or(false, |status| status.is_confirmed())
+ {
+ theme.ignored_border
+ } else {
+ theme.info_border
+ };
+
+ div()
+ .w_full()
+ .px(cx.gutter_dimensions.full_width())
+ .child(
+ h_flex().w_full().border_t_1().border_color(border_color),
+ )
+ .into_any()
+ }),
+ disposition: BlockDisposition::Below,
+ priority: 0,
+ },
+ ],
+ None,
+ cx,
+ )
});
- if new_step.as_ref() != self.active_workflow_step_range.as_ref() {
- if let Some(old_step_range) = self.active_workflow_step_range.take() {
- self.hide_workflow_step(old_step_range, cx);
+ self.workflow_steps.insert(
+ step_range.clone(),
+ WorkflowStep {
+ range: step_range.clone(),
+ header_block_id: block_ids[0],
+ footer_block_id: block_ids[1],
+ resolved_step,
+ assist: None,
+ },
+ );
+ }
+
+ self.update_active_workflow_step(cx);
+ }
+
+ fn update_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) {
+ let new_step = self.active_workflow_step_for_cursor(cx);
+ if new_step.as_ref() != self.active_workflow_step.as_ref() {
+ if let Some(old_step) = self.active_workflow_step.take() {
+ self.hide_workflow_step(old_step.range, cx);
}
if let Some(new_step) = new_step {
- self.activate_workflow_step(new_step, cx);
+ self.show_workflow_step(new_step.range.clone(), cx);
+ self.active_workflow_step = Some(new_step);
}
}
}
@@ -1965,35 +2504,30 @@ impl ContextEditor {
step_range: Range<language::Anchor>,
cx: &mut ViewContext<Self>,
) {
- let Some(step_assist) = self.workflow_assists.get_mut(&step_range) else {
+ let Some(step) = self.workflow_steps.get_mut(&step_range) else {
+ return;
+ };
+ let Some(assist) = step.assist.as_ref() else {
return;
};
- let Some(editor) = step_assist.editor.upgrade() else {
- self.workflow_assists.remove(&step_range);
+ let Some(editor) = assist.editor.upgrade() else {
return;
};
- InlineAssistant::update_global(cx, |assistant, cx| {
- step_assist.assist_ids.retain(|assist_id| {
- match assistant.status_for_assist(*assist_id, cx) {
- Some(CodegenStatus::Idle) | None => {
- assistant.finish_assist(*assist_id, true, cx);
- false
- }
- _ => true,
+ if matches!(step.status(cx), WorkflowStepStatus::Idle) {
+ let assist = step.assist.take().unwrap();
+ InlineAssistant::update_global(cx, |assistant, cx| {
+ for assist_id in assist.assist_ids {
+ assistant.finish_assist(assist_id, true, cx)
}
});
- });
- if step_assist.assist_ids.is_empty() {
- let editor_was_open = step_assist.editor_was_open;
- self.workflow_assists.remove(&step_range);
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 !editor_was_open && pane.is_active_preview_item(item_id) {
+ if !assist.editor_was_open && pane.is_active_preview_item(item_id) {
pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
.detach_and_log_err(cx);
}
@@ -2004,200 +2538,205 @@ impl ContextEditor {
}
}
- fn activate_workflow_step(
+ fn show_workflow_step(
&mut self,
step_range: Range<language::Anchor>,
cx: &mut ViewContext<Self>,
- ) -> Option<()> {
- if self.scroll_to_existing_workflow_assist(&step_range, cx) {
- return None;
- }
-
- let step = self
- .workflow_step(&step_range, cx)
- .with_context(|| format!("could not find workflow step for range {:?}", step_range))
- .log_err()?;
- let Some(resolved) = step.status.as_resolved() else {
- return None;
+ ) {
+ let Some(step) = self.workflow_steps.get_mut(&step_range) else {
+ return;
};
- let title = resolved.title.clone();
- let suggestions = resolved.suggestions.clone();
-
- if let Some((editor, assist_ids, editor_was_open)) = {
- let assistant_panel = self.assistant_panel.upgrade()?;
- if suggestions.is_empty() {
- return None;
+ let mut scroll_to_assist_id = None;
+ match step.status(cx) {
+ WorkflowStepStatus::Idle => {
+ if let Some(assist) = step.assist.as_ref() {
+ scroll_to_assist_id = assist.assist_ids.first().copied();
+ } else if let Some(Ok(resolved)) = step.resolved_step.as_ref() {
+ step.assist = Self::open_assists_for_step(
+ resolved,
+ &self.project,
+ &self.assistant_panel,
+ &self.workspace,
+ cx,
+ );
+ }
+ }
+ WorkflowStepStatus::Pending => {
+ if let Some(assist) = step.assist.as_ref() {
+ let assistant = InlineAssistant::global(cx);
+ scroll_to_assist_id = assist
+ .assist_ids
+ .iter()
+ .copied()
+ .find(|assist_id| assistant.assist_status(*assist_id, cx).is_pending());
+ }
+ }
+ WorkflowStepStatus::Done => {
+ if let Some(assist) = step.assist.as_ref() {
+ scroll_to_assist_id = assist.assist_ids.first().copied();
+ }
}
+ _ => {}
+ }
- let editor;
- let mut editor_was_open = false;
- let mut suggestion_groups = Vec::new();
- if suggestions.len() == 1 && suggestions.values().next().unwrap().len() == 1 {
- // If there's only one buffer and one suggestion group, open it directly
- let (buffer, groups) = suggestions.into_iter().next().unwrap();
- let group = groups.into_iter().next().unwrap();
- editor = self
- .workspace
+ if let Some(assist_id) = scroll_to_assist_id {
+ if let Some(editor) = step
+ .assist
+ .as_ref()
+ .and_then(|assists| assists.editor.upgrade())
+ {
+ self.workspace
.update(cx, |workspace, cx| {
- let active_pane = workspace.active_pane().clone();
- editor_was_open =
- workspace.is_project_item_open::<Editor>(&active_pane, &buffer, cx);
- workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
+ workspace.activate_item(&editor, false, false, cx);
})
- .log_err()?;
-
- let (&excerpt_id, _, _) = editor
- .read(cx)
- .buffer()
- .read(cx)
- .read(cx)
- .as_singleton()
- .unwrap();
+ .ok();
+ InlineAssistant::update_global(cx, |assistant, cx| {
+ assistant.scroll_to_assist(assist_id, cx)
+ });
+ }
+ }
+ }
- // 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()
- };
+ fn open_assists_for_step(
+ resolved_step: &ResolvedWorkflowStep,
+ project: &Model<Project>,
+ assistant_panel: &WeakView<AssistantPanel>,
+ workspace: &WeakView<Workspace>,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<WorkflowAssist> {
+ let assistant_panel = assistant_panel.upgrade()?;
+ if resolved_step.suggestions.is_empty() {
+ return None;
+ }
- editor.set_scroll_anchor(
- ScrollAnchor {
- offset: gpui::Point::default(),
- anchor,
- },
+ let editor;
+ let mut editor_was_open = false;
+ let mut suggestion_groups = Vec::new();
+ if resolved_step.suggestions.len() == 1
+ && resolved_step.suggestions.values().next().unwrap().len() == 1
+ {
+ // If there's only one buffer and one suggestion group, open it directly
+ let (buffer, groups) = resolved_step.suggestions.iter().next().unwrap();
+ let group = groups.into_iter().next().unwrap();
+ editor = workspace
+ .update(cx, |workspace, cx| {
+ let active_pane = workspace.active_pane().clone();
+ editor_was_open =
+ workspace.is_project_item_open::<Editor>(&active_pane, buffer, cx);
+ workspace.open_project_item::<Editor>(
+ active_pane,
+ buffer.clone(),
+ false,
+ false,
cx,
- );
- });
+ )
+ })
+ .log_err()?;
- 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 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));
- }
+ let (&excerpt_id, _, _) = editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .read(cx)
+ .as_singleton()
+ .unwrap();
+
+ // 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()
+ };
- 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()?;
- }
+ editor.set_scroll_anchor(
+ ScrollAnchor {
+ offset: gpui::Point::default(),
+ anchor,
+ },
+ cx,
+ );
+ });
- 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,
+ 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 = project.read(cx).replica_id();
+ let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite)
+ .with_title(resolved_step.title.clone());
+ for (buffer, groups) in &resolved_step.suggestions {
+ let excerpt_ids = multibuffer.push_excerpts(
+ buffer.clone(),
+ groups.iter().map(|suggestion_group| ExcerptRange {
+ context: suggestion_group.context_range.clone(),
+ primary: None,
+ }),
cx,
- ));
+ );
+ suggestion_groups.extend(excerpt_ids.into_iter().zip(groups));
}
- }
-
- if let Some(range) = self.active_workflow_step_range.clone() {
- self.workflow_assists.insert(
- range,
- WorkflowAssist {
- assist_ids: assist_ids.clone(),
- editor: editor.downgrade(),
- editor_was_open,
- },
- );
- }
+ multibuffer
+ });
- Some((editor, assist_ids, editor_was_open))
- } {
- self.workflow_assists.insert(
- step_range.clone(),
- WorkflowAssist {
- assist_ids,
- editor_was_open,
- editor: editor.downgrade(),
- },
- );
+ editor = cx.new_view(|cx| {
+ Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx)
+ });
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
+ })
+ .log_err()?;
}
- self.active_workflow_step_range = Some(step_range);
+ 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,
+ workspace,
+ &assistant_panel,
+ cx,
+ ));
+ }
+ }
- Some(())
- }
+ let mut observations = Vec::new();
+ InlineAssistant::update_global(cx, |assistant, _cx| {
+ for assist_id in &assist_ids {
+ observations.push(assistant.observe_assist(*assist_id));
+ }
+ });
- fn active_workflow_step<'a>(&'a self, cx: &'a AppContext) -> Option<&'a crate::WorkflowStep> {
- self.active_workflow_step_range
- .as_ref()
- .and_then(|step_range| {
- self.context
- .read(cx)
- .workflow_step_for_range(step_range.clone())
- })
- }
+ Some(WorkflowAssist {
+ assist_ids,
+ editor: editor.downgrade(),
+ editor_was_open,
+ _observe_assist_status: cx.spawn(|this, mut cx| async move {
+ while !observations.is_empty() {
+ let (result, ix, _) = futures::future::select_all(
+ observations
+ .iter_mut()
+ .map(|observation| Box::pin(observation.changed())),
+ )
+ .await;
- fn workflow_step<'a>(
- &'a mut self,
- step_range: &Range<text::Anchor>,
- cx: &'a mut ViewContext<ContextEditor>,
- ) -> Option<&'a crate::WorkflowStep> {
- self.context
- .read(cx)
- .workflow_step_for_range(step_range.clone())
- }
+ if result.is_err() {
+ observations.remove(ix);
+ }
- fn scroll_to_existing_workflow_assist(
- &self,
- step_range: &Range<language::Anchor>,
- cx: &mut ViewContext<Self>,
- ) -> bool {
- let step_assists = match self.workflow_assists.get(step_range) {
- Some(assists) => assists,
- None => return false,
- };
- let editor = match step_assists.editor.upgrade() {
- Some(editor) => editor,
- None => return false,
- };
- for assist_id in &step_assists.assist_ids {
- match InlineAssistant::global(cx).status_for_assist(*assist_id, cx) {
- Some(CodegenStatus::Idle) | None => {}
- _ => {
- self.workspace
- .update(cx, |workspace, cx| {
- workspace.activate_item(&editor, false, false, cx);
- })
- .ok();
- InlineAssistant::update_global(cx, |assistant, cx| {
- assistant.scroll_to_assist(*assist_id, cx)
- });
- return true;
+ if this.update(&mut cx, |_, cx| cx.notify()).is_err() {
+ break;
+ }
}
- }
- }
- false
+ }),
+ })
}
fn handle_editor_search_event(
@@ -284,7 +284,8 @@ pub enum ContextEvent {
AssistError(String),
MessagesEdited,
SummaryChanged,
- WorkflowStepsChanged,
+ WorkflowStepsRemoved(Vec<Range<language::Anchor>>),
+ WorkflowStepUpdated(Range<language::Anchor>),
StreamedCompletion,
PendingSlashCommandsUpdated {
removed: Vec<Range<language::Anchor>>,
@@ -360,22 +361,17 @@ pub struct ResolvedWorkflowStep {
pub enum WorkflowStepStatus {
Pending(Task<Option<()>>),
Resolved(ResolvedWorkflowStep),
+ Error(Arc<anyhow::Error>),
}
impl WorkflowStepStatus {
- pub fn as_resolved(&self) -> Option<&ResolvedWorkflowStep> {
+ pub fn into_resolved(&self) -> Option<Result<ResolvedWorkflowStep, Arc<anyhow::Error>>> {
match self {
- WorkflowStepStatus::Resolved(suggestions) => Some(suggestions),
+ WorkflowStepStatus::Resolved(resolved) => Some(Ok(resolved.clone())),
+ WorkflowStepStatus::Error(error) => Some(Err(error.clone())),
WorkflowStepStatus::Pending(_) => None,
}
}
-
- pub fn is_resolved(&self) -> bool {
- match self {
- WorkflowStepStatus::Resolved(_) => true,
- WorkflowStepStatus::Pending(_) => false,
- }
- }
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -583,12 +579,16 @@ impl WorkflowSuggestion {
impl Debug for WorkflowStepStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
- WorkflowStepStatus::Pending(_) => write!(f, "EditStepOperations::Pending"),
+ WorkflowStepStatus::Pending(_) => write!(f, "WorkflowStepStatus::Pending"),
WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => f
- .debug_struct("EditStepOperations::Parsed")
+ .debug_struct("WorkflowStepStatus::Resolved")
.field("title", title)
.field("suggestions", suggestions)
.finish(),
+ WorkflowStepStatus::Error(error) => f
+ .debug_tuple("WorkflowStepStatus::Error")
+ .field(error)
+ .finish(),
}
}
}
@@ -1058,7 +1058,7 @@ impl Context {
language::Event::Edited => {
self.count_remaining_tokens(cx);
self.reparse_slash_commands(cx);
- self.prune_invalid_edit_steps(cx);
+ self.prune_invalid_workflow_steps(cx);
cx.emit(ContextEvent::MessagesEdited);
}
_ => {}
@@ -1165,46 +1165,59 @@ impl Context {
}
}
- fn prune_invalid_edit_steps(&mut self, cx: &mut ModelContext<Self>) {
+ fn prune_invalid_workflow_steps(&mut self, cx: &mut ModelContext<Self>) {
let buffer = self.buffer.read(cx);
let prev_len = self.workflow_steps.len();
+ let mut removed = Vec::new();
self.workflow_steps.retain(|step| {
- step.tagged_range.start.is_valid(buffer) && step.tagged_range.end.is_valid(buffer)
+ if step.tagged_range.start.is_valid(buffer) && step.tagged_range.end.is_valid(buffer) {
+ true
+ } else {
+ removed.push(step.tagged_range.clone());
+ false
+ }
});
if self.workflow_steps.len() != prev_len {
- cx.emit(ContextEvent::WorkflowStepsChanged);
+ cx.emit(ContextEvent::WorkflowStepsRemoved(removed));
cx.notify();
}
}
- fn parse_edit_steps_in_range(
+ fn parse_workflow_steps_in_range(
&mut self,
range: Range<usize>,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) {
let mut new_edit_steps = Vec::new();
+ let mut edits = Vec::new();
let buffer = self.buffer.read(cx).snapshot();
let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
let mut in_step = false;
- let mut step_start = 0;
+ let mut step_open_tag_start_ix = 0;
let mut line_start_offset = message_lines.offset();
while let Some(line) = message_lines.next() {
if let Some(step_start_index) = line.find("<step>") {
if !in_step {
in_step = true;
- step_start = line_start_offset + step_start_index;
+ step_open_tag_start_ix = line_start_offset + step_start_index;
}
}
if let Some(step_end_index) = line.find("</step>") {
if in_step {
- let start_anchor = buffer.anchor_after(step_start);
- let end_anchor =
- buffer.anchor_before(line_start_offset + step_end_index + "</step>".len());
- let tagged_range = start_anchor..end_anchor;
+ let step_open_tag_end_ix = step_open_tag_start_ix + "<step>".len();
+ let mut step_end_tag_start_ix = line_start_offset + step_end_index;
+ let step_end_tag_end_ix = step_end_tag_start_ix + "</step>".len();
+ if buffer.reversed_chars_at(step_end_tag_start_ix).next() == Some('\n') {
+ step_end_tag_start_ix -= 1;
+ }
+ edits.push((step_open_tag_start_ix..step_open_tag_end_ix, ""));
+ edits.push((step_end_tag_start_ix..step_end_tag_end_ix, ""));
+ let tagged_range = buffer.anchor_after(step_open_tag_end_ix)
+ ..buffer.anchor_before(step_end_tag_start_ix);
// Check if a step with the same range already exists
let existing_step_index = self
@@ -1212,14 +1225,11 @@ impl Context {
.binary_search_by(|probe| probe.tagged_range.cmp(&tagged_range, &buffer));
if let Err(ix) = existing_step_index {
- // Step doesn't exist, so add it
- let task =
- self.resolve_workflow_step(tagged_range.clone(), project.clone(), cx);
new_edit_steps.push((
ix,
WorkflowStep {
tagged_range,
- status: WorkflowStepStatus::Pending(task),
+ status: WorkflowStepStatus::Pending(Task::ready(None)),
},
));
}
@@ -1231,144 +1241,176 @@ impl Context {
line_start_offset = message_lines.offset();
}
- // Insert new steps and generate their corresponding tasks
+ let mut updated = Vec::new();
for (index, step) in new_edit_steps.into_iter().rev() {
+ let step_range = step.tagged_range.clone();
+ updated.push(step_range.clone());
self.workflow_steps.insert(index, step);
+ self.resolve_workflow_step(step_range, project.clone(), cx);
}
-
- cx.emit(ContextEvent::WorkflowStepsChanged);
- cx.notify();
+ self.buffer
+ .update(cx, |buffer, cx| buffer.edit(edits, None, cx));
}
- fn resolve_workflow_step(
- &self,
+ pub fn resolve_workflow_step(
+ &mut self,
tagged_range: Range<language::Anchor>,
project: Model<Project>,
cx: &mut ModelContext<Self>,
- ) -> Task<Option<()>> {
- let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
- return Task::ready(Err(anyhow!("no active model")).log_err());
+ ) {
+ let Ok(step_index) = self
+ .workflow_steps
+ .binary_search_by(|step| step.tagged_range.cmp(&tagged_range, self.buffer.read(cx)))
+ else {
+ return;
};
let mut request = self.to_completion_request(cx);
- let step_text = self
- .buffer
- .read(cx)
- .text_for_range(tagged_range.clone())
- .collect::<String>();
-
- cx.spawn(|this, mut cx| {
- async move {
- let mut prompt = this.update(&mut cx, |this, _| {
- this.prompt_builder.generate_step_resolution_prompt()
- })??;
- prompt.push_str(&step_text);
+ let Some(edit_step) = self.workflow_steps.get_mut(step_index) else {
+ return;
+ };
- request.messages.push(LanguageModelRequestMessage {
- role: Role::User,
- content: prompt,
- });
+ if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
+ let step_text = self
+ .buffer
+ .read(cx)
+ .text_for_range(tagged_range.clone())
+ .collect::<String>();
- // Invoke the model to get its edit suggestions for this workflow step.
- let resolution = model
- .use_tool::<tool::WorkflowStepResolution>(request, &cx)
- .await?;
+ let tagged_range = tagged_range.clone();
+ edit_step.status = WorkflowStepStatus::Pending(cx.spawn(|this, mut cx| {
+ async move {
+ let result = async {
+ let mut prompt = this.update(&mut cx, |this, _| {
+ this.prompt_builder.generate_step_resolution_prompt()
+ })??;
+ prompt.push_str(&step_text);
+
+ request.messages.push(LanguageModelRequestMessage {
+ role: Role::User,
+ content: prompt,
+ });
- // Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
- let suggestion_tasks: Vec<_> = resolution
- .suggestions
- .iter()
- .map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
- .collect();
-
- // Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
- let suggestions = future::join_all(suggestion_tasks)
- .await
- .into_iter()
- .filter_map(|task| task.log_err())
- .collect::<Vec<_>>();
+ // Invoke the model to get its edit suggestions for this workflow step.
+ let resolution = model
+ .use_tool::<tool::WorkflowStepResolution>(request, &cx)
+ .await?;
- let mut suggestions_by_buffer = HashMap::default();
- for (buffer, suggestion) in suggestions {
- suggestions_by_buffer
- .entry(buffer)
- .or_insert_with(Vec::new)
- .push(suggestion);
- }
+ // Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
+ let suggestion_tasks: Vec<_> = resolution
+ .suggestions
+ .iter()
+ .map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
+ .collect();
- let mut suggestion_groups_by_buffer = HashMap::default();
- for (buffer, mut suggestions) in suggestions_by_buffer {
- let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
- let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
- // Sort suggestions by their range so that earlier, larger ranges come first
- suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
-
- // Merge overlapping suggestions
- suggestions.dedup_by(|a, b| b.try_merge(&a, &snapshot));
-
- // Create context ranges for each suggestion
- for suggestion in suggestions {
- let context_range = {
- let suggestion_point_range = suggestion.range().to_point(&snapshot);
- let start_row = suggestion_point_range.start.row.saturating_sub(5);
- let end_row = cmp::min(
- suggestion_point_range.end.row + 5,
- snapshot.max_point().row,
- );
- let start = snapshot.anchor_before(Point::new(start_row, 0));
- let end = snapshot
- .anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
- start..end
- };
+ // Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
+ let suggestions = future::join_all(suggestion_tasks)
+ .await
+ .into_iter()
+ .filter_map(|task| task.log_err())
+ .collect::<Vec<_>>();
+
+ let mut suggestions_by_buffer = HashMap::default();
+ for (buffer, suggestion) in suggestions {
+ suggestions_by_buffer
+ .entry(buffer)
+ .or_insert_with(Vec::new)
+ .push(suggestion);
+ }
- if let Some(last_group) = suggestion_groups.last_mut() {
- if last_group
- .context_range
- .end
- .cmp(&context_range.start, &snapshot)
- .is_ge()
- {
- // Merge with the previous group if context ranges overlap
- last_group.context_range.end = context_range.end;
- last_group.suggestions.push(suggestion);
- } else {
- // Create a new group
- suggestion_groups.push(WorkflowSuggestionGroup {
- context_range,
- suggestions: vec![suggestion],
- });
+ let mut suggestion_groups_by_buffer = HashMap::default();
+ for (buffer, mut suggestions) in suggestions_by_buffer {
+ let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
+ let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
+ // Sort suggestions by their range so that earlier, larger ranges come first
+ suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
+
+ // Merge overlapping suggestions
+ suggestions.dedup_by(|a, b| b.try_merge(&a, &snapshot));
+
+ // Create context ranges for each suggestion
+ for suggestion in suggestions {
+ let context_range = {
+ let suggestion_point_range =
+ suggestion.range().to_point(&snapshot);
+ let start_row =
+ suggestion_point_range.start.row.saturating_sub(5);
+ let end_row = cmp::min(
+ suggestion_point_range.end.row + 5,
+ snapshot.max_point().row,
+ );
+ let start = snapshot.anchor_before(Point::new(start_row, 0));
+ let end = snapshot.anchor_after(Point::new(
+ end_row,
+ snapshot.line_len(end_row),
+ ));
+ start..end
+ };
+
+ if let Some(last_group) = suggestion_groups.last_mut() {
+ if last_group
+ .context_range
+ .end
+ .cmp(&context_range.start, &snapshot)
+ .is_ge()
+ {
+ // Merge with the previous group if context ranges overlap
+ last_group.context_range.end = context_range.end;
+ last_group.suggestions.push(suggestion);
+ } else {
+ // Create a new group
+ suggestion_groups.push(WorkflowSuggestionGroup {
+ context_range,
+ suggestions: vec![suggestion],
+ });
+ }
+ } else {
+ // Create the first group
+ suggestion_groups.push(WorkflowSuggestionGroup {
+ context_range,
+ suggestions: vec![suggestion],
+ });
+ }
}
- } else {
- // Create the first group
- suggestion_groups.push(WorkflowSuggestionGroup {
- context_range,
- suggestions: vec![suggestion],
- });
+
+ suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
}
- }
- suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
+ Ok((resolution.step_title, suggestion_groups_by_buffer))
+ };
+
+ let result = result.await;
+ this.update(&mut cx, |this, cx| {
+ let step_index = this
+ .workflow_steps
+ .binary_search_by(|step| {
+ step.tagged_range.cmp(&tagged_range, this.buffer.read(cx))
+ })
+ .map_err(|_| anyhow!("edit step not found"))?;
+ if let Some(edit_step) = this.workflow_steps.get_mut(step_index) {
+ edit_step.status = match result {
+ Ok((title, suggestions)) => {
+ WorkflowStepStatus::Resolved(ResolvedWorkflowStep {
+ title,
+ suggestions,
+ })
+ }
+ Err(error) => WorkflowStepStatus::Error(Arc::new(error)),
+ };
+ cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range));
+ cx.notify();
+ }
+ anyhow::Ok(())
+ })?
}
+ .log_err()
+ }));
+ } else {
+ edit_step.status = WorkflowStepStatus::Error(Arc::new(anyhow!("no active model")));
+ }
- this.update(&mut cx, |this, cx| {
- let step_index = this
- .workflow_steps
- .binary_search_by(|step| {
- step.tagged_range.cmp(&tagged_range, this.buffer.read(cx))
- })
- .map_err(|_| anyhow!("edit step not found"))?;
- if let Some(edit_step) = this.workflow_steps.get_mut(step_index) {
- edit_step.status = WorkflowStepStatus::Resolved(ResolvedWorkflowStep {
- title: resolution.step_title,
- suggestions: suggestion_groups_by_buffer,
- });
- cx.emit(ContextEvent::WorkflowStepsChanged);
- }
- anyhow::Ok(())
- })?
- }
- .log_err()
- })
+ cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range));
+ cx.notify();
}
pub fn pending_command_for_position(
@@ -1587,7 +1629,7 @@ impl Context {
message_start_offset..message_new_end_offset
});
if let Some(project) = this.project.clone() {
- this.parse_edit_steps_in_range(message_range, project, cx);
+ this.parse_workflow_steps_in_range(message_range, project, cx);
}
cx.emit(ContextEvent::StreamedCompletion);
@@ -3011,13 +3053,13 @@ mod tests {
vec![
(
Point::new(response_start_row + 2, 0)
- ..Point::new(response_start_row + 14, 7),
- WorkflowStepEditSuggestionStatus::Pending
+ ..Point::new(response_start_row + 13, 3),
+ WorkflowStepTestStatus::Pending
),
(
- Point::new(response_start_row + 16, 0)
- ..Point::new(response_start_row + 28, 7),
- WorkflowStepEditSuggestionStatus::Pending
+ Point::new(response_start_row + 15, 0)
+ ..Point::new(response_start_row + 26, 3),
+ WorkflowStepTestStatus::Pending
),
]
);
@@ -3041,45 +3083,45 @@ mod tests {
// Wait for tool use to be processed.
cx.run_until_parked();
- // Verify that the last edit step is not pending anymore.
+ // Verify that the first edit step is not pending anymore.
context.read_with(cx, |context, cx| {
assert_eq!(
workflow_steps(context, cx),
vec![
(
Point::new(response_start_row + 2, 0)
- ..Point::new(response_start_row + 14, 7),
- WorkflowStepEditSuggestionStatus::Pending
+ ..Point::new(response_start_row + 13, 3),
+ WorkflowStepTestStatus::Resolved
),
(
- Point::new(response_start_row + 16, 0)
- ..Point::new(response_start_row + 28, 7),
- WorkflowStepEditSuggestionStatus::Resolved
+ Point::new(response_start_row + 15, 0)
+ ..Point::new(response_start_row + 26, 3),
+ WorkflowStepTestStatus::Pending
),
]
);
});
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
- enum WorkflowStepEditSuggestionStatus {
+ enum WorkflowStepTestStatus {
Pending,
Resolved,
+ Error,
}
fn workflow_steps(
context: &Context,
cx: &AppContext,
- ) -> Vec<(Range<Point>, WorkflowStepEditSuggestionStatus)> {
+ ) -> Vec<(Range<Point>, WorkflowStepTestStatus)> {
context
.workflow_steps
.iter()
.map(|step| {
let buffer = context.buffer.read(cx);
let status = match &step.status {
- WorkflowStepStatus::Pending(_) => WorkflowStepEditSuggestionStatus::Pending,
- WorkflowStepStatus::Resolved { .. } => {
- WorkflowStepEditSuggestionStatus::Resolved
- }
+ WorkflowStepStatus::Pending(_) => WorkflowStepTestStatus::Pending,
+ WorkflowStepStatus::Resolved { .. } => WorkflowStepTestStatus::Resolved,
+ WorkflowStepStatus::Error(_) => WorkflowStepTestStatus::Error,
};
(step.tagged_range.to_point(buffer), status)
})
@@ -68,6 +68,9 @@ pub struct InlineAssistant {
assists: HashMap<InlineAssistId, InlineAssist>,
assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
+ assist_observations:
+ HashMap<InlineAssistId, (async_watch::Sender<()>, async_watch::Receiver<()>)>,
+ confirmed_assists: HashMap<InlineAssistId, Model<Codegen>>,
prompt_history: VecDeque<String>,
prompt_builder: Arc<PromptBuilder>,
telemetry: Option<Arc<Telemetry>>,
@@ -88,6 +91,8 @@ impl InlineAssistant {
assists: HashMap::default(),
assists_by_editor: HashMap::default(),
assist_groups: HashMap::default(),
+ assist_observations: HashMap::default(),
+ confirmed_assists: HashMap::default(),
prompt_history: VecDeque::default(),
prompt_builder,
telemetry: Some(telemetry),
@@ -343,6 +348,7 @@ impl InlineAssistant {
height: prompt_editor_height,
render: build_assist_editor_renderer(prompt_editor),
disposition: BlockDisposition::Above,
+ priority: 0,
},
BlockProperties {
style: BlockStyle::Sticky,
@@ -357,6 +363,7 @@ impl InlineAssistant {
.into_any_element()
}),
disposition: BlockDisposition::Below,
+ priority: 0,
},
];
@@ -654,8 +661,21 @@ impl InlineAssistant {
if undo {
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
+ } else {
+ self.confirmed_assists.insert(assist_id, assist.codegen);
}
}
+
+ // Remove the assist from the status updates map
+ self.assist_observations.remove(&assist_id);
+ }
+
+ pub fn undo_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
+ let Some(codegen) = self.confirmed_assists.remove(&assist_id) else {
+ return false;
+ };
+ codegen.update(cx, |this, cx| this.undo(cx));
+ true
}
fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
@@ -854,6 +874,10 @@ impl InlineAssistant {
)
})
.log_err();
+
+ if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
+ tx.send(()).ok();
+ }
}
pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
@@ -864,19 +888,24 @@ impl InlineAssistant {
};
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
+
+ if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
+ tx.send(()).ok();
+ }
}
- pub fn status_for_assist(
- &self,
- assist_id: InlineAssistId,
- cx: &WindowContext,
- ) -> Option<CodegenStatus> {
- let assist = self.assists.get(&assist_id)?;
- match &assist.codegen.read(cx).status {
- CodegenStatus::Idle => Some(CodegenStatus::Idle),
- CodegenStatus::Pending => Some(CodegenStatus::Pending),
- CodegenStatus::Done => Some(CodegenStatus::Done),
- CodegenStatus::Error(error) => Some(CodegenStatus::Error(anyhow!("{:?}", error))),
+ pub fn assist_status(&self, assist_id: InlineAssistId, cx: &AppContext) -> InlineAssistStatus {
+ if let Some(assist) = self.assists.get(&assist_id) {
+ match &assist.codegen.read(cx).status {
+ CodegenStatus::Idle => InlineAssistStatus::Idle,
+ CodegenStatus::Pending => InlineAssistStatus::Pending,
+ CodegenStatus::Done => InlineAssistStatus::Done,
+ CodegenStatus::Error(_) => InlineAssistStatus::Error,
+ }
+ } else if self.confirmed_assists.contains_key(&assist_id) {
+ InlineAssistStatus::Confirmed
+ } else {
+ InlineAssistStatus::Canceled
}
}
@@ -1051,6 +1080,7 @@ impl InlineAssistant {
.into_any_element()
}),
disposition: BlockDisposition::Above,
+ priority: 0,
});
}
@@ -1060,6 +1090,37 @@ impl InlineAssistant {
.collect();
})
}
+
+ pub fn observe_assist(&mut self, assist_id: InlineAssistId) -> async_watch::Receiver<()> {
+ if let Some((_, rx)) = self.assist_observations.get(&assist_id) {
+ rx.clone()
+ } else {
+ let (tx, rx) = async_watch::channel(());
+ self.assist_observations.insert(assist_id, (tx, rx.clone()));
+ rx
+ }
+ }
+}
+
+pub enum InlineAssistStatus {
+ Idle,
+ Pending,
+ Done,
+ Error,
+ Confirmed,
+ Canceled,
+}
+
+impl InlineAssistStatus {
+ pub(crate) fn is_pending(&self) -> bool {
+ matches!(self, Self::Pending)
+ }
+ pub(crate) fn is_confirmed(&self) -> bool {
+ matches!(self, Self::Confirmed)
+ }
+ pub(crate) fn is_done(&self) -> bool {
+ matches!(self, Self::Done)
+ }
}
struct EditorInlineAssists {
@@ -1964,6 +2025,8 @@ impl InlineAssist {
if assist.decorations.is_none() {
this.finish_assist(assist_id, false, cx);
+ } else if let Some(tx) = this.assist_observations.get(&assist_id) {
+ tx.0.send(()).ok();
}
}
})
@@ -2037,7 +2100,7 @@ pub struct Codegen {
builder: Arc<PromptBuilder>,
}
-pub enum CodegenStatus {
+enum CodegenStatus {
Idle,
Pending,
Done,
@@ -449,6 +449,7 @@ impl ProjectDiagnosticsEditor {
style: BlockStyle::Sticky,
render: diagnostic_header_renderer(primary),
disposition: BlockDisposition::Above,
+ priority: 0,
});
}
@@ -470,6 +471,7 @@ impl ProjectDiagnosticsEditor {
diagnostic, None, true, true,
),
disposition: BlockDisposition::Below,
+ priority: 0,
});
}
}
@@ -508,6 +510,7 @@ impl ProjectDiagnosticsEditor {
style: block.style,
render: block.render,
disposition: block.disposition,
+ priority: 0,
})
}),
Some(Autoscroll::fit()),
@@ -1281,12 +1281,14 @@ pub mod tests {
position.to_point(&buffer),
height
);
+ let priority = rng.gen_range(1..100);
BlockProperties {
style: BlockStyle::Fixed,
position,
height,
disposition,
render: Box::new(|_| div().into_any()),
+ priority: priority,
}
})
.collect::<Vec<_>>();
@@ -84,6 +84,7 @@ pub struct CustomBlock {
style: BlockStyle,
render: Arc<Mutex<RenderBlock>>,
disposition: BlockDisposition,
+ priority: usize,
}
pub struct BlockProperties<P> {
@@ -92,6 +93,7 @@ pub struct BlockProperties<P> {
pub style: BlockStyle,
pub render: RenderBlock,
pub disposition: BlockDisposition,
+ pub priority: usize,
}
impl<P: Debug> Debug for BlockProperties<P> {
@@ -182,6 +184,7 @@ pub(crate) enum BlockType {
pub(crate) trait BlockLike {
fn block_type(&self) -> BlockType;
fn disposition(&self) -> BlockDisposition;
+ fn priority(&self) -> usize;
}
#[allow(clippy::large_enum_variant)]
@@ -215,6 +218,14 @@ impl BlockLike for Block {
fn disposition(&self) -> BlockDisposition {
self.disposition()
}
+
+ fn priority(&self) -> usize {
+ match self {
+ Block::Custom(block) => block.priority,
+ Block::ExcerptHeader { .. } => usize::MAX,
+ Block::ExcerptFooter { .. } => 0,
+ }
+ }
}
impl Block {
@@ -660,7 +671,10 @@ impl BlockMap {
(BlockType::Header, BlockType::Header) => Ordering::Equal,
(BlockType::Header, _) => Ordering::Less,
(_, BlockType::Header) => Ordering::Greater,
- (BlockType::Custom(a_id), BlockType::Custom(b_id)) => a_id.cmp(&b_id),
+ (BlockType::Custom(a_id), BlockType::Custom(b_id)) => block_b
+ .priority()
+ .cmp(&block_a.priority())
+ .then_with(|| a_id.cmp(&b_id)),
})
})
});
@@ -802,6 +816,7 @@ impl<'a> BlockMapWriter<'a> {
render: Arc::new(Mutex::new(block.render)),
disposition: block.disposition,
style: block.style,
+ priority: block.priority,
});
self.0.custom_blocks.insert(block_ix, new_block.clone());
self.0.custom_blocks_by_id.insert(id, new_block);
@@ -832,6 +847,7 @@ impl<'a> BlockMapWriter<'a> {
style: block.style,
render: block.render.clone(),
disposition: block.disposition,
+ priority: block.priority,
};
let new_block = Arc::new(new_block);
*block = new_block.clone();
@@ -1463,6 +1479,7 @@ mod tests {
height: 1,
disposition: BlockDisposition::Above,
render: Box::new(|_| div().into_any()),
+ priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -1470,6 +1487,7 @@ mod tests {
height: 2,
disposition: BlockDisposition::Above,
render: Box::new(|_| div().into_any()),
+ priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -1477,6 +1495,7 @@ mod tests {
height: 3,
disposition: BlockDisposition::Below,
render: Box::new(|_| div().into_any()),
+ priority: 0,
},
]);
@@ -1716,6 +1735,7 @@ mod tests {
height: 1,
disposition: BlockDisposition::Above,
render: Box::new(|_| div().into_any()),
+ priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -1723,6 +1743,7 @@ mod tests {
height: 2,
disposition: BlockDisposition::Above,
render: Box::new(|_| div().into_any()),
+ priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -1730,6 +1751,7 @@ mod tests {
height: 3,
disposition: BlockDisposition::Below,
render: Box::new(|_| div().into_any()),
+ priority: 0,
},
]);
@@ -1819,6 +1841,7 @@ mod tests {
disposition: BlockDisposition::Above,
render: Box::new(|_| div().into_any()),
height: 1,
+ priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
@@ -1826,6 +1849,7 @@ mod tests {
disposition: BlockDisposition::Below,
render: Box::new(|_| div().into_any()),
height: 1,
+ priority: 0,
},
]);
@@ -1924,6 +1948,7 @@ mod tests {
height,
disposition,
render: Box::new(|_| div().into_any()),
+ priority: 0,
}
})
.collect::<Vec<_>>();
@@ -1944,6 +1969,7 @@ mod tests {
style: props.style,
render: Box::new(|_| div().into_any()),
disposition: props.disposition,
+ priority: 0,
}));
for (block_id, props) in block_ids.into_iter().zip(block_properties) {
custom_blocks.push((block_id, props));
@@ -2014,6 +2040,7 @@ mod tests {
disposition: block.disposition,
id: *id,
height: block.height,
+ priority: block.priority,
},
)
}));
@@ -2235,6 +2262,7 @@ mod tests {
disposition: BlockDisposition,
id: CustomBlockId,
height: u32,
+ priority: usize,
},
}
@@ -2250,6 +2278,14 @@ mod tests {
fn disposition(&self) -> BlockDisposition {
self.disposition()
}
+
+ fn priority(&self) -> usize {
+ match self {
+ ExpectedBlock::Custom { priority, .. } => *priority,
+ ExpectedBlock::ExcerptHeader { .. } => usize::MAX,
+ ExpectedBlock::ExcerptFooter { .. } => 0,
+ }
+ }
}
impl ExpectedBlock {
@@ -2277,6 +2313,7 @@ mod tests {
id: block.id,
disposition: block.disposition,
height: block.height,
+ priority: block.priority,
},
Block::ExcerptHeader {
height,
@@ -9614,6 +9614,7 @@ impl Editor {
}
}),
disposition: BlockDisposition::Below,
+ priority: 0,
}],
Some(Autoscroll::fit()),
cx,
@@ -9877,6 +9878,7 @@ impl Editor {
height: message_height,
render: diagnostic_block_renderer(diagnostic, None, true, true),
disposition: BlockDisposition::Below,
+ priority: 0,
}
}),
cx,
@@ -10182,6 +10184,7 @@ impl Editor {
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
+ cx.notify();
blocks
}
@@ -10196,6 +10199,7 @@ impl Editor {
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
+ cx.notify();
}
pub fn replace_blocks(
@@ -10208,9 +10212,8 @@ impl Editor {
.update(cx, |display_map, _cx| display_map.replace_blocks(renderers));
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
- } else {
- cx.notify();
}
+ cx.notify();
}
pub fn remove_blocks(
@@ -10225,6 +10228,7 @@ impl Editor {
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
+ cx.notify();
}
pub fn row_for_block(
@@ -3785,6 +3785,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
disposition: BlockDisposition::Below,
height: 1,
render: Box::new(|_| div().into_any()),
+ priority: 0,
}],
Some(Autoscroll::fit()),
cx,
@@ -6478,6 +6478,7 @@ mod tests {
height: 3,
position: Anchor::min(),
render: Box::new(|cx| div().h(3. * cx.line_height()).into_any()),
+ priority: 0,
}],
None,
cx,
@@ -525,6 +525,7 @@ impl Editor {
.child(editor_with_deleted_text.clone())
.into_any_element()
}),
+ priority: 0,
}),
None,
cx,
@@ -87,6 +87,7 @@ impl EditorBlock {
style: BlockStyle::Sticky,
render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
disposition: BlockDisposition::Below,
+ priority: 0,
};
let block_id = editor.insert_blocks([block], None, cx)[0];
@@ -50,6 +50,7 @@ pub enum TintColor {
Accent,
Negative,
Warning,
+ Positive,
}
impl TintColor {
@@ -73,6 +74,12 @@ impl TintColor {
label_color: cx.theme().colors().text,
icon_color: cx.theme().colors().text,
},
+ TintColor::Positive => ButtonLikeStyles {
+ background: cx.theme().status().success_background,
+ border_color: cx.theme().status().success_border,
+ label_color: cx.theme().colors().text,
+ icon_color: cx.theme().colors().text,
+ },
}
}
}
@@ -83,6 +90,7 @@ impl From<TintColor> for Color {
TintColor::Accent => Color::Accent,
TintColor::Negative => Color::Error,
TintColor::Warning => Color::Warning,
+ TintColor::Positive => Color::Success,
}
}
}
@@ -256,6 +256,7 @@ pub enum IconName {
TextSearch,
Trash,
TriangleRight,
+ Undo,
Update,
WholeWord,
XCircle,
@@ -419,6 +420,7 @@ impl IconName {
IconName::Trash => "icons/trash.svg",
IconName::TriangleRight => "icons/triangle_right.svg",
IconName::Update => "icons/update.svg",
+ IconName::Undo => "icons/undo.svg",
IconName::WholeWord => "icons/word_search.svg",
IconName::XCircle => "icons/error.svg",
IconName::ZedAssistant => "icons/zed_assistant.svg",