workflow.rs

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