1mod step_view;
2
3use crate::{
4 prompts::StepResolutionContext, AssistantPanel, Context, InlineAssistId, InlineAssistant,
5};
6use anyhow::{anyhow, Error, Result};
7use collections::HashMap;
8use editor::Editor;
9use futures::future;
10use gpui::{
11 AppContext, Model, ModelContext, Task, UpdateGlobal as _, View, WeakModel, WeakView,
12 WindowContext,
13};
14use language::{Anchor, Buffer, BufferSnapshot, SymbolPath};
15use language_model::{LanguageModelRegistry, LanguageModelRequestMessage, Role};
16use project::Project;
17use rope::Point;
18use serde::{Deserialize, Serialize};
19use smol::stream::StreamExt;
20use std::{cmp, fmt::Write, ops::Range, sync::Arc};
21use text::{AnchorRangeExt as _, OffsetRangeExt as _};
22use util::ResultExt as _;
23use workspace::Workspace;
24
25pub use step_view::WorkflowStepView;
26
27pub struct WorkflowStepResolution {
28 tagged_range: Range<Anchor>,
29 output: String,
30 context: WeakModel<Context>,
31 resolve_task: Option<Task<()>>,
32 pub result: Option<Result<ResolvedWorkflowStep, Arc<Error>>>,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct ResolvedWorkflowStep {
37 pub title: String,
38 pub suggestion_groups: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
39}
40
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct WorkflowSuggestionGroup {
43 pub context_range: Range<language::Anchor>,
44 pub suggestions: Vec<WorkflowSuggestion>,
45}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct WorkflowSuggestion {
49 pub kind: WorkflowSuggestionKind,
50 pub tool_input: String,
51 pub tool_output: tool::WorkflowSuggestionTool,
52}
53
54impl WorkflowSuggestion {
55 pub fn range(&self) -> Range<Anchor> {
56 self.kind.range()
57 }
58
59 pub fn show(
60 &self,
61 editor: &View<Editor>,
62 excerpt_id: editor::ExcerptId,
63 workspace: &WeakView<Workspace>,
64 assistant_panel: &View<AssistantPanel>,
65 cx: &mut ui::ViewContext<crate::assistant_panel::ContextEditor>,
66 ) -> Option<InlineAssistId> {
67 self.kind
68 .show(editor, excerpt_id, workspace, assistant_panel, cx)
69 }
70
71 fn try_merge(&mut self, other: &mut WorkflowSuggestion, snapshot: &BufferSnapshot) -> bool {
72 self.kind.try_merge(&other.kind, snapshot)
73 }
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub enum WorkflowSuggestionKind {
78 Update {
79 symbol_path: SymbolPath,
80 range: Range<language::Anchor>,
81 description: String,
82 },
83 CreateFile {
84 description: String,
85 },
86 InsertSiblingBefore {
87 symbol_path: SymbolPath,
88 position: language::Anchor,
89 description: String,
90 },
91 InsertSiblingAfter {
92 symbol_path: SymbolPath,
93 position: language::Anchor,
94 description: String,
95 },
96 PrependChild {
97 symbol_path: Option<SymbolPath>,
98 position: language::Anchor,
99 description: String,
100 },
101 AppendChild {
102 symbol_path: Option<SymbolPath>,
103 position: language::Anchor,
104 description: String,
105 },
106 Delete {
107 symbol_path: SymbolPath,
108 range: Range<language::Anchor>,
109 },
110}
111
112impl WorkflowStepResolution {
113 pub fn new(range: Range<Anchor>, context: WeakModel<Context>) -> Self {
114 Self {
115 tagged_range: range,
116 output: String::new(),
117 context,
118 result: None,
119 resolve_task: None,
120 }
121 }
122
123 pub fn step_text(&self, context: &Context, cx: &AppContext) -> String {
124 context
125 .buffer()
126 .clone()
127 .read(cx)
128 .text_for_range(self.tagged_range.clone())
129 .collect::<String>()
130 }
131
132 pub fn resolve(&mut self, cx: &mut ModelContext<WorkflowStepResolution>) -> Option<()> {
133 let range = self.tagged_range.clone();
134 let context = self.context.upgrade()?;
135 let context = context.read(cx);
136 let project = context.project()?;
137 let prompt_builder = context.prompt_builder();
138 let mut request = context.to_completion_request(cx);
139 let model = LanguageModelRegistry::read_global(cx).active_model();
140 let context_buffer = context.buffer();
141 let step_text = context_buffer
142 .read(cx)
143 .text_for_range(range.clone())
144 .collect::<String>();
145
146 let mut workflow_context = String::new();
147 for message in context.messages(cx) {
148 write!(&mut workflow_context, "<message role={}>", message.role).unwrap();
149 for chunk in context_buffer.read(cx).text_for_range(message.offset_range) {
150 write!(&mut workflow_context, "{chunk}").unwrap();
151 }
152 write!(&mut workflow_context, "</message>").unwrap();
153 }
154
155 self.resolve_task = Some(cx.spawn(|this, mut cx| async move {
156 let result = async {
157 let Some(model) = model else {
158 return Err(anyhow!("no model selected"));
159 };
160
161 this.update(&mut cx, |this, cx| {
162 this.output.clear();
163 this.result = None;
164 this.result_updated(cx);
165 cx.notify();
166 })?;
167
168 let resolution_context = StepResolutionContext {
169 workflow_context,
170 step_to_resolve: step_text.clone(),
171 };
172 let mut prompt =
173 prompt_builder.generate_step_resolution_prompt(&resolution_context)?;
174 prompt.push_str(&step_text);
175 request.messages.push(LanguageModelRequestMessage {
176 role: Role::User,
177 content: vec![prompt.into()],
178 });
179
180 // Invoke the model to get its edit suggestions for this workflow step.
181 let mut stream = model
182 .use_tool_stream::<tool::WorkflowStepResolutionTool>(request, &cx)
183 .await?;
184 while let Some(chunk) = stream.next().await {
185 let chunk = chunk?;
186 this.update(&mut cx, |this, cx| {
187 this.output.push_str(&chunk);
188 cx.notify();
189 })?;
190 }
191
192 let resolution = this.update(&mut cx, |this, _| {
193 serde_json::from_str::<tool::WorkflowStepResolutionTool>(&this.output)
194 })??;
195
196 this.update(&mut cx, |this, cx| {
197 this.output = serde_json::to_string_pretty(&resolution).unwrap();
198 cx.notify();
199 })?;
200
201 // Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
202 let suggestion_tasks: Vec<_> = resolution
203 .suggestions
204 .iter()
205 .map(|suggestion| {
206 suggestion.resolve(step_text.clone(), project.clone(), cx.clone())
207 })
208 .collect();
209
210 // Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
211 let suggestions = future::join_all(suggestion_tasks)
212 .await
213 .into_iter()
214 .filter_map(|task| task.log_err())
215 .collect::<Vec<_>>();
216
217 let mut suggestions_by_buffer = HashMap::default();
218 for (buffer, suggestion) in suggestions {
219 suggestions_by_buffer
220 .entry(buffer)
221 .or_insert_with(Vec::new)
222 .push(suggestion);
223 }
224
225 let mut suggestion_groups_by_buffer = HashMap::default();
226 for (buffer, mut suggestions) in suggestions_by_buffer {
227 let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
228 let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
229 // Sort suggestions by their range so that earlier, larger ranges come first
230 suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
231
232 // Merge overlapping suggestions
233 suggestions.dedup_by(|a, b| b.try_merge(a, &snapshot));
234
235 // Create context ranges for each suggestion
236 for suggestion in suggestions {
237 let context_range = {
238 let suggestion_point_range = suggestion.range().to_point(&snapshot);
239 let start_row = suggestion_point_range.start.row.saturating_sub(5);
240 let end_row = cmp::min(
241 suggestion_point_range.end.row + 5,
242 snapshot.max_point().row,
243 );
244 let start = snapshot.anchor_before(Point::new(start_row, 0));
245 let end = snapshot
246 .anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
247 start..end
248 };
249
250 if let Some(last_group) = suggestion_groups.last_mut() {
251 if last_group
252 .context_range
253 .end
254 .cmp(&context_range.start, &snapshot)
255 .is_ge()
256 {
257 // Merge with the previous group if context ranges overlap
258 last_group.context_range.end = context_range.end;
259 last_group.suggestions.push(suggestion);
260 } else {
261 // Create a new group
262 suggestion_groups.push(WorkflowSuggestionGroup {
263 context_range,
264 suggestions: vec![suggestion],
265 });
266 }
267 } else {
268 // Create the first group
269 suggestion_groups.push(WorkflowSuggestionGroup {
270 context_range,
271 suggestions: vec![suggestion],
272 });
273 }
274 }
275
276 suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
277 }
278
279 Ok((resolution.step_title, suggestion_groups_by_buffer))
280 };
281
282 let result = result.await;
283 this.update(&mut cx, |this, cx| {
284 this.result = Some(match result {
285 Ok((title, suggestion_groups)) => Ok(ResolvedWorkflowStep {
286 title,
287 suggestion_groups,
288 }),
289 Err(error) => Err(Arc::new(error)),
290 });
291 this.context
292 .update(cx, |context, cx| context.workflow_step_updated(range, cx))
293 .ok();
294 cx.notify();
295 })
296 .ok();
297 }));
298 None
299 }
300
301 fn result_updated(&mut self, cx: &mut ModelContext<Self>) {
302 self.context
303 .update(cx, |context, cx| {
304 context.workflow_step_updated(self.tagged_range.clone(), cx)
305 })
306 .ok();
307 }
308}
309
310impl WorkflowSuggestionKind {
311 pub fn range(&self) -> Range<language::Anchor> {
312 match self {
313 Self::Update { range, .. } => range.clone(),
314 Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
315 Self::InsertSiblingBefore { position, .. }
316 | Self::InsertSiblingAfter { position, .. }
317 | Self::PrependChild { position, .. }
318 | Self::AppendChild { position, .. } => *position..*position,
319 Self::Delete { range, .. } => range.clone(),
320 }
321 }
322
323 pub fn description(&self) -> Option<&str> {
324 match self {
325 Self::Update { description, .. }
326 | Self::CreateFile { description }
327 | Self::InsertSiblingBefore { description, .. }
328 | Self::InsertSiblingAfter { description, .. }
329 | Self::PrependChild { description, .. }
330 | Self::AppendChild { description, .. } => Some(description),
331 Self::Delete { .. } => None,
332 }
333 }
334
335 fn description_mut(&mut self) -> Option<&mut String> {
336 match self {
337 Self::Update { description, .. }
338 | Self::CreateFile { description }
339 | Self::InsertSiblingBefore { description, .. }
340 | Self::InsertSiblingAfter { description, .. }
341 | Self::PrependChild { description, .. }
342 | Self::AppendChild { description, .. } => Some(description),
343 Self::Delete { .. } => None,
344 }
345 }
346
347 fn symbol_path(&self) -> Option<&SymbolPath> {
348 match self {
349 Self::Update { symbol_path, .. } => Some(symbol_path),
350 Self::InsertSiblingBefore { symbol_path, .. } => Some(symbol_path),
351 Self::InsertSiblingAfter { symbol_path, .. } => Some(symbol_path),
352 Self::PrependChild { symbol_path, .. } => symbol_path.as_ref(),
353 Self::AppendChild { symbol_path, .. } => symbol_path.as_ref(),
354 Self::Delete { symbol_path, .. } => Some(symbol_path),
355 Self::CreateFile { .. } => None,
356 }
357 }
358
359 fn kind(&self) -> &str {
360 match self {
361 Self::Update { .. } => "Update",
362 Self::CreateFile { .. } => "CreateFile",
363 Self::InsertSiblingBefore { .. } => "InsertSiblingBefore",
364 Self::InsertSiblingAfter { .. } => "InsertSiblingAfter",
365 Self::PrependChild { .. } => "PrependChild",
366 Self::AppendChild { .. } => "AppendChild",
367 Self::Delete { .. } => "Delete",
368 }
369 }
370
371 fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
372 let range = self.range();
373 let other_range = other.range();
374
375 // Don't merge if we don't contain the other suggestion.
376 if range.start.cmp(&other_range.start, buffer).is_gt()
377 || range.end.cmp(&other_range.end, buffer).is_lt()
378 {
379 return false;
380 }
381
382 if let Some(description) = self.description_mut() {
383 if let Some(other_description) = other.description() {
384 description.push('\n');
385 description.push_str(other_description);
386 }
387 }
388 true
389 }
390
391 pub fn show(
392 &self,
393 editor: &View<Editor>,
394 excerpt_id: editor::ExcerptId,
395 workspace: &WeakView<Workspace>,
396 assistant_panel: &View<AssistantPanel>,
397 cx: &mut WindowContext,
398 ) -> Option<InlineAssistId> {
399 let mut initial_transaction_id = None;
400 let initial_prompt;
401 let suggestion_range;
402 let buffer = editor.read(cx).buffer().clone();
403 let snapshot = buffer.read(cx).snapshot(cx);
404
405 match self {
406 Self::Update {
407 range, description, ..
408 } => {
409 initial_prompt = description.clone();
410 suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
411 ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
412 }
413 Self::CreateFile { description } => {
414 initial_prompt = description.clone();
415 suggestion_range = editor::Anchor::min()..editor::Anchor::min();
416 }
417 Self::InsertSiblingBefore {
418 position,
419 description,
420 ..
421 } => {
422 let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
423 initial_prompt = description.clone();
424 suggestion_range = buffer.update(cx, |buffer, cx| {
425 buffer.start_transaction(cx);
426 let line_start = buffer.insert_empty_line(position, true, true, cx);
427 initial_transaction_id = buffer.end_transaction(cx);
428 buffer.refresh_preview(cx);
429
430 let line_start = buffer.read(cx).anchor_before(line_start);
431 line_start..line_start
432 });
433 }
434 Self::InsertSiblingAfter {
435 position,
436 description,
437 ..
438 } => {
439 let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
440 initial_prompt = description.clone();
441 suggestion_range = buffer.update(cx, |buffer, cx| {
442 buffer.start_transaction(cx);
443 let line_start = buffer.insert_empty_line(position, true, true, cx);
444 initial_transaction_id = buffer.end_transaction(cx);
445 buffer.refresh_preview(cx);
446
447 let line_start = buffer.read(cx).anchor_before(line_start);
448 line_start..line_start
449 });
450 }
451 Self::PrependChild {
452 position,
453 description,
454 ..
455 } => {
456 let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
457 initial_prompt = description.clone();
458 suggestion_range = buffer.update(cx, |buffer, cx| {
459 buffer.start_transaction(cx);
460 let line_start = buffer.insert_empty_line(position, false, true, cx);
461 initial_transaction_id = buffer.end_transaction(cx);
462 buffer.refresh_preview(cx);
463
464 let line_start = buffer.read(cx).anchor_before(line_start);
465 line_start..line_start
466 });
467 }
468 Self::AppendChild {
469 position,
470 description,
471 ..
472 } => {
473 let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
474 initial_prompt = description.clone();
475 suggestion_range = buffer.update(cx, |buffer, cx| {
476 buffer.start_transaction(cx);
477 let line_start = buffer.insert_empty_line(position, true, false, cx);
478 initial_transaction_id = buffer.end_transaction(cx);
479 buffer.refresh_preview(cx);
480
481 let line_start = buffer.read(cx).anchor_before(line_start);
482 line_start..line_start
483 });
484 }
485 Self::Delete { range, .. } => {
486 initial_prompt = "Delete".to_string();
487 suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
488 ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
489 }
490 }
491
492 InlineAssistant::update_global(cx, |inline_assistant, cx| {
493 Some(inline_assistant.suggest_assist(
494 editor,
495 suggestion_range,
496 initial_prompt,
497 initial_transaction_id,
498 Some(workspace.clone()),
499 Some(assistant_panel),
500 cx,
501 ))
502 })
503 }
504}
505
506pub mod tool {
507 use std::path::Path;
508
509 use super::*;
510 use anyhow::Context as _;
511 use gpui::AsyncAppContext;
512 use language::ParseStatus;
513 use language_model::LanguageModelTool;
514 use project::ProjectPath;
515 use schemars::JsonSchema;
516
517 #[derive(Debug, Serialize, Deserialize, JsonSchema)]
518 pub struct WorkflowStepResolutionTool {
519 /// An extremely short title for the edit step represented by these operations.
520 pub step_title: String,
521 /// A sequence of operations to apply to the codebase.
522 /// When multiple operations are required for a step, be sure to include multiple operations in this list.
523 pub suggestions: Vec<WorkflowSuggestionTool>,
524 }
525
526 impl LanguageModelTool for WorkflowStepResolutionTool {
527 fn name() -> String {
528 "edit".into()
529 }
530
531 fn description() -> String {
532 "suggest edits to one or more locations in the codebase".into()
533 }
534 }
535
536 /// A description of an operation to apply to one location in the codebase.
537 ///
538 /// This object represents a single edit operation that can be performed on a specific file
539 /// in the codebase. It encapsulates both the location (file path) and the nature of the
540 /// edit to be made.
541 ///
542 /// # Fields
543 ///
544 /// * `path`: A string representing the file path where the edit operation should be applied.
545 /// This path is relative to the root of the project or repository.
546 ///
547 /// * `kind`: An enum representing the specific type of edit operation to be performed.
548 ///
549 /// # Usage
550 ///
551 /// `EditOperation` is used within a code editor to represent and apply
552 /// programmatic changes to source code. It provides a structured way to describe
553 /// edits for features like refactoring tools or AI-assisted coding suggestions.
554 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
555 pub struct WorkflowSuggestionTool {
556 /// The path to the file containing the relevant operation
557 pub path: String,
558 #[serde(flatten)]
559 pub kind: WorkflowSuggestionToolKind,
560 }
561
562 impl WorkflowSuggestionTool {
563 pub(super) async fn resolve(
564 &self,
565 tool_input: String,
566 project: Model<Project>,
567 mut cx: AsyncAppContext,
568 ) -> Result<(Model<Buffer>, super::WorkflowSuggestion)> {
569 let path = self.path.clone();
570 let kind = self.kind.clone();
571 let buffer = project
572 .update(&mut cx, |project, cx| {
573 let project_path = project
574 .find_project_path(Path::new(&path), cx)
575 .or_else(|| {
576 // If we couldn't find a project path for it, put it in the active worktree
577 // so that when we create the buffer, it can be saved.
578 let worktree = project
579 .active_entry()
580 .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
581 .or_else(|| project.worktrees(cx).next())?;
582 let worktree = worktree.read(cx);
583
584 Some(ProjectPath {
585 worktree_id: worktree.id(),
586 path: Arc::from(Path::new(&path)),
587 })
588 })
589 .with_context(|| format!("worktree not found for {:?}", path))?;
590 anyhow::Ok(project.open_buffer(project_path, cx))
591 })??
592 .await?;
593
594 let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?;
595 while *parse_status.borrow() != ParseStatus::Idle {
596 parse_status.changed().await?;
597 }
598
599 let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
600 let outline = snapshot.outline(None).context("no outline for buffer")?;
601
602 let kind = match kind {
603 WorkflowSuggestionToolKind::Update {
604 symbol,
605 description,
606 } => {
607 let (symbol_path, symbol) = outline
608 .find_most_similar(&symbol)
609 .with_context(|| format!("symbol not found: {:?}", symbol))?;
610 let symbol = symbol.to_point(&snapshot);
611 let start = symbol
612 .annotation_range
613 .map_or(symbol.range.start, |range| range.start);
614 let start = Point::new(start.row, 0);
615 let end = Point::new(
616 symbol.range.end.row,
617 snapshot.line_len(symbol.range.end.row),
618 );
619 let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
620 WorkflowSuggestionKind::Update {
621 range,
622 description,
623 symbol_path,
624 }
625 }
626 WorkflowSuggestionToolKind::Create { description } => {
627 WorkflowSuggestionKind::CreateFile { description }
628 }
629 WorkflowSuggestionToolKind::InsertSiblingBefore {
630 symbol,
631 description,
632 } => {
633 let (symbol_path, symbol) = outline
634 .find_most_similar(&symbol)
635 .with_context(|| format!("symbol not found: {:?}", symbol))?;
636 let symbol = symbol.to_point(&snapshot);
637 let position = snapshot.anchor_before(
638 symbol
639 .annotation_range
640 .map_or(symbol.range.start, |annotation_range| {
641 annotation_range.start
642 }),
643 );
644 WorkflowSuggestionKind::InsertSiblingBefore {
645 position,
646 description,
647 symbol_path,
648 }
649 }
650 WorkflowSuggestionToolKind::InsertSiblingAfter {
651 symbol,
652 description,
653 } => {
654 let (symbol_path, symbol) = outline
655 .find_most_similar(&symbol)
656 .with_context(|| format!("symbol not found: {:?}", symbol))?;
657 let symbol = symbol.to_point(&snapshot);
658 let position = snapshot.anchor_after(symbol.range.end);
659 WorkflowSuggestionKind::InsertSiblingAfter {
660 position,
661 description,
662 symbol_path,
663 }
664 }
665 WorkflowSuggestionToolKind::PrependChild {
666 symbol,
667 description,
668 } => {
669 if let Some(symbol) = symbol {
670 let (symbol_path, symbol) = outline
671 .find_most_similar(&symbol)
672 .with_context(|| format!("symbol not found: {:?}", symbol))?;
673 let symbol = symbol.to_point(&snapshot);
674
675 let position = snapshot.anchor_after(
676 symbol
677 .body_range
678 .map_or(symbol.range.start, |body_range| body_range.start),
679 );
680 WorkflowSuggestionKind::PrependChild {
681 position,
682 description,
683 symbol_path: Some(symbol_path),
684 }
685 } else {
686 WorkflowSuggestionKind::PrependChild {
687 position: language::Anchor::MIN,
688 description,
689 symbol_path: None,
690 }
691 }
692 }
693 WorkflowSuggestionToolKind::AppendChild {
694 symbol,
695 description,
696 } => {
697 if let Some(symbol) = symbol {
698 let (symbol_path, symbol) = outline
699 .find_most_similar(&symbol)
700 .with_context(|| format!("symbol not found: {:?}", symbol))?;
701 let symbol = symbol.to_point(&snapshot);
702
703 let position = snapshot.anchor_before(
704 symbol
705 .body_range
706 .map_or(symbol.range.end, |body_range| body_range.end),
707 );
708 WorkflowSuggestionKind::AppendChild {
709 position,
710 description,
711 symbol_path: Some(symbol_path),
712 }
713 } else {
714 WorkflowSuggestionKind::PrependChild {
715 position: language::Anchor::MAX,
716 description,
717 symbol_path: None,
718 }
719 }
720 }
721 WorkflowSuggestionToolKind::Delete { symbol } => {
722 let (symbol_path, symbol) = outline
723 .find_most_similar(&symbol)
724 .with_context(|| format!("symbol not found: {:?}", symbol))?;
725 let symbol = symbol.to_point(&snapshot);
726 let start = symbol
727 .annotation_range
728 .map_or(symbol.range.start, |range| range.start);
729 let start = Point::new(start.row, 0);
730 let end = Point::new(
731 symbol.range.end.row,
732 snapshot.line_len(symbol.range.end.row),
733 );
734 let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
735 WorkflowSuggestionKind::Delete { range, symbol_path }
736 }
737 };
738
739 let suggestion = WorkflowSuggestion {
740 kind,
741 tool_output: self.clone(),
742 tool_input,
743 };
744
745 Ok((buffer, suggestion))
746 }
747 }
748
749 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
750 #[serde(tag = "kind")]
751 pub enum WorkflowSuggestionToolKind {
752 /// Rewrites the specified symbol entirely based on the given description.
753 /// This operation completely replaces the existing symbol with new content.
754 Update {
755 /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
756 /// The path should uniquely identify the symbol within the containing file.
757 symbol: String,
758 /// A brief description of the transformation to apply to the symbol.
759 description: String,
760 },
761 /// Creates a new file with the given path based on the provided description.
762 /// This operation adds a new file to the codebase.
763 Create {
764 /// A brief description of the file to be created.
765 description: String,
766 },
767 /// Inserts a new symbol based on the given description before the specified symbol.
768 /// This operation adds new content immediately preceding an existing symbol.
769 InsertSiblingBefore {
770 /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
771 /// The new content will be inserted immediately before this symbol.
772 symbol: String,
773 /// A brief description of the new symbol to be inserted.
774 description: String,
775 },
776 /// Inserts a new symbol based on the given description after the specified symbol.
777 /// This operation adds new content immediately following an existing symbol.
778 InsertSiblingAfter {
779 /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
780 /// The new content will be inserted immediately after this symbol.
781 symbol: String,
782 /// A brief description of the new symbol to be inserted.
783 description: String,
784 },
785 /// Inserts a new symbol as a child of the specified symbol at the start.
786 /// This operation adds new content as the first child of an existing symbol (or file if no symbol is provided).
787 PrependChild {
788 /// An optional fully-qualified reference to the symbol after the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
789 /// If provided, the new content will be inserted as the first child of this symbol.
790 /// If not provided, the new content will be inserted at the top of the file.
791 symbol: Option<String>,
792 /// A brief description of the new symbol to be inserted.
793 description: String,
794 },
795 /// Inserts a new symbol as a child of the specified symbol at the end.
796 /// This operation adds new content as the last child of an existing symbol (or file if no symbol is provided).
797 AppendChild {
798 /// An optional fully-qualified reference to the symbol before the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
799 /// If provided, the new content will be inserted as the last child of this symbol.
800 /// If not provided, the new content will be applied at the bottom of the file.
801 symbol: Option<String>,
802 /// A brief description of the new symbol to be inserted.
803 description: String,
804 },
805 /// Deletes the specified symbol from the containing file.
806 Delete {
807 /// An fully-qualified reference to the symbol to be deleted, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
808 symbol: String,
809 },
810 }
811}