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