Add a workflow step resolution view (#16315)

You can now click on a step header (the words `Step 3`, etc) to open a
new tab containing a dedicated view for the resolution of that step.
This view looks similar to a context editor, and has sections for the
step input, the streaming tool output, and the interpreted results.

Hitting `cmd-enter` in this view re-resolves the step.


https://github.com/user-attachments/assets/64d82cdb-e70f-4204-8697-b30df5a645d5



Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-08-15 14:16:58 -07:00 committed by GitHub
parent 583959f82a
commit 776442f3ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 525 additions and 79 deletions

View File

@ -15,7 +15,7 @@ use crate::{
DebugWorkflowSteps, DeployHistory, DeployPromptLibrary, InlineAssist, InlineAssistId,
InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand,
PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, ResolvedWorkflowStep,
SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, WorkflowStepView,
};
use crate::{ContextStoreEvent, ShowConfiguration};
use anyhow::{anyhow, Result};
@ -36,10 +36,10 @@ use fs::Fs;
use gpui::{
canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
Context as _, DismissEvent, Empty, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render,
RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task,
Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext,
Context as _, CursorStyle, DismissEvent, Empty, Entity, EntityId, EventEmitter, FocusHandle,
FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
ReadGlobal, Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled,
Subscription, Task, Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext,
};
use indexed_docs::IndexedDocsStore;
use language::{
@ -59,7 +59,7 @@ use std::{
borrow::Cow,
cmp::{self, Ordering},
fmt::Write,
ops::Range,
ops::{DerefMut, Range},
path::PathBuf,
sync::Arc,
time::Duration,
@ -1388,7 +1388,7 @@ impl WorkflowStep {
fn status(&self, cx: &AppContext) -> WorkflowStepStatus {
match self.resolved_step.as_ref() {
Some(Ok(step)) => {
if step.suggestions.is_empty() {
if step.suggestion_groups.is_empty() {
WorkflowStepStatus::Empty
} else if let Some(assist) = self.assist.as_ref() {
let assistant = InlineAssistant::global(cx);
@ -2030,7 +2030,10 @@ impl ContextEditor {
.collect::<String>()
));
match &step.resolution.read(cx).result {
Some(Ok(ResolvedWorkflowStep { title, suggestions })) => {
Some(Ok(ResolvedWorkflowStep {
title,
suggestion_groups: suggestions,
})) => {
output.push_str("Resolution:\n");
output.push_str(&format!(" {:?}\n", title));
output.push_str(&format!(" {:?}\n", suggestions));
@ -2571,16 +2574,33 @@ impl ContextEditor {
})
.unwrap_or_default();
let step_label = if let Some(index) = step_index {
Label::new(format!("Step {index}")).size(LabelSize::Small)
} else {
Label::new("Step").size(LabelSize::Small)
};
let step_label = if current_status.as_ref().is_some_and(|status| status.is_confirmed()) {
h_flex().items_center().gap_2().child(step_label.strikethrough(true).color(Color::Muted)).child(Icon::new(IconName::Check).size(IconSize::Small).color(Color::Created))
} else {
div().child(step_label)
};
let step_label = step_label.id("step")
.cursor(CursorStyle::PointingHand)
.on_click({
let this = weak_self.clone();
let step_range = step_range.clone();
move |_, cx| {
this
.update(cx, |this, cx| {
this.open_workflow_step(
step_range.clone(), cx,
);
})
.ok();
}
});
div()
.w_full()
.px(cx.gutter_dimensions.full_width())
@ -2699,6 +2719,30 @@ impl ContextEditor {
self.update_active_workflow_step(cx);
}
fn open_workflow_step(
&mut self,
step_range: Range<language::Anchor>,
cx: &mut ViewContext<Self>,
) -> Option<()> {
let pane = self
.assistant_panel
.update(cx, |panel, _| panel.pane())
.ok()??;
let context = self.context.read(cx);
let language_registry = context.language_registry();
let step = context.workflow_step_for_range(step_range)?;
let resolution = step.resolution.clone();
let view = cx.new_view(|cx| {
WorkflowStepView::new(self.context.clone(), resolution, language_registry, cx)
});
cx.deref_mut().defer(move |cx| {
pane.update(cx, |pane, cx| {
pane.add_item(Box::new(view), true, true, None, cx);
});
});
None
}
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() {
@ -2820,18 +2864,24 @@ impl ContextEditor {
cx: &mut ViewContext<Self>,
) -> Option<WorkflowAssist> {
let assistant_panel = assistant_panel.upgrade()?;
if resolved_step.suggestions.is_empty() {
if resolved_step.suggestion_groups.is_empty() {
return None;
}
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 resolved_step.suggestion_groups.len() == 1
&& resolved_step
.suggestion_groups
.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 (buffer, groups) = resolved_step.suggestion_groups.iter().next().unwrap();
let group = groups.into_iter().next().unwrap();
editor = workspace
.update(cx, |workspace, cx| {
@ -2884,7 +2934,7 @@ impl ContextEditor {
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 {
for (buffer, groups) in &resolved_step.suggestion_groups {
let excerpt_ids = multibuffer.push_excerpts(
buffer.clone(),
groups.iter().map(|suggestion_group| ExcerptRange {

View File

@ -838,6 +838,10 @@ impl Context {
&self.buffer
}
pub fn language_registry(&self) -> Arc<LanguageRegistry> {
self.language_registry.clone()
}
pub fn project(&self) -> Option<Model<Project>> {
self.project.clone()
}
@ -1073,6 +1077,7 @@ impl Context {
}
fn parse_workflow_steps_in_range(&mut self, range: Range<usize>, cx: &mut ModelContext<Self>) {
let weak_self = cx.weak_model();
let mut new_edit_steps = Vec::new();
let mut edits = Vec::new();
@ -1116,7 +1121,10 @@ impl Context {
ix,
WorkflowStep {
resolution: cx.new_model(|_| {
WorkflowStepResolution::new(tagged_range.clone())
WorkflowStepResolution::new(
tagged_range.clone(),
weak_self.clone(),
)
}),
tagged_range,
_task: None,
@ -1161,21 +1169,21 @@ impl Context {
cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range.clone()));
cx.notify();
let task = self.workflow_steps[step_index]
.resolution
.update(cx, |resolution, cx| resolution.resolve(self, cx));
self.workflow_steps[step_index]._task = task.map(|task| {
cx.spawn(|this, mut cx| async move {
task.await;
this.update(&mut cx, |_, cx| {
cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range));
cx.notify();
})
.ok();
})
let resolution = self.workflow_steps[step_index].resolution.clone();
cx.defer(move |cx| {
resolution.update(cx, |resolution, cx| resolution.resolve(cx));
});
}
pub fn workflow_step_updated(
&mut self,
range: Range<language::Anchor>,
cx: &mut ModelContext<Self>,
) {
cx.emit(ContextEvent::WorkflowStepUpdated(range));
cx.notify();
}
pub fn pending_command_for_position(
&mut self,
position: language::Anchor,

View File

@ -54,7 +54,10 @@ impl ContextInspector {
let step = context.read(cx).workflow_step_for_range(range)?;
let mut output = String::from("\n\n");
match &step.resolution.read(cx).result {
Some(Ok(ResolvedWorkflowStep { title, suggestions })) => {
Some(Ok(ResolvedWorkflowStep {
title,
suggestion_groups: suggestions,
})) => {
writeln!(output, "Resolution:").ok()?;
writeln!(output, " {title:?}").ok()?;
if suggestions.is_empty() {
@ -189,27 +192,31 @@ fn pretty_print_workflow_suggestion(
) {
use std::fmt::Write;
let (position, description, range) = match &suggestion.kind {
WorkflowSuggestionKind::Update { range, description } => {
(None, Some(description), Some(range))
}
WorkflowSuggestionKind::Update {
range, description, ..
} => (None, Some(description), Some(range)),
WorkflowSuggestionKind::CreateFile { description } => (None, Some(description), None),
WorkflowSuggestionKind::AppendChild {
position,
description,
..
} => (Some(position), Some(description), None),
WorkflowSuggestionKind::InsertSiblingBefore {
position,
description,
..
} => (Some(position), Some(description), None),
WorkflowSuggestionKind::InsertSiblingAfter {
position,
description,
..
} => (Some(position), Some(description), None),
WorkflowSuggestionKind::PrependChild {
position,
description,
..
} => (Some(position), Some(description), None),
WorkflowSuggestionKind::Delete { range } => (None, None, Some(range)),
WorkflowSuggestionKind::Delete { range, .. } => (None, None, Some(range)),
};
writeln!(out, " Tool input: {}", suggestion.tool_input).ok();
writeln!(

View File

@ -1,3 +1,5 @@
mod step_view;
use crate::{
prompts::StepResolutionContext, AssistantPanel, Context, InlineAssistId, InlineAssistant,
};
@ -5,8 +7,11 @@ use anyhow::{anyhow, Error, Result};
use collections::HashMap;
use editor::Editor;
use futures::future;
use gpui::{Model, ModelContext, Task, UpdateGlobal as _, View, WeakView, WindowContext};
use language::{Anchor, Buffer, BufferSnapshot};
use gpui::{
AppContext, Model, ModelContext, Task, UpdateGlobal as _, View, WeakModel, WeakView,
WindowContext,
};
use language::{Anchor, Buffer, BufferSnapshot, SymbolPath};
use language_model::{LanguageModelRegistry, LanguageModelRequestMessage, Role};
use project::Project;
use rope::Point;
@ -17,16 +22,20 @@ use text::{AnchorRangeExt as _, OffsetRangeExt as _};
use util::ResultExt as _;
use workspace::Workspace;
pub use step_view::WorkflowStepView;
pub struct WorkflowStepResolution {
tagged_range: Range<Anchor>,
output: String,
context: WeakModel<Context>,
resolve_task: Option<Task<()>>,
pub result: Option<Result<ResolvedWorkflowStep, Arc<Error>>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedWorkflowStep {
pub title: String,
pub suggestions: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
pub suggestion_groups: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@ -67,6 +76,7 @@ impl WorkflowSuggestion {
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WorkflowSuggestionKind {
Update {
symbol_path: SymbolPath,
range: Range<language::Anchor>,
description: String,
},
@ -74,48 +84,63 @@ pub enum WorkflowSuggestionKind {
description: String,
},
InsertSiblingBefore {
symbol_path: SymbolPath,
position: language::Anchor,
description: String,
},
InsertSiblingAfter {
symbol_path: SymbolPath,
position: language::Anchor,
description: String,
},
PrependChild {
symbol_path: Option<SymbolPath>,
position: language::Anchor,
description: String,
},
AppendChild {
symbol_path: Option<SymbolPath>,
position: language::Anchor,
description: String,
},
Delete {
symbol_path: SymbolPath,
range: Range<language::Anchor>,
},
}
impl WorkflowStepResolution {
pub fn new(range: Range<Anchor>) -> Self {
pub fn new(range: Range<Anchor>, context: WeakModel<Context>) -> Self {
Self {
tagged_range: range,
output: String::new(),
context,
result: None,
resolve_task: None,
}
}
pub fn resolve(
&mut self,
context: &Context,
cx: &mut ModelContext<WorkflowStepResolution>,
) -> Option<Task<()>> {
pub fn step_text(&self, context: &Context, cx: &AppContext) -> String {
context
.buffer()
.clone()
.read(cx)
.text_for_range(self.tagged_range.clone())
.collect::<String>()
}
pub fn resolve(&mut self, cx: &mut ModelContext<WorkflowStepResolution>) -> Option<()> {
let range = self.tagged_range.clone();
let context = self.context.upgrade()?;
let context = context.read(cx);
let project = context.project()?;
let context_buffer = context.buffer().clone();
let prompt_builder = context.prompt_builder();
let mut request = context.to_completion_request(cx);
let model = LanguageModelRegistry::read_global(cx).active_model();
let context_buffer = context.buffer();
let step_text = context_buffer
.read(cx)
.text_for_range(self.tagged_range.clone())
.text_for_range(range.clone())
.collect::<String>();
let mut workflow_context = String::new();
@ -127,7 +152,7 @@ impl WorkflowStepResolution {
write!(&mut workflow_context, "</message>").unwrap();
}
Some(cx.spawn(|this, mut cx| async move {
self.resolve_task = Some(cx.spawn(|this, mut cx| async move {
let result = async {
let Some(model) = model else {
return Err(anyhow!("no model selected"));
@ -136,6 +161,7 @@ impl WorkflowStepResolution {
this.update(&mut cx, |this, cx| {
this.output.clear();
this.result = None;
this.result_updated(cx);
cx.notify();
})?;
@ -167,6 +193,11 @@ impl WorkflowStepResolution {
serde_json::from_str::<tool::WorkflowStepResolutionTool>(&this.output)
})??;
this.update(&mut cx, |this, cx| {
this.output = serde_json::to_string_pretty(&resolution).unwrap();
cx.notify();
})?;
// Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
let suggestion_tasks: Vec<_> = resolution
.suggestions
@ -251,13 +282,28 @@ impl WorkflowStepResolution {
let result = result.await;
this.update(&mut cx, |this, cx| {
this.result = Some(match result {
Ok((title, suggestions)) => Ok(ResolvedWorkflowStep { title, suggestions }),
Ok((title, suggestion_groups)) => Ok(ResolvedWorkflowStep {
title,
suggestion_groups,
}),
Err(error) => Err(Arc::new(error)),
});
this.context
.update(cx, |context, cx| context.workflow_step_updated(range, cx))
.ok();
cx.notify();
})
.ok();
}))
}));
None
}
fn result_updated(&mut self, cx: &mut ModelContext<Self>) {
self.context
.update(cx, |context, cx| {
context.workflow_step_updated(self.tagged_range.clone(), cx)
})
.ok();
}
}
@ -270,7 +316,7 @@ impl WorkflowSuggestionKind {
| Self::InsertSiblingAfter { position, .. }
| Self::PrependChild { position, .. }
| Self::AppendChild { position, .. } => *position..*position,
Self::Delete { range } => range.clone(),
Self::Delete { range, .. } => range.clone(),
}
}
@ -298,6 +344,30 @@ impl WorkflowSuggestionKind {
}
}
fn symbol_path(&self) -> Option<&SymbolPath> {
match self {
Self::Update { symbol_path, .. } => Some(symbol_path),
Self::InsertSiblingBefore { symbol_path, .. } => Some(symbol_path),
Self::InsertSiblingAfter { symbol_path, .. } => Some(symbol_path),
Self::PrependChild { symbol_path, .. } => symbol_path.as_ref(),
Self::AppendChild { symbol_path, .. } => symbol_path.as_ref(),
Self::Delete { symbol_path, .. } => Some(symbol_path),
Self::CreateFile { .. } => None,
}
}
fn kind(&self) -> &str {
match self {
Self::Update { .. } => "Update",
Self::CreateFile { .. } => "CreateFile",
Self::InsertSiblingBefore { .. } => "InsertSiblingBefore",
Self::InsertSiblingAfter { .. } => "InsertSiblingAfter",
Self::PrependChild { .. } => "PrependChild",
Self::AppendChild { .. } => "AppendChild",
Self::Delete { .. } => "Delete",
}
}
fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
let range = self.range();
let other_range = other.range();
@ -333,7 +403,9 @@ impl WorkflowSuggestionKind {
let snapshot = buffer.read(cx).snapshot(cx);
match self {
Self::Update { range, description } => {
Self::Update {
range, description, ..
} => {
initial_prompt = description.clone();
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
@ -345,6 +417,7 @@ impl WorkflowSuggestionKind {
Self::InsertSiblingBefore {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
@ -361,6 +434,7 @@ impl WorkflowSuggestionKind {
Self::InsertSiblingAfter {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
@ -377,6 +451,7 @@ impl WorkflowSuggestionKind {
Self::PrependChild {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
@ -393,6 +468,7 @@ impl WorkflowSuggestionKind {
Self::AppendChild {
position,
description,
..
} => {
let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
initial_prompt = description.clone();
@ -406,7 +482,7 @@ impl WorkflowSuggestionKind {
line_start..line_start
});
}
Self::Delete { range } => {
Self::Delete { range, .. } => {
initial_prompt = "Delete".to_string();
suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
@ -528,10 +604,10 @@ pub mod tool {
symbol,
description,
} => {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let start = symbol
.annotation_range
.map_or(symbol.range.start, |range| range.start);
@ -541,7 +617,11 @@ pub mod tool {
snapshot.line_len(symbol.range.end.row),
);
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
WorkflowSuggestionKind::Update { range, description }
WorkflowSuggestionKind::Update {
range,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::Create { description } => {
WorkflowSuggestionKind::CreateFile { description }
@ -550,10 +630,10 @@ pub mod tool {
symbol,
description,
} => {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_before(
symbol
.annotation_range
@ -564,20 +644,22 @@ pub mod tool {
WorkflowSuggestionKind::InsertSiblingBefore {
position,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::InsertSiblingAfter {
symbol,
description,
} => {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_after(symbol.range.end);
WorkflowSuggestionKind::InsertSiblingAfter {
position,
description,
symbol_path,
}
}
WorkflowSuggestionToolKind::PrependChild {
@ -585,10 +667,10 @@ pub mod tool {
description,
} => {
if let Some(symbol) = symbol {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_after(
symbol
@ -598,11 +680,13 @@ pub mod tool {
WorkflowSuggestionKind::PrependChild {
position,
description,
symbol_path: Some(symbol_path),
}
} else {
WorkflowSuggestionKind::PrependChild {
position: language::Anchor::MIN,
description,
symbol_path: None,
}
}
}
@ -611,10 +695,10 @@ pub mod tool {
description,
} => {
if let Some(symbol) = symbol {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let position = snapshot.anchor_before(
symbol
@ -624,19 +708,21 @@ pub mod tool {
WorkflowSuggestionKind::AppendChild {
position,
description,
symbol_path: Some(symbol_path),
}
} else {
WorkflowSuggestionKind::PrependChild {
position: language::Anchor::MAX,
description,
symbol_path: None,
}
}
}
WorkflowSuggestionToolKind::Delete { symbol } => {
let symbol = outline
let (symbol_path, symbol) = outline
.find_most_similar(&symbol)
.with_context(|| format!("symbol not found: {:?}", symbol))?
.to_point(&snapshot);
.with_context(|| format!("symbol not found: {:?}", symbol))?;
let symbol = symbol.to_point(&snapshot);
let start = symbol
.annotation_range
.map_or(symbol.range.start, |range| range.start);
@ -646,7 +732,7 @@ pub mod tool {
snapshot.line_len(symbol.range.end.row),
);
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
WorkflowSuggestionKind::Delete { range }
WorkflowSuggestionKind::Delete { range, symbol_path }
}
};

View File

@ -0,0 +1,290 @@
use super::WorkflowStepResolution;
use crate::{Assist, Context};
use editor::{
display_map::{BlockDisposition, BlockProperties, BlockStyle},
Editor, EditorEvent, ExcerptRange, MultiBuffer,
};
use gpui::{
div, AnyElement, AppContext, Context as _, Empty, EventEmitter, FocusableView, IntoElement,
Model, ParentElement as _, Render, SharedString, Styled as _, View, ViewContext,
VisualContext as _, WeakModel, WindowContext,
};
use language::{language_settings::SoftWrap, Anchor, Buffer, LanguageRegistry};
use std::{ops::DerefMut, sync::Arc};
use theme::ActiveTheme as _;
use ui::{
h_flex, v_flex, ButtonCommon as _, ButtonLike, ButtonStyle, Color, InteractiveElement as _,
Label, LabelCommon as _,
};
use workspace::{
item::{self, Item},
pane,
searchable::SearchableItemHandle,
};
pub struct WorkflowStepView {
step: WeakModel<WorkflowStepResolution>,
tool_output_buffer: Model<Buffer>,
editor: View<Editor>,
}
impl WorkflowStepView {
pub fn new(
context: Model<Context>,
step: Model<WorkflowStepResolution>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let tool_output_buffer = cx.new_model(|cx| Buffer::local(step.read(cx).output.clone(), cx));
let buffer = cx.new_model(|cx| {
let mut buffer = MultiBuffer::without_headers(0, language::Capability::ReadWrite);
buffer.push_excerpts(
context.read(cx).buffer().clone(),
[ExcerptRange {
context: step.read(cx).tagged_range.clone(),
primary: None,
}],
cx,
);
buffer.push_excerpts(
tool_output_buffer.clone(),
[ExcerptRange {
context: Anchor::MIN..Anchor::MAX,
primary: None,
}],
cx,
);
buffer
});
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let output_excerpt = buffer_snapshot.excerpts().skip(1).next().unwrap().0;
let input_start_anchor = multi_buffer::Anchor::min();
let output_start_anchor = buffer_snapshot
.anchor_in_excerpt(output_excerpt, Anchor::MIN)
.unwrap();
let output_end_anchor = multi_buffer::Anchor::max();
let handle = cx.view().downgrade();
let editor = cx.new_view(|cx| {
let mut editor = Editor::for_multibuffer(buffer.clone(), None, false, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_runnables(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_read_only(true);
editor.insert_blocks(
[
BlockProperties {
position: input_start_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(|cx| section_header("Step Input", cx)),
disposition: BlockDisposition::Above,
priority: 0,
},
BlockProperties {
position: output_start_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(|cx| section_header("Tool Output", cx)),
disposition: BlockDisposition::Above,
priority: 0,
},
BlockProperties {
position: output_end_anchor,
height: 1,
style: BlockStyle::Fixed,
render: Box::new(move |cx| {
if let Some(result) = handle.upgrade().and_then(|this| {
this.update(cx.deref_mut(), |this, cx| this.render_result(cx))
}) {
v_flex()
.child(section_header("Output", cx))
.child(
div().pl(cx.gutter_dimensions.full_width()).child(result),
)
.into_any_element()
} else {
Empty.into_any_element()
}
}),
disposition: BlockDisposition::Below,
priority: 0,
},
],
None,
cx,
);
editor
});
cx.observe(&step, Self::step_updated).detach();
cx.observe_release(&step, Self::step_released).detach();
cx.spawn(|this, mut cx| async move {
if let Ok(language) = language_registry.language_for_name("JSON").await {
this.update(&mut cx, |this, cx| {
this.tool_output_buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(language), cx);
});
})
.ok();
}
})
.detach();
Self {
tool_output_buffer,
step: step.downgrade(),
editor,
}
}
fn render_result(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let step = self.step.upgrade()?;
let result = step.read(cx).result.as_ref()?;
match result {
Ok(result) => Some(
v_flex()
.child(result.title.clone())
.children(result.suggestion_groups.iter().filter_map(
|(buffer, suggestion_groups)| {
let path = buffer.read(cx).file().map(|f| f.path());
v_flex()
.mb_2()
.border_b_1()
.children(path.map(|path| format!("path: {}", path.display())))
.children(suggestion_groups.iter().map(|group| {
v_flex().pl_2().children(group.suggestions.iter().map(
|suggestion| {
v_flex()
.children(
suggestion
.kind
.description()
.map(|desc| format!("description: {desc}")),
)
.child(format!("kind: {}", suggestion.kind.kind()))
.children(
suggestion.kind.symbol_path().map(|path| {
format!("symbol path: {}", path.0)
}),
)
},
))
}))
.into()
},
))
.into_any_element(),
),
Err(error) => Some(format!("{:?}", error).into_any_element()),
}
}
fn step_updated(&mut self, step: Model<WorkflowStepResolution>, cx: &mut ViewContext<Self>) {
self.tool_output_buffer.update(cx, |buffer, cx| {
let text = step.read(cx).output.clone();
buffer.set_text(text, cx);
});
cx.notify();
}
fn step_released(&mut self, _: &mut WorkflowStepResolution, cx: &mut ViewContext<Self>) {
cx.emit(EditorEvent::Closed);
}
fn resolve(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
self.step
.update(cx, |step, cx| {
step.resolve(cx);
})
.ok();
}
}
fn section_header(
name: &'static str,
cx: &mut editor::display_map::BlockContext,
) -> gpui::AnyElement {
h_flex()
.pl(cx.gutter_dimensions.full_width())
.h_11()
.w_full()
.relative()
.gap_1()
.child(
ButtonLike::new("role")
.style(ButtonStyle::Filled)
.child(Label::new(name).color(Color::Default)),
)
.into_any_element()
}
impl Render for WorkflowStepView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.key_context("ContextEditor")
.on_action(cx.listener(Self::resolve))
.flex_grow()
.bg(cx.theme().colors().editor_background)
.child(self.editor.clone())
}
}
impl EventEmitter<EditorEvent> for WorkflowStepView {}
impl FocusableView for WorkflowStepView {
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
self.editor.read(cx).focus_handle(cx)
}
}
impl Item for WorkflowStepView {
type Event = EditorEvent;
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
Some("workflow step".into())
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
match event {
EditorEvent::Edited { .. } => {
f(item::ItemEvent::Edit);
}
EditorEvent::TitleChanged => {
f(item::ItemEvent::UpdateTab);
}
EditorEvent::Closed => f(item::ItemEvent::CloseItem),
_ => {}
}
}
fn tab_tooltip_text(&self, _cx: &AppContext) -> Option<SharedString> {
None
}
fn as_searchable(&self, _handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
None
}
fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| {
Item::set_nav_history(editor, nav_history, cx)
})
}
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
self.editor
.update(cx, |editor, cx| Item::navigate(editor, data, cx))
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |editor, cx| Item::deactivated(editor, cx))
}
}

View File

@ -71,7 +71,7 @@ pub use language_registry::{
PendingLanguageServer, QUERY_FILENAME_PREFIXES,
};
pub use lsp::LanguageServerId;
pub use outline::{render_item, Outline, OutlineItem};
pub use outline::*;
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
pub use text::{AnchorRangeExt, LineEnding};
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};

View File

@ -25,6 +25,9 @@ pub struct OutlineItem<T> {
pub annotation_range: Option<Range<T>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SymbolPath(pub String);
impl<T: ToPoint> OutlineItem<T> {
/// Converts to an equivalent outline item, but with parameterized over Points.
pub fn to_point(&self, buffer: &BufferSnapshot) -> OutlineItem<Point> {
@ -85,7 +88,7 @@ impl<T> Outline<T> {
}
/// Find the most similar symbol to the provided query using normalized Levenshtein distance.
pub fn find_most_similar(&self, query: &str) -> Option<&OutlineItem<T>> {
pub fn find_most_similar(&self, query: &str) -> Option<(SymbolPath, &OutlineItem<T>)> {
const SIMILARITY_THRESHOLD: f64 = 0.6;
let (position, similarity) = self
@ -99,8 +102,10 @@ impl<T> Outline<T> {
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())?;
if similarity >= SIMILARITY_THRESHOLD {
let item = self.items.get(position)?;
Some(item)
self.path_candidates
.get(position)
.map(|candidate| SymbolPath(candidate.string.clone()))
.zip(self.items.get(position))
} else {
None
}
@ -250,15 +255,15 @@ mod tests {
]);
assert_eq!(
outline.find_most_similar("pub fn process"),
Some(&outline.items[0])
Some((SymbolPath("fn process".into()), &outline.items[0]))
);
assert_eq!(
outline.find_most_similar("async fn process"),
Some(&outline.items[0])
Some((SymbolPath("fn process".into()), &outline.items[0])),
);
assert_eq!(
outline.find_most_similar("struct Processor"),
Some(&outline.items[1])
Some((SymbolPath("struct DataProcessor".into()), &outline.items[1]))
);
assert_eq!(outline.find_most_similar("struct User"), None);
assert_eq!(outline.find_most_similar("struct"), None);