Detailed changes
@@ -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)
- };
+ } 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 {
@@ -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,
@@ -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!(
@@ -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 }
}
};
@@ -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))
+ }
+}
@@ -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};
@@ -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);