1use crate::{AssistantPanel, InlineAssistId, InlineAssistant};
2use anyhow::{anyhow, Context as _, Result};
3use collections::HashMap;
4use editor::Editor;
5use gpui::AsyncAppContext;
6use gpui::{Model, Task, UpdateGlobal as _, View, WeakView, WindowContext};
7use language::{Anchor, Buffer, BufferSnapshot, Outline, OutlineItem, ParseStatus, SymbolPath};
8use project::{Project, ProjectPath};
9use rope::Point;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use std::{ops::Range, path::Path, sync::Arc};
13use workspace::Workspace;
14
15const IMPORTS_SYMBOL: &str = "#imports";
16
17#[derive(Debug)]
18pub(crate) struct WorkflowStep {
19 pub range: Range<language::Anchor>,
20 pub leading_tags_end: text::Anchor,
21 pub trailing_tag_start: Option<text::Anchor>,
22 pub edits: Arc<[Result<WorkflowStepEdit>]>,
23 pub resolution_task: Option<Task<()>>,
24 pub resolution: Option<Arc<Result<WorkflowStepResolution>>>,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub(crate) struct WorkflowStepEdit {
29 pub path: String,
30 pub kind: WorkflowStepEditKind,
31}
32
33#[derive(Clone, Debug, Eq, PartialEq)]
34pub(crate) struct WorkflowStepResolution {
35 pub title: String,
36 pub suggestion_groups: HashMap<Model<Buffer>, Vec<WorkflowSuggestionGroup>>,
37}
38
39#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct WorkflowSuggestionGroup {
41 pub context_range: Range<language::Anchor>,
42 pub suggestions: Vec<WorkflowSuggestion>,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub enum WorkflowSuggestion {
47 Update {
48 symbol_path: SymbolPath,
49 range: Range<language::Anchor>,
50 description: String,
51 },
52 CreateFile {
53 description: String,
54 },
55 InsertSiblingBefore {
56 symbol_path: SymbolPath,
57 position: language::Anchor,
58 description: String,
59 },
60 InsertSiblingAfter {
61 symbol_path: SymbolPath,
62 position: language::Anchor,
63 description: String,
64 },
65 PrependChild {
66 symbol_path: Option<SymbolPath>,
67 position: language::Anchor,
68 description: String,
69 },
70 AppendChild {
71 symbol_path: Option<SymbolPath>,
72 position: language::Anchor,
73 description: String,
74 },
75 Delete {
76 symbol_path: SymbolPath,
77 range: Range<language::Anchor>,
78 },
79}
80
81impl WorkflowSuggestion {
82 pub fn range(&self) -> Range<language::Anchor> {
83 match self {
84 Self::Update { range, .. } => range.clone(),
85 Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
86 Self::InsertSiblingBefore { position, .. }
87 | Self::InsertSiblingAfter { position, .. }
88 | Self::PrependChild { position, .. }
89 | Self::AppendChild { position, .. } => *position..*position,
90 Self::Delete { range, .. } => range.clone(),
91 }
92 }
93
94 pub fn description(&self) -> Option<&str> {
95 match self {
96 Self::Update { description, .. }
97 | Self::CreateFile { description }
98 | Self::InsertSiblingBefore { description, .. }
99 | Self::InsertSiblingAfter { description, .. }
100 | Self::PrependChild { description, .. }
101 | Self::AppendChild { description, .. } => Some(description),
102 Self::Delete { .. } => None,
103 }
104 }
105
106 fn description_mut(&mut self) -> Option<&mut String> {
107 match self {
108 Self::Update { description, .. }
109 | Self::CreateFile { description }
110 | Self::InsertSiblingBefore { description, .. }
111 | Self::InsertSiblingAfter { description, .. }
112 | Self::PrependChild { description, .. }
113 | Self::AppendChild { description, .. } => Some(description),
114 Self::Delete { .. } => None,
115 }
116 }
117
118 pub fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool {
119 let range = self.range();
120 let other_range = other.range();
121
122 // Don't merge if we don't contain the other suggestion.
123 if range.start.cmp(&other_range.start, buffer).is_gt()
124 || range.end.cmp(&other_range.end, buffer).is_lt()
125 {
126 return false;
127 }
128
129 if let Some(description) = self.description_mut() {
130 if let Some(other_description) = other.description() {
131 description.push('\n');
132 description.push_str(other_description);
133 }
134 }
135 true
136 }
137
138 pub fn show(
139 &self,
140 editor: &View<Editor>,
141 excerpt_id: editor::ExcerptId,
142 workspace: &WeakView<Workspace>,
143 assistant_panel: &View<AssistantPanel>,
144 cx: &mut WindowContext,
145 ) -> Option<InlineAssistId> {
146 let mut initial_transaction_id = None;
147 let initial_prompt;
148 let suggestion_range;
149 let buffer = editor.read(cx).buffer().clone();
150 let snapshot = buffer.read(cx).snapshot(cx);
151
152 match self {
153 Self::Update {
154 range, description, ..
155 } => {
156 initial_prompt = description.clone();
157 suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
158 ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
159 }
160 Self::CreateFile { description } => {
161 initial_prompt = description.clone();
162 suggestion_range = editor::Anchor::min()..editor::Anchor::min();
163 }
164 Self::InsertSiblingBefore {
165 position,
166 description,
167 ..
168 } => {
169 let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
170 initial_prompt = description.clone();
171 suggestion_range = buffer.update(cx, |buffer, cx| {
172 buffer.start_transaction(cx);
173 let line_start = buffer.insert_empty_line(position, true, true, cx);
174 initial_transaction_id = buffer.end_transaction(cx);
175 buffer.refresh_preview(cx);
176
177 let line_start = buffer.read(cx).anchor_before(line_start);
178 line_start..line_start
179 });
180 }
181 Self::InsertSiblingAfter {
182 position,
183 description,
184 ..
185 } => {
186 let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
187 initial_prompt = description.clone();
188 suggestion_range = buffer.update(cx, |buffer, cx| {
189 buffer.start_transaction(cx);
190 let line_start = buffer.insert_empty_line(position, true, true, cx);
191 initial_transaction_id = buffer.end_transaction(cx);
192 buffer.refresh_preview(cx);
193
194 let line_start = buffer.read(cx).anchor_before(line_start);
195 line_start..line_start
196 });
197 }
198 Self::PrependChild {
199 position,
200 description,
201 ..
202 } => {
203 let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
204 initial_prompt = description.clone();
205 suggestion_range = buffer.update(cx, |buffer, cx| {
206 buffer.start_transaction(cx);
207 let line_start = buffer.insert_empty_line(position, false, true, cx);
208 initial_transaction_id = buffer.end_transaction(cx);
209 buffer.refresh_preview(cx);
210
211 let line_start = buffer.read(cx).anchor_before(line_start);
212 line_start..line_start
213 });
214 }
215 Self::AppendChild {
216 position,
217 description,
218 ..
219 } => {
220 let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?;
221 initial_prompt = description.clone();
222 suggestion_range = buffer.update(cx, |buffer, cx| {
223 buffer.start_transaction(cx);
224 let line_start = buffer.insert_empty_line(position, true, false, cx);
225 initial_transaction_id = buffer.end_transaction(cx);
226 buffer.refresh_preview(cx);
227
228 let line_start = buffer.read(cx).anchor_before(line_start);
229 line_start..line_start
230 });
231 }
232 Self::Delete { range, .. } => {
233 initial_prompt = "Delete".to_string();
234 suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
235 ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
236 }
237 }
238
239 InlineAssistant::update_global(cx, |inline_assistant, cx| {
240 Some(inline_assistant.suggest_assist(
241 editor,
242 suggestion_range,
243 initial_prompt,
244 initial_transaction_id,
245 Some(workspace.clone()),
246 Some(assistant_panel),
247 cx,
248 ))
249 })
250 }
251}
252
253impl WorkflowStepEdit {
254 pub fn new(
255 path: Option<String>,
256 operation: Option<String>,
257 symbol: Option<String>,
258 description: Option<String>,
259 ) -> Result<Self> {
260 let path = path.ok_or_else(|| anyhow!("missing path"))?;
261 let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
262
263 let kind = match operation.as_str() {
264 "update" => WorkflowStepEditKind::Update {
265 symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?,
266 description: description.ok_or_else(|| anyhow!("missing description"))?,
267 },
268 "insert_sibling_before" => WorkflowStepEditKind::InsertSiblingBefore {
269 symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?,
270 description: description.ok_or_else(|| anyhow!("missing description"))?,
271 },
272 "insert_sibling_after" => WorkflowStepEditKind::InsertSiblingAfter {
273 symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?,
274 description: description.ok_or_else(|| anyhow!("missing description"))?,
275 },
276 "prepend_child" => WorkflowStepEditKind::PrependChild {
277 symbol,
278 description: description.ok_or_else(|| anyhow!("missing description"))?,
279 },
280 "append_child" => WorkflowStepEditKind::AppendChild {
281 symbol,
282 description: description.ok_or_else(|| anyhow!("missing description"))?,
283 },
284 "delete" => WorkflowStepEditKind::Delete {
285 symbol: symbol.ok_or_else(|| anyhow!("missing symbol"))?,
286 },
287 "create" => WorkflowStepEditKind::Create {
288 description: description.ok_or_else(|| anyhow!("missing description"))?,
289 },
290 _ => Err(anyhow!("unknown operation {operation:?}"))?,
291 };
292
293 Ok(Self { path, kind })
294 }
295
296 pub async fn resolve(
297 &self,
298 project: Model<Project>,
299 mut cx: AsyncAppContext,
300 ) -> Result<(Model<Buffer>, super::WorkflowSuggestion)> {
301 let path = self.path.clone();
302 let kind = self.kind.clone();
303 let buffer = project
304 .update(&mut cx, |project, cx| {
305 let project_path = project
306 .find_project_path(Path::new(&path), cx)
307 .or_else(|| {
308 // If we couldn't find a project path for it, put it in the active worktree
309 // so that when we create the buffer, it can be saved.
310 let worktree = project
311 .active_entry()
312 .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
313 .or_else(|| project.worktrees(cx).next())?;
314 let worktree = worktree.read(cx);
315
316 Some(ProjectPath {
317 worktree_id: worktree.id(),
318 path: Arc::from(Path::new(&path)),
319 })
320 })
321 .with_context(|| format!("worktree not found for {:?}", path))?;
322 anyhow::Ok(project.open_buffer(project_path, cx))
323 })??
324 .await?;
325
326 let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?;
327 while *parse_status.borrow() != ParseStatus::Idle {
328 parse_status.changed().await?;
329 }
330
331 let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
332 let outline = snapshot.outline(None).context("no outline for buffer")?;
333
334 let suggestion = match kind {
335 WorkflowStepEditKind::Update {
336 symbol,
337 description,
338 } => {
339 let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
340 let start = symbol
341 .annotation_range
342 .map_or(symbol.range.start, |range| range.start);
343 let start = Point::new(start.row, 0);
344 let end = Point::new(
345 symbol.range.end.row,
346 snapshot.line_len(symbol.range.end.row),
347 );
348 let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
349 WorkflowSuggestion::Update {
350 range,
351 description,
352 symbol_path,
353 }
354 }
355 WorkflowStepEditKind::Create { description } => {
356 WorkflowSuggestion::CreateFile { description }
357 }
358 WorkflowStepEditKind::InsertSiblingBefore {
359 symbol,
360 description,
361 } => {
362 let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
363 let position = snapshot.anchor_before(
364 symbol
365 .annotation_range
366 .map_or(symbol.range.start, |annotation_range| {
367 annotation_range.start
368 }),
369 );
370 WorkflowSuggestion::InsertSiblingBefore {
371 position,
372 description,
373 symbol_path,
374 }
375 }
376 WorkflowStepEditKind::InsertSiblingAfter {
377 symbol,
378 description,
379 } => {
380 let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
381 let position = snapshot.anchor_after(symbol.range.end);
382 WorkflowSuggestion::InsertSiblingAfter {
383 position,
384 description,
385 symbol_path,
386 }
387 }
388 WorkflowStepEditKind::PrependChild {
389 symbol,
390 description,
391 } => {
392 if let Some(symbol) = symbol {
393 let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
394
395 let position = snapshot.anchor_after(
396 symbol
397 .body_range
398 .map_or(symbol.range.start, |body_range| body_range.start),
399 );
400 WorkflowSuggestion::PrependChild {
401 position,
402 description,
403 symbol_path: Some(symbol_path),
404 }
405 } else {
406 WorkflowSuggestion::PrependChild {
407 position: language::Anchor::MIN,
408 description,
409 symbol_path: None,
410 }
411 }
412 }
413 WorkflowStepEditKind::AppendChild {
414 symbol,
415 description,
416 } => {
417 if let Some(symbol) = symbol {
418 let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
419
420 let position = snapshot.anchor_before(
421 symbol
422 .body_range
423 .map_or(symbol.range.end, |body_range| body_range.end),
424 );
425 WorkflowSuggestion::AppendChild {
426 position,
427 description,
428 symbol_path: Some(symbol_path),
429 }
430 } else {
431 WorkflowSuggestion::PrependChild {
432 position: language::Anchor::MAX,
433 description,
434 symbol_path: None,
435 }
436 }
437 }
438 WorkflowStepEditKind::Delete { symbol } => {
439 let (symbol_path, symbol) = Self::resolve_symbol(&snapshot, &outline, &symbol)?;
440 let start = symbol
441 .annotation_range
442 .map_or(symbol.range.start, |range| range.start);
443 let start = Point::new(start.row, 0);
444 let end = Point::new(
445 symbol.range.end.row,
446 snapshot.line_len(symbol.range.end.row),
447 );
448 let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
449 WorkflowSuggestion::Delete { range, symbol_path }
450 }
451 };
452
453 Ok((buffer, suggestion))
454 }
455
456 fn resolve_symbol(
457 snapshot: &BufferSnapshot,
458 outline: &Outline<Anchor>,
459 symbol: &str,
460 ) -> Result<(SymbolPath, OutlineItem<Point>)> {
461 if symbol == IMPORTS_SYMBOL {
462 let target_row = find_first_non_comment_line(snapshot);
463 Ok((
464 SymbolPath(IMPORTS_SYMBOL.to_string()),
465 OutlineItem {
466 range: Point::new(target_row, 0)..Point::new(target_row + 1, 0),
467 ..Default::default()
468 },
469 ))
470 } else {
471 let (symbol_path, symbol) = outline
472 .find_most_similar(symbol)
473 .with_context(|| format!("symbol not found: {symbol}"))?;
474 Ok((symbol_path, symbol.to_point(snapshot)))
475 }
476 }
477}
478
479fn find_first_non_comment_line(snapshot: &BufferSnapshot) -> u32 {
480 let Some(language) = snapshot.language() else {
481 return 0;
482 };
483
484 let scope = language.default_scope();
485 let comment_prefixes = scope.line_comment_prefixes();
486
487 let mut chunks = snapshot.as_rope().chunks();
488 let mut target_row = 0;
489 loop {
490 let starts_with_comment = chunks
491 .peek()
492 .map(|chunk| {
493 comment_prefixes
494 .iter()
495 .any(|s| chunk.starts_with(s.as_ref().trim_end()))
496 })
497 .unwrap_or(false);
498
499 if !starts_with_comment {
500 break;
501 }
502
503 target_row += 1;
504 if !chunks.next_line() {
505 break;
506 }
507 }
508 target_row
509}
510
511#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
512#[serde(tag = "operation")]
513pub enum WorkflowStepEditKind {
514 /// Rewrites the specified symbol entirely based on the given description.
515 /// This operation completely replaces the existing symbol with new content.
516 Update {
517 /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
518 /// The path should uniquely identify the symbol within the containing file.
519 symbol: String,
520 /// A brief description of the transformation to apply to the symbol.
521 description: String,
522 },
523 /// Creates a new file with the given path based on the provided description.
524 /// This operation adds a new file to the codebase.
525 Create {
526 /// A brief description of the file to be created.
527 description: String,
528 },
529 /// Inserts a new symbol based on the given description before the specified symbol.
530 /// This operation adds new content immediately preceding an existing symbol.
531 InsertSiblingBefore {
532 /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
533 /// The new content will be inserted immediately before this symbol.
534 symbol: String,
535 /// A brief description of the new symbol to be inserted.
536 description: String,
537 },
538 /// Inserts a new symbol based on the given description after the specified symbol.
539 /// This operation adds new content immediately following an existing symbol.
540 InsertSiblingAfter {
541 /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
542 /// The new content will be inserted immediately after this symbol.
543 symbol: String,
544 /// A brief description of the new symbol to be inserted.
545 description: String,
546 },
547 /// Inserts a new symbol as a child of the specified symbol at the start.
548 /// This operation adds new content as the first child of an existing symbol (or file if no symbol is provided).
549 PrependChild {
550 /// 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`.
551 /// If provided, the new content will be inserted as the first child of this symbol.
552 /// If not provided, the new content will be inserted at the top of the file.
553 symbol: Option<String>,
554 /// A brief description of the new symbol to be inserted.
555 description: String,
556 },
557 /// Inserts a new symbol as a child of the specified symbol at the end.
558 /// This operation adds new content as the last child of an existing symbol (or file if no symbol is provided).
559 AppendChild {
560 /// 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`.
561 /// If provided, the new content will be inserted as the last child of this symbol.
562 /// If not provided, the new content will be applied at the bottom of the file.
563 symbol: Option<String>,
564 /// A brief description of the new symbol to be inserted.
565 description: String,
566 },
567 /// Deletes the specified symbol from the containing file.
568 Delete {
569 /// An fully-qualified reference to the symbol to be deleted, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`.
570 symbol: String,
571 },
572}