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}