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