diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index ef840aa1c8..fef2fb288c 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -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::() )); 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, + cx: &mut ViewContext, + ) -> 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) { 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, ) -> Option { 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 { diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index f144f6c96a..d30ec47a65 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -838,6 +838,10 @@ impl Context { &self.buffer } + pub fn language_registry(&self) -> Arc { + self.language_registry.clone() + } + pub fn project(&self) -> Option> { self.project.clone() } @@ -1073,6 +1077,7 @@ impl Context { } fn parse_workflow_steps_in_range(&mut self, range: Range, cx: &mut ModelContext) { + 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, + cx: &mut ModelContext, + ) { + cx.emit(ContextEvent::WorkflowStepUpdated(range)); + cx.notify(); + } + pub fn pending_command_for_position( &mut self, position: language::Anchor, diff --git a/crates/assistant/src/context_inspector.rs b/crates/assistant/src/context_inspector.rs index eb79557955..ed1c22f1cd 100644 --- a/crates/assistant/src/context_inspector.rs +++ b/crates/assistant/src/context_inspector.rs @@ -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!( diff --git a/crates/assistant/src/workflow.rs b/crates/assistant/src/workflow.rs index ecb0545c4d..55a85bb718 100644 --- a/crates/assistant/src/workflow.rs +++ b/crates/assistant/src/workflow.rs @@ -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, output: String, + context: WeakModel, + resolve_task: Option>, pub result: Option>>, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ResolvedWorkflowStep { pub title: String, - pub suggestions: HashMap, Vec>, + pub suggestion_groups: HashMap, Vec>, } #[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, 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, position: language::Anchor, description: String, }, AppendChild { + symbol_path: Option, position: language::Anchor, description: String, }, Delete { + symbol_path: SymbolPath, range: Range, }, } impl WorkflowStepResolution { - pub fn new(range: Range) -> Self { + pub fn new(range: Range, context: WeakModel) -> Self { Self { tagged_range: range, output: String::new(), + context, result: None, + resolve_task: None, } } - pub fn resolve( - &mut self, - context: &Context, - cx: &mut ModelContext, - ) -> Option> { + pub fn step_text(&self, context: &Context, cx: &AppContext) -> String { + context + .buffer() + .clone() + .read(cx) + .text_for_range(self.tagged_range.clone()) + .collect::() + } + + pub fn resolve(&mut self, cx: &mut ModelContext) -> 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::(); let mut workflow_context = String::new(); @@ -127,7 +152,7 @@ impl WorkflowStepResolution { write!(&mut workflow_context, "").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::(&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.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 } } }; diff --git a/crates/assistant/src/workflow/step_view.rs b/crates/assistant/src/workflow/step_view.rs new file mode 100644 index 0000000000..a1b3bd7ee2 --- /dev/null +++ b/crates/assistant/src/workflow/step_view.rs @@ -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, + tool_output_buffer: Model, + editor: View, +} + +impl WorkflowStepView { + pub fn new( + context: Model, + step: Model, + language_registry: Arc, + cx: &mut ViewContext, + ) -> 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) -> Option { + 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, cx: &mut ViewContext) { + 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) { + cx.emit(EditorEvent::Closed); + } + + fn resolve(&mut self, _: &Assist, cx: &mut ViewContext) { + 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) -> 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 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 { + 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 { + None + } + + fn as_searchable(&self, _handle: &View) -> Option> { + None + } + + fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + Item::set_nav_history(editor, nav_history, cx) + }) + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| Item::navigate(editor, data, cx)) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, cx)) + } +} diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 725180c211..ddb65fd594 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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}; diff --git a/crates/language/src/outline.rs b/crates/language/src/outline.rs index 6819e5c807..3d89c52fc0 100644 --- a/crates/language/src/outline.rs +++ b/crates/language/src/outline.rs @@ -25,6 +25,9 @@ pub struct OutlineItem { pub annotation_range: Option>, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SymbolPath(pub String); + impl OutlineItem { /// Converts to an equivalent outline item, but with parameterized over Points. pub fn to_point(&self, buffer: &BufferSnapshot) -> OutlineItem { @@ -85,7 +88,7 @@ impl Outline { } /// Find the most similar symbol to the provided query using normalized Levenshtein distance. - pub fn find_most_similar(&self, query: &str) -> Option<&OutlineItem> { + pub fn find_most_similar(&self, query: &str) -> Option<(SymbolPath, &OutlineItem)> { const SIMILARITY_THRESHOLD: f64 = 0.6; let (position, similarity) = self @@ -99,8 +102,10 @@ impl Outline { .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);