1use crate::{
2 CacheStatus, InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus, TextThread,
3 TextThreadEvent, TextThreadId, TextThreadOperation, TextThreadSummary,
4};
5use anyhow::Result;
6use assistant_slash_command::{
7 ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
8 SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, SlashCommandWorkingSet,
9};
10use assistant_slash_commands::FileSlashCommand;
11use collections::{HashMap, HashSet};
12use fs::FakeFs;
13use futures::{
14 channel::mpsc,
15 stream::{self, StreamExt},
16};
17use gpui::{App, Entity, SharedString, Task, TestAppContext, WeakEntity, prelude::*};
18use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
19use language_model::{
20 ConfiguredModel, LanguageModelCacheConfiguration, LanguageModelRegistry, Role,
21 fake_provider::{FakeLanguageModel, FakeLanguageModelProvider},
22};
23use parking_lot::Mutex;
24use pretty_assertions::assert_eq;
25use project::Project;
26use prompt_store::PromptBuilder;
27use rand::prelude::*;
28use serde_json::json;
29use settings::SettingsStore;
30use std::{
31 cell::RefCell,
32 env,
33 ops::Range,
34 path::Path,
35 rc::Rc,
36 sync::{Arc, atomic::AtomicBool},
37};
38use text::{ReplicaId, ToOffset, network::Network};
39use ui::{IconName, Window};
40use unindent::Unindent;
41use util::RandomCharIter;
42use workspace::Workspace;
43
44#[gpui::test]
45fn test_inserting_and_removing_messages(cx: &mut App) {
46 init_test(cx);
47
48 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
49 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
50 let text_thread = cx.new(|cx| {
51 TextThread::local(
52 registry,
53 None,
54 None,
55 prompt_builder.clone(),
56 Arc::new(SlashCommandWorkingSet::default()),
57 cx,
58 )
59 });
60 let buffer = text_thread.read(cx).buffer().clone();
61
62 let message_1 = text_thread.read(cx).message_anchors[0].clone();
63 assert_eq!(
64 messages(&text_thread, cx),
65 vec![(message_1.id, Role::User, 0..0)]
66 );
67
68 let message_2 = text_thread.update(cx, |context, cx| {
69 context
70 .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
71 .unwrap()
72 });
73 assert_eq!(
74 messages(&text_thread, cx),
75 vec![
76 (message_1.id, Role::User, 0..1),
77 (message_2.id, Role::Assistant, 1..1)
78 ]
79 );
80
81 buffer.update(cx, |buffer, cx| {
82 buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
83 });
84 assert_eq!(
85 messages(&text_thread, cx),
86 vec![
87 (message_1.id, Role::User, 0..2),
88 (message_2.id, Role::Assistant, 2..3)
89 ]
90 );
91
92 let message_3 = text_thread.update(cx, |context, cx| {
93 context
94 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
95 .unwrap()
96 });
97 assert_eq!(
98 messages(&text_thread, cx),
99 vec![
100 (message_1.id, Role::User, 0..2),
101 (message_2.id, Role::Assistant, 2..4),
102 (message_3.id, Role::User, 4..4)
103 ]
104 );
105
106 let message_4 = text_thread.update(cx, |context, cx| {
107 context
108 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
109 .unwrap()
110 });
111 assert_eq!(
112 messages(&text_thread, cx),
113 vec![
114 (message_1.id, Role::User, 0..2),
115 (message_2.id, Role::Assistant, 2..4),
116 (message_4.id, Role::User, 4..5),
117 (message_3.id, Role::User, 5..5),
118 ]
119 );
120
121 buffer.update(cx, |buffer, cx| {
122 buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
123 });
124 assert_eq!(
125 messages(&text_thread, cx),
126 vec![
127 (message_1.id, Role::User, 0..2),
128 (message_2.id, Role::Assistant, 2..4),
129 (message_4.id, Role::User, 4..6),
130 (message_3.id, Role::User, 6..7),
131 ]
132 );
133
134 // Deleting across message boundaries merges the messages.
135 buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
136 assert_eq!(
137 messages(&text_thread, cx),
138 vec![
139 (message_1.id, Role::User, 0..3),
140 (message_3.id, Role::User, 3..4),
141 ]
142 );
143
144 // Undoing the deletion should also undo the merge.
145 buffer.update(cx, |buffer, cx| buffer.undo(cx));
146 assert_eq!(
147 messages(&text_thread, cx),
148 vec![
149 (message_1.id, Role::User, 0..2),
150 (message_2.id, Role::Assistant, 2..4),
151 (message_4.id, Role::User, 4..6),
152 (message_3.id, Role::User, 6..7),
153 ]
154 );
155
156 // Redoing the deletion should also redo the merge.
157 buffer.update(cx, |buffer, cx| buffer.redo(cx));
158 assert_eq!(
159 messages(&text_thread, cx),
160 vec![
161 (message_1.id, Role::User, 0..3),
162 (message_3.id, Role::User, 3..4),
163 ]
164 );
165
166 // Ensure we can still insert after a merged message.
167 let message_5 = text_thread.update(cx, |context, cx| {
168 context
169 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
170 .unwrap()
171 });
172 assert_eq!(
173 messages(&text_thread, cx),
174 vec![
175 (message_1.id, Role::User, 0..3),
176 (message_5.id, Role::System, 3..4),
177 (message_3.id, Role::User, 4..5)
178 ]
179 );
180}
181
182#[gpui::test]
183fn test_message_splitting(cx: &mut App) {
184 init_test(cx);
185
186 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
187
188 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
189 let text_thread = cx.new(|cx| {
190 TextThread::local(
191 registry.clone(),
192 None,
193 None,
194 prompt_builder.clone(),
195 Arc::new(SlashCommandWorkingSet::default()),
196 cx,
197 )
198 });
199 let buffer = text_thread.read(cx).buffer().clone();
200
201 let message_1 = text_thread.read(cx).message_anchors[0].clone();
202 assert_eq!(
203 messages(&text_thread, cx),
204 vec![(message_1.id, Role::User, 0..0)]
205 );
206
207 buffer.update(cx, |buffer, cx| {
208 buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
209 });
210
211 let (_, message_2) =
212 text_thread.update(cx, |text_thread, cx| text_thread.split_message(3..3, cx));
213 let message_2 = message_2.unwrap();
214
215 // We recycle newlines in the middle of a split message
216 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
217 assert_eq!(
218 messages(&text_thread, cx),
219 vec![
220 (message_1.id, Role::User, 0..4),
221 (message_2.id, Role::User, 4..16),
222 ]
223 );
224
225 let (_, message_3) =
226 text_thread.update(cx, |text_thread, cx| text_thread.split_message(3..3, cx));
227 let message_3 = message_3.unwrap();
228
229 // We don't recycle newlines at the end of a split message
230 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
231 assert_eq!(
232 messages(&text_thread, cx),
233 vec![
234 (message_1.id, Role::User, 0..4),
235 (message_3.id, Role::User, 4..5),
236 (message_2.id, Role::User, 5..17),
237 ]
238 );
239
240 let (_, message_4) =
241 text_thread.update(cx, |text_thread, cx| text_thread.split_message(9..9, cx));
242 let message_4 = message_4.unwrap();
243 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
244 assert_eq!(
245 messages(&text_thread, cx),
246 vec![
247 (message_1.id, Role::User, 0..4),
248 (message_3.id, Role::User, 4..5),
249 (message_2.id, Role::User, 5..9),
250 (message_4.id, Role::User, 9..17),
251 ]
252 );
253
254 let (_, message_5) =
255 text_thread.update(cx, |text_thread, cx| text_thread.split_message(9..9, cx));
256 let message_5 = message_5.unwrap();
257 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
258 assert_eq!(
259 messages(&text_thread, cx),
260 vec![
261 (message_1.id, Role::User, 0..4),
262 (message_3.id, Role::User, 4..5),
263 (message_2.id, Role::User, 5..9),
264 (message_4.id, Role::User, 9..10),
265 (message_5.id, Role::User, 10..18),
266 ]
267 );
268
269 let (message_6, message_7) =
270 text_thread.update(cx, |text_thread, cx| text_thread.split_message(14..16, cx));
271 let message_6 = message_6.unwrap();
272 let message_7 = message_7.unwrap();
273 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
274 assert_eq!(
275 messages(&text_thread, cx),
276 vec![
277 (message_1.id, Role::User, 0..4),
278 (message_3.id, Role::User, 4..5),
279 (message_2.id, Role::User, 5..9),
280 (message_4.id, Role::User, 9..10),
281 (message_5.id, Role::User, 10..14),
282 (message_6.id, Role::User, 14..17),
283 (message_7.id, Role::User, 17..19),
284 ]
285 );
286}
287
288#[gpui::test]
289fn test_messages_for_offsets(cx: &mut App) {
290 init_test(cx);
291
292 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
293 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
294 let text_thread = cx.new(|cx| {
295 TextThread::local(
296 registry,
297 None,
298 None,
299 prompt_builder.clone(),
300 Arc::new(SlashCommandWorkingSet::default()),
301 cx,
302 )
303 });
304 let buffer = text_thread.read(cx).buffer().clone();
305
306 let message_1 = text_thread.read(cx).message_anchors[0].clone();
307 assert_eq!(
308 messages(&text_thread, cx),
309 vec![(message_1.id, Role::User, 0..0)]
310 );
311
312 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
313 let message_2 = text_thread
314 .update(cx, |text_thread, cx| {
315 text_thread.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
316 })
317 .unwrap();
318 buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
319
320 let message_3 = text_thread
321 .update(cx, |text_thread, cx| {
322 text_thread.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
323 })
324 .unwrap();
325 buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
326
327 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
328 assert_eq!(
329 messages(&text_thread, cx),
330 vec![
331 (message_1.id, Role::User, 0..4),
332 (message_2.id, Role::User, 4..8),
333 (message_3.id, Role::User, 8..11)
334 ]
335 );
336
337 assert_eq!(
338 message_ids_for_offsets(&text_thread, &[0, 4, 9], cx),
339 [message_1.id, message_2.id, message_3.id]
340 );
341 assert_eq!(
342 message_ids_for_offsets(&text_thread, &[0, 1, 11], cx),
343 [message_1.id, message_3.id]
344 );
345
346 let message_4 = text_thread
347 .update(cx, |text_thread, cx| {
348 text_thread.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
349 })
350 .unwrap();
351 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
352 assert_eq!(
353 messages(&text_thread, cx),
354 vec![
355 (message_1.id, Role::User, 0..4),
356 (message_2.id, Role::User, 4..8),
357 (message_3.id, Role::User, 8..12),
358 (message_4.id, Role::User, 12..12)
359 ]
360 );
361 assert_eq!(
362 message_ids_for_offsets(&text_thread, &[0, 4, 8, 12], cx),
363 [message_1.id, message_2.id, message_3.id, message_4.id]
364 );
365
366 fn message_ids_for_offsets(
367 context: &Entity<TextThread>,
368 offsets: &[usize],
369 cx: &App,
370 ) -> Vec<MessageId> {
371 context
372 .read(cx)
373 .messages_for_offsets(offsets.iter().copied(), cx)
374 .into_iter()
375 .map(|message| message.id)
376 .collect()
377 }
378}
379
380#[gpui::test]
381async fn test_slash_commands(cx: &mut TestAppContext) {
382 cx.update(init_test);
383
384 let fs = FakeFs::new(cx.background_executor.clone());
385
386 fs.insert_tree(
387 "/test",
388 json!({
389 "src": {
390 "lib.rs": "fn one() -> usize { 1 }",
391 "main.rs": "
392 use crate::one;
393 fn main() { one(); }
394 ".unindent(),
395 }
396 }),
397 )
398 .await;
399
400 let slash_command_registry = cx.update(SlashCommandRegistry::default_global);
401 slash_command_registry.register_command(FileSlashCommand, false);
402
403 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
404 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
405 let text_thread = cx.new(|cx| {
406 TextThread::local(
407 registry.clone(),
408 None,
409 None,
410 prompt_builder.clone(),
411 Arc::new(SlashCommandWorkingSet::default()),
412 cx,
413 )
414 });
415
416 #[derive(Default)]
417 struct ContextRanges {
418 parsed_commands: HashSet<Range<language::Anchor>>,
419 command_outputs: HashMap<InvokedSlashCommandId, Range<language::Anchor>>,
420 output_sections: HashSet<Range<language::Anchor>>,
421 }
422
423 let context_ranges = Rc::new(RefCell::new(ContextRanges::default()));
424 text_thread.update(cx, |_, cx| {
425 cx.subscribe(&text_thread, {
426 let context_ranges = context_ranges.clone();
427 move |text_thread, _, event, _| {
428 let mut context_ranges = context_ranges.borrow_mut();
429 match event {
430 TextThreadEvent::InvokedSlashCommandChanged { command_id } => {
431 let command = text_thread.invoked_slash_command(command_id).unwrap();
432 context_ranges
433 .command_outputs
434 .insert(*command_id, command.range.clone());
435 }
436 TextThreadEvent::ParsedSlashCommandsUpdated { removed, updated } => {
437 for range in removed {
438 context_ranges.parsed_commands.remove(range);
439 }
440 for command in updated {
441 context_ranges
442 .parsed_commands
443 .insert(command.source_range.clone());
444 }
445 }
446 TextThreadEvent::SlashCommandOutputSectionAdded { section } => {
447 context_ranges.output_sections.insert(section.range.clone());
448 }
449 _ => {}
450 }
451 }
452 })
453 .detach();
454 });
455
456 let buffer = text_thread.read_with(cx, |text_thread, _| text_thread.buffer().clone());
457
458 // Insert a slash command
459 buffer.update(cx, |buffer, cx| {
460 buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
461 });
462 assert_text_and_context_ranges(
463 &buffer,
464 &context_ranges,
465 &"
466 «/file src/lib.rs»"
467 .unindent(),
468 cx,
469 );
470
471 // Edit the argument of the slash command.
472 buffer.update(cx, |buffer, cx| {
473 let edit_offset = buffer.text().find("lib.rs").unwrap();
474 buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
475 });
476 assert_text_and_context_ranges(
477 &buffer,
478 &context_ranges,
479 &"
480 «/file src/main.rs»"
481 .unindent(),
482 cx,
483 );
484
485 // Edit the name of the slash command, using one that doesn't exist.
486 buffer.update(cx, |buffer, cx| {
487 let edit_offset = buffer.text().find("/file").unwrap();
488 buffer.edit(
489 [(edit_offset..edit_offset + "/file".len(), "/unknown")],
490 None,
491 cx,
492 );
493 });
494 assert_text_and_context_ranges(
495 &buffer,
496 &context_ranges,
497 &"
498 /unknown src/main.rs"
499 .unindent(),
500 cx,
501 );
502
503 // Undoing the insertion of an non-existent slash command resorts the previous one.
504 buffer.update(cx, |buffer, cx| buffer.undo(cx));
505 assert_text_and_context_ranges(
506 &buffer,
507 &context_ranges,
508 &"
509 «/file src/main.rs»"
510 .unindent(),
511 cx,
512 );
513
514 let (command_output_tx, command_output_rx) = mpsc::unbounded();
515 text_thread.update(cx, |text_thread, cx| {
516 let command_source_range = text_thread.parsed_slash_commands[0].source_range.clone();
517 text_thread.insert_command_output(
518 command_source_range,
519 "file",
520 Task::ready(Ok(command_output_rx.boxed())),
521 true,
522 cx,
523 );
524 });
525 assert_text_and_context_ranges(
526 &buffer,
527 &context_ranges,
528 &"
529 ⟦«/file src/main.rs»
530 …⟧
531 "
532 .unindent(),
533 cx,
534 );
535
536 command_output_tx
537 .unbounded_send(Ok(SlashCommandEvent::StartSection {
538 icon: IconName::Ai,
539 label: "src/main.rs".into(),
540 metadata: None,
541 }))
542 .unwrap();
543 command_output_tx
544 .unbounded_send(Ok(SlashCommandEvent::Content("src/main.rs".into())))
545 .unwrap();
546 cx.run_until_parked();
547 assert_text_and_context_ranges(
548 &buffer,
549 &context_ranges,
550 &"
551 ⟦«/file src/main.rs»
552 src/main.rs…⟧
553 "
554 .unindent(),
555 cx,
556 );
557
558 command_output_tx
559 .unbounded_send(Ok(SlashCommandEvent::Content("\nfn main() {}".into())))
560 .unwrap();
561 cx.run_until_parked();
562 assert_text_and_context_ranges(
563 &buffer,
564 &context_ranges,
565 &"
566 ⟦«/file src/main.rs»
567 src/main.rs
568 fn main() {}…⟧
569 "
570 .unindent(),
571 cx,
572 );
573
574 command_output_tx
575 .unbounded_send(Ok(SlashCommandEvent::EndSection))
576 .unwrap();
577 cx.run_until_parked();
578 assert_text_and_context_ranges(
579 &buffer,
580 &context_ranges,
581 &"
582 ⟦«/file src/main.rs»
583 ⟪src/main.rs
584 fn main() {}⟫…⟧
585 "
586 .unindent(),
587 cx,
588 );
589
590 drop(command_output_tx);
591 cx.run_until_parked();
592 assert_text_and_context_ranges(
593 &buffer,
594 &context_ranges,
595 &"
596 ⟦⟪src/main.rs
597 fn main() {}⟫⟧
598 "
599 .unindent(),
600 cx,
601 );
602
603 #[track_caller]
604 fn assert_text_and_context_ranges(
605 buffer: &Entity<Buffer>,
606 ranges: &RefCell<ContextRanges>,
607 expected_marked_text: &str,
608 cx: &mut TestAppContext,
609 ) {
610 let mut actual_marked_text = String::new();
611 buffer.update(cx, |buffer, _| {
612 struct Endpoint {
613 offset: usize,
614 marker: char,
615 }
616
617 let ranges = ranges.borrow();
618 let mut endpoints = Vec::new();
619 for range in ranges.command_outputs.values() {
620 endpoints.push(Endpoint {
621 offset: range.start.to_offset(buffer),
622 marker: '⟦',
623 });
624 }
625 for range in ranges.parsed_commands.iter() {
626 endpoints.push(Endpoint {
627 offset: range.start.to_offset(buffer),
628 marker: '«',
629 });
630 }
631 for range in ranges.output_sections.iter() {
632 endpoints.push(Endpoint {
633 offset: range.start.to_offset(buffer),
634 marker: '⟪',
635 });
636 }
637
638 for range in ranges.output_sections.iter() {
639 endpoints.push(Endpoint {
640 offset: range.end.to_offset(buffer),
641 marker: '⟫',
642 });
643 }
644 for range in ranges.parsed_commands.iter() {
645 endpoints.push(Endpoint {
646 offset: range.end.to_offset(buffer),
647 marker: '»',
648 });
649 }
650 for range in ranges.command_outputs.values() {
651 endpoints.push(Endpoint {
652 offset: range.end.to_offset(buffer),
653 marker: '⟧',
654 });
655 }
656
657 endpoints.sort_by_key(|endpoint| endpoint.offset);
658 let mut offset = 0;
659 for endpoint in endpoints {
660 actual_marked_text.extend(buffer.text_for_range(offset..endpoint.offset));
661 actual_marked_text.push(endpoint.marker);
662 offset = endpoint.offset;
663 }
664 actual_marked_text.extend(buffer.text_for_range(offset..buffer.len()));
665 });
666
667 assert_eq!(actual_marked_text, expected_marked_text);
668 }
669}
670
671#[gpui::test]
672async fn test_serialization(cx: &mut TestAppContext) {
673 cx.update(init_test);
674
675 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
676 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
677 let text_thread = cx.new(|cx| {
678 TextThread::local(
679 registry.clone(),
680 None,
681 None,
682 prompt_builder.clone(),
683 Arc::new(SlashCommandWorkingSet::default()),
684 cx,
685 )
686 });
687 let buffer = text_thread.read_with(cx, |text_thread, _| text_thread.buffer().clone());
688 let message_0 = text_thread.read_with(cx, |text_thread, _| text_thread.message_anchors[0].id);
689 let message_1 = text_thread.update(cx, |text_thread, cx| {
690 text_thread
691 .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
692 .unwrap()
693 });
694 let message_2 = text_thread.update(cx, |text_thread, cx| {
695 text_thread
696 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
697 .unwrap()
698 });
699 buffer.update(cx, |buffer, cx| {
700 buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
701 buffer.finalize_last_transaction();
702 });
703 let _message_3 = text_thread.update(cx, |text_thread, cx| {
704 text_thread
705 .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
706 .unwrap()
707 });
708 buffer.update(cx, |buffer, cx| buffer.undo(cx));
709 assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
710 assert_eq!(
711 cx.read(|cx| messages(&text_thread, cx)),
712 [
713 (message_0, Role::User, 0..2),
714 (message_1.id, Role::Assistant, 2..6),
715 (message_2.id, Role::System, 6..6),
716 ]
717 );
718
719 let serialized_context = text_thread.read_with(cx, |text_thread, cx| text_thread.serialize(cx));
720 let deserialized_context = cx.new(|cx| {
721 TextThread::deserialize(
722 serialized_context,
723 Path::new("").into(),
724 registry.clone(),
725 prompt_builder.clone(),
726 Arc::new(SlashCommandWorkingSet::default()),
727 None,
728 None,
729 cx,
730 )
731 });
732 let deserialized_buffer =
733 deserialized_context.read_with(cx, |text_thread, _| text_thread.buffer().clone());
734 assert_eq!(
735 deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
736 "a\nb\nc\n"
737 );
738 assert_eq!(
739 cx.read(|cx| messages(&deserialized_context, cx)),
740 [
741 (message_0, Role::User, 0..2),
742 (message_1.id, Role::Assistant, 2..6),
743 (message_2.id, Role::System, 6..6),
744 ]
745 );
746}
747
748#[gpui::test(iterations = 25)]
749async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: StdRng) {
750 cx.update(init_test);
751
752 let min_peers = env::var("MIN_PEERS")
753 .map(|i| i.parse().expect("invalid `MIN_PEERS` variable"))
754 .unwrap_or(2);
755 let max_peers = env::var("MAX_PEERS")
756 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
757 .unwrap_or(5);
758 let operations = env::var("OPERATIONS")
759 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
760 .unwrap_or(50);
761
762 let slash_commands = cx.update(SlashCommandRegistry::default_global);
763 slash_commands.register_command(FakeSlashCommand("cmd-1".into()), false);
764 slash_commands.register_command(FakeSlashCommand("cmd-2".into()), false);
765 slash_commands.register_command(FakeSlashCommand("cmd-3".into()), false);
766
767 let registry = Arc::new(LanguageRegistry::test(cx.background_executor.clone()));
768 let network = Arc::new(Mutex::new(Network::new(rng.clone())));
769 let mut text_threads = Vec::new();
770
771 let num_peers = rng.random_range(min_peers..=max_peers);
772 let context_id = TextThreadId::new();
773 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
774 for i in 0..num_peers {
775 let context = cx.new(|cx| {
776 TextThread::new(
777 context_id.clone(),
778 ReplicaId::new(i as u16),
779 language::Capability::ReadWrite,
780 registry.clone(),
781 prompt_builder.clone(),
782 Arc::new(SlashCommandWorkingSet::default()),
783 None,
784 None,
785 cx,
786 )
787 });
788
789 cx.update(|cx| {
790 cx.subscribe(&context, {
791 let network = network.clone();
792 move |_, event, _| {
793 if let TextThreadEvent::Operation(op) = event {
794 network
795 .lock()
796 .broadcast(ReplicaId::new(i as u16), vec![op.to_proto()]);
797 }
798 }
799 })
800 .detach();
801 });
802
803 text_threads.push(context);
804 network.lock().add_peer(ReplicaId::new(i as u16));
805 }
806
807 let mut mutation_count = operations;
808
809 while mutation_count > 0
810 || !network.lock().is_idle()
811 || network.lock().contains_disconnected_peers()
812 {
813 let context_index = rng.random_range(0..text_threads.len());
814 let text_thread = &text_threads[context_index];
815
816 match rng.random_range(0..100) {
817 0..=29 if mutation_count > 0 => {
818 log::info!("Context {}: edit buffer", context_index);
819 text_thread.update(cx, |text_thread, cx| {
820 text_thread
821 .buffer()
822 .update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
823 });
824 mutation_count -= 1;
825 }
826 30..=44 if mutation_count > 0 => {
827 text_thread.update(cx, |text_thread, cx| {
828 let range = text_thread.buffer().read(cx).random_byte_range(0, &mut rng);
829 log::info!("Context {}: split message at {:?}", context_index, range);
830 text_thread.split_message(range, cx);
831 });
832 mutation_count -= 1;
833 }
834 45..=59 if mutation_count > 0 => {
835 text_thread.update(cx, |text_thread, cx| {
836 if let Some(message) = text_thread.messages(cx).choose(&mut rng) {
837 let role = *[Role::User, Role::Assistant, Role::System]
838 .choose(&mut rng)
839 .unwrap();
840 log::info!(
841 "Context {}: insert message after {:?} with {:?}",
842 context_index,
843 message.id,
844 role
845 );
846 text_thread.insert_message_after(message.id, role, MessageStatus::Done, cx);
847 }
848 });
849 mutation_count -= 1;
850 }
851 60..=74 if mutation_count > 0 => {
852 text_thread.update(cx, |text_thread, cx| {
853 let command_text = "/".to_string()
854 + slash_commands
855 .command_names()
856 .choose(&mut rng)
857 .unwrap()
858 .clone()
859 .as_ref();
860
861 let command_range = text_thread.buffer().update(cx, |buffer, cx| {
862 let offset = buffer.random_byte_range(0, &mut rng).start;
863 buffer.edit(
864 [(offset..offset, format!("\n{}\n", command_text))],
865 None,
866 cx,
867 );
868 offset + 1..offset + 1 + command_text.len()
869 });
870
871 let output_text = RandomCharIter::new(&mut rng)
872 .filter(|c| *c != '\r')
873 .take(10)
874 .collect::<String>();
875
876 let mut events = vec![Ok(SlashCommandEvent::StartMessage {
877 role: Role::User,
878 merge_same_roles: true,
879 })];
880
881 let num_sections = rng.random_range(0..=3);
882 let mut section_start = 0;
883 for _ in 0..num_sections {
884 let mut section_end = rng.random_range(section_start..=output_text.len());
885 while !output_text.is_char_boundary(section_end) {
886 section_end += 1;
887 }
888 events.push(Ok(SlashCommandEvent::StartSection {
889 icon: IconName::Ai,
890 label: "section".into(),
891 metadata: None,
892 }));
893 events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
894 text: output_text[section_start..section_end].to_string(),
895 run_commands_in_text: false,
896 })));
897 events.push(Ok(SlashCommandEvent::EndSection));
898 section_start = section_end;
899 }
900
901 if section_start < output_text.len() {
902 events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
903 text: output_text[section_start..].to_string(),
904 run_commands_in_text: false,
905 })));
906 }
907
908 log::info!(
909 "Context {}: insert slash command output at {:?} with {:?} events",
910 context_index,
911 command_range,
912 events.len()
913 );
914
915 let command_range = text_thread
916 .buffer()
917 .read(cx)
918 .anchor_after(command_range.start)
919 ..text_thread
920 .buffer()
921 .read(cx)
922 .anchor_after(command_range.end);
923 text_thread.insert_command_output(
924 command_range,
925 "/command",
926 Task::ready(Ok(stream::iter(events).boxed())),
927 true,
928 cx,
929 );
930 });
931 cx.run_until_parked();
932 mutation_count -= 1;
933 }
934 75..=84 if mutation_count > 0 => {
935 text_thread.update(cx, |text_thread, cx| {
936 if let Some(message) = text_thread.messages(cx).choose(&mut rng) {
937 let new_status = match rng.random_range(0..3) {
938 0 => MessageStatus::Done,
939 1 => MessageStatus::Pending,
940 _ => MessageStatus::Error(SharedString::from("Random error")),
941 };
942 log::info!(
943 "Context {}: update message {:?} status to {:?}",
944 context_index,
945 message.id,
946 new_status
947 );
948 text_thread.update_metadata(message.id, cx, |metadata| {
949 metadata.status = new_status;
950 });
951 }
952 });
953 mutation_count -= 1;
954 }
955 _ => {
956 let replica_id = ReplicaId::new(context_index as u16);
957 if network.lock().is_disconnected(replica_id) {
958 network.lock().reconnect_peer(replica_id, ReplicaId::new(0));
959
960 let (ops_to_send, ops_to_receive) = cx.read(|cx| {
961 let host_context = &text_threads[0].read(cx);
962 let guest_context = text_thread.read(cx);
963 (
964 guest_context.serialize_ops(&host_context.version(cx), cx),
965 host_context.serialize_ops(&guest_context.version(cx), cx),
966 )
967 });
968 let ops_to_send = ops_to_send.await;
969 let ops_to_receive = ops_to_receive
970 .await
971 .into_iter()
972 .map(TextThreadOperation::from_proto)
973 .collect::<Result<Vec<_>>>()
974 .unwrap();
975 log::info!(
976 "Context {}: reconnecting. Sent {} operations, received {} operations",
977 context_index,
978 ops_to_send.len(),
979 ops_to_receive.len()
980 );
981
982 network.lock().broadcast(replica_id, ops_to_send);
983 text_thread.update(cx, |text_thread, cx| {
984 text_thread.apply_ops(ops_to_receive, cx)
985 });
986 } else if rng.random_bool(0.1) && replica_id != ReplicaId::new(0) {
987 log::info!("Context {}: disconnecting", context_index);
988 network.lock().disconnect_peer(replica_id);
989 } else if network.lock().has_unreceived(replica_id) {
990 log::info!("Context {}: applying operations", context_index);
991 let ops = network.lock().receive(replica_id);
992 let ops = ops
993 .into_iter()
994 .map(TextThreadOperation::from_proto)
995 .collect::<Result<Vec<_>>>()
996 .unwrap();
997 text_thread.update(cx, |text_thread, cx| text_thread.apply_ops(ops, cx));
998 }
999 }
1000 }
1001 }
1002
1003 cx.read(|cx| {
1004 let first_context = text_threads[0].read(cx);
1005 for text_thread in &text_threads[1..] {
1006 let text_thread = text_thread.read(cx);
1007 assert!(text_thread.pending_ops.is_empty(), "pending ops: {:?}", text_thread.pending_ops);
1008 assert_eq!(
1009 text_thread.buffer().read(cx).text(),
1010 first_context.buffer().read(cx).text(),
1011 "Context {:?} text != Context 0 text",
1012 text_thread.buffer().read(cx).replica_id()
1013 );
1014 assert_eq!(
1015 text_thread.message_anchors,
1016 first_context.message_anchors,
1017 "Context {:?} messages != Context 0 messages",
1018 text_thread.buffer().read(cx).replica_id()
1019 );
1020 assert_eq!(
1021 text_thread.messages_metadata,
1022 first_context.messages_metadata,
1023 "Context {:?} message metadata != Context 0 message metadata",
1024 text_thread.buffer().read(cx).replica_id()
1025 );
1026 assert_eq!(
1027 text_thread.slash_command_output_sections,
1028 first_context.slash_command_output_sections,
1029 "Context {:?} slash command output sections != Context 0 slash command output sections",
1030 text_thread.buffer().read(cx).replica_id()
1031 );
1032 }
1033 });
1034}
1035
1036#[gpui::test]
1037fn test_mark_cache_anchors(cx: &mut App) {
1038 init_test(cx);
1039
1040 let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
1041 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
1042 let text_thread = cx.new(|cx| {
1043 TextThread::local(
1044 registry,
1045 None,
1046 None,
1047 prompt_builder.clone(),
1048 Arc::new(SlashCommandWorkingSet::default()),
1049 cx,
1050 )
1051 });
1052 let buffer = text_thread.read(cx).buffer().clone();
1053
1054 // Create a test cache configuration
1055 let cache_configuration = &Some(LanguageModelCacheConfiguration {
1056 max_cache_anchors: 3,
1057 should_speculate: true,
1058 min_total_token: 10,
1059 });
1060
1061 let message_1 = text_thread.read(cx).message_anchors[0].clone();
1062
1063 text_thread.update(cx, |text_thread, cx| {
1064 text_thread.mark_cache_anchors(cache_configuration, false, cx)
1065 });
1066
1067 assert_eq!(
1068 messages_cache(&text_thread, cx)
1069 .iter()
1070 .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
1071 .count(),
1072 0,
1073 "Empty messages should not have any cache anchors."
1074 );
1075
1076 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
1077 let message_2 = text_thread
1078 .update(cx, |text_thread, cx| {
1079 text_thread.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx)
1080 })
1081 .unwrap();
1082
1083 buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbbbbbb")], None, cx));
1084 let message_3 = text_thread
1085 .update(cx, |text_thread, cx| {
1086 text_thread.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx)
1087 })
1088 .unwrap();
1089 buffer.update(cx, |buffer, cx| buffer.edit([(12..12, "cccccc")], None, cx));
1090
1091 text_thread.update(cx, |text_thread, cx| {
1092 text_thread.mark_cache_anchors(cache_configuration, false, cx)
1093 });
1094 assert_eq!(buffer.read(cx).text(), "aaa\nbbbbbbb\ncccccc");
1095 assert_eq!(
1096 messages_cache(&text_thread, cx)
1097 .iter()
1098 .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
1099 .count(),
1100 0,
1101 "Messages should not be marked for cache before going over the token minimum."
1102 );
1103 text_thread.update(cx, |text_thread, _| {
1104 text_thread.token_count = Some(20);
1105 });
1106
1107 text_thread.update(cx, |text_thread, cx| {
1108 text_thread.mark_cache_anchors(cache_configuration, true, cx)
1109 });
1110 assert_eq!(
1111 messages_cache(&text_thread, cx)
1112 .iter()
1113 .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
1114 .collect::<Vec<bool>>(),
1115 vec![true, true, false],
1116 "Last message should not be an anchor on speculative request."
1117 );
1118
1119 text_thread
1120 .update(cx, |text_thread, cx| {
1121 text_thread.insert_message_after(
1122 message_3.id,
1123 Role::Assistant,
1124 MessageStatus::Pending,
1125 cx,
1126 )
1127 })
1128 .unwrap();
1129
1130 text_thread.update(cx, |text_thread, cx| {
1131 text_thread.mark_cache_anchors(cache_configuration, false, cx)
1132 });
1133 assert_eq!(
1134 messages_cache(&text_thread, cx)
1135 .iter()
1136 .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
1137 .collect::<Vec<bool>>(),
1138 vec![false, true, true, false],
1139 "Most recent message should also be cached if not a speculative request."
1140 );
1141 text_thread.update(cx, |text_thread, cx| {
1142 text_thread.update_cache_status_for_completion(cx)
1143 });
1144 assert_eq!(
1145 messages_cache(&text_thread, cx)
1146 .iter()
1147 .map(|(_, cache)| cache
1148 .as_ref()
1149 .map_or(None, |cache| Some(cache.status.clone())))
1150 .collect::<Vec<Option<CacheStatus>>>(),
1151 vec![
1152 Some(CacheStatus::Cached),
1153 Some(CacheStatus::Cached),
1154 Some(CacheStatus::Cached),
1155 None
1156 ],
1157 "All user messages prior to anchor should be marked as cached."
1158 );
1159
1160 buffer.update(cx, |buffer, cx| buffer.edit([(14..14, "d")], None, cx));
1161 text_thread.update(cx, |text_thread, cx| {
1162 text_thread.mark_cache_anchors(cache_configuration, false, cx)
1163 });
1164 assert_eq!(
1165 messages_cache(&text_thread, cx)
1166 .iter()
1167 .map(|(_, cache)| cache
1168 .as_ref()
1169 .map_or(None, |cache| Some(cache.status.clone())))
1170 .collect::<Vec<Option<CacheStatus>>>(),
1171 vec![
1172 Some(CacheStatus::Cached),
1173 Some(CacheStatus::Cached),
1174 Some(CacheStatus::Pending),
1175 None
1176 ],
1177 "Modifying a message should invalidate it's cache but leave previous messages."
1178 );
1179 buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "e")], None, cx));
1180 text_thread.update(cx, |text_thread, cx| {
1181 text_thread.mark_cache_anchors(cache_configuration, false, cx)
1182 });
1183 assert_eq!(
1184 messages_cache(&text_thread, cx)
1185 .iter()
1186 .map(|(_, cache)| cache
1187 .as_ref()
1188 .map_or(None, |cache| Some(cache.status.clone())))
1189 .collect::<Vec<Option<CacheStatus>>>(),
1190 vec![
1191 Some(CacheStatus::Pending),
1192 Some(CacheStatus::Pending),
1193 Some(CacheStatus::Pending),
1194 None
1195 ],
1196 "Modifying a message should invalidate all future messages."
1197 );
1198}
1199
1200#[gpui::test]
1201async fn test_summarization(cx: &mut TestAppContext) {
1202 let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx);
1203
1204 // Initial state should be pending
1205 text_thread.read_with(cx, |text_thread, _| {
1206 assert!(matches!(text_thread.summary(), TextThreadSummary::Pending));
1207 assert_eq!(
1208 text_thread.summary().or_default(),
1209 TextThreadSummary::DEFAULT
1210 );
1211 });
1212
1213 let message_1 = text_thread.read_with(cx, |text_thread, _cx| {
1214 text_thread.message_anchors[0].clone()
1215 });
1216 text_thread.update(cx, |context, cx| {
1217 context
1218 .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
1219 .unwrap();
1220 });
1221
1222 // Send a message
1223 text_thread.update(cx, |text_thread, cx| {
1224 text_thread.assist(cx);
1225 });
1226
1227 simulate_successful_response(&fake_model, cx);
1228
1229 // Should start generating summary when there are >= 2 messages
1230 text_thread.read_with(cx, |text_thread, _| {
1231 assert!(!text_thread.summary().content().unwrap().done);
1232 });
1233
1234 cx.run_until_parked();
1235 fake_model.send_last_completion_stream_text_chunk("Brief");
1236 fake_model.send_last_completion_stream_text_chunk(" Introduction");
1237 fake_model.end_last_completion_stream();
1238 cx.run_until_parked();
1239
1240 // Summary should be set
1241 text_thread.read_with(cx, |text_thread, _| {
1242 assert_eq!(text_thread.summary().or_default(), "Brief Introduction");
1243 });
1244
1245 // We should be able to manually set a summary
1246 text_thread.update(cx, |text_thread, cx| {
1247 text_thread.set_custom_summary("Brief Intro".into(), cx);
1248 });
1249
1250 text_thread.read_with(cx, |text_thread, _| {
1251 assert_eq!(text_thread.summary().or_default(), "Brief Intro");
1252 });
1253}
1254
1255#[gpui::test]
1256async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
1257 let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx);
1258
1259 test_summarize_error(&fake_model, &text_thread, cx);
1260
1261 // Now we should be able to set a summary
1262 text_thread.update(cx, |text_thread, cx| {
1263 text_thread.set_custom_summary("Brief Intro".into(), cx);
1264 });
1265
1266 text_thread.read_with(cx, |text_thread, _| {
1267 assert_eq!(text_thread.summary().or_default(), "Brief Intro");
1268 });
1269}
1270
1271#[gpui::test]
1272async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
1273 let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx);
1274
1275 test_summarize_error(&fake_model, &text_thread, cx);
1276
1277 // Sending another message should not trigger another summarize request
1278 text_thread.update(cx, |text_thread, cx| {
1279 text_thread.assist(cx);
1280 });
1281
1282 simulate_successful_response(&fake_model, cx);
1283
1284 text_thread.read_with(cx, |text_thread, _| {
1285 // State is still Error, not Generating
1286 assert!(matches!(text_thread.summary(), TextThreadSummary::Error));
1287 });
1288
1289 // But the summarize request can be invoked manually
1290 text_thread.update(cx, |text_thread, cx| {
1291 text_thread.summarize(true, cx);
1292 });
1293
1294 text_thread.read_with(cx, |text_thread, _| {
1295 assert!(!text_thread.summary().content().unwrap().done);
1296 });
1297
1298 cx.run_until_parked();
1299 fake_model.send_last_completion_stream_text_chunk("A successful summary");
1300 fake_model.end_last_completion_stream();
1301 cx.run_until_parked();
1302
1303 text_thread.read_with(cx, |text_thread, _| {
1304 assert_eq!(text_thread.summary().or_default(), "A successful summary");
1305 });
1306}
1307
1308fn test_summarize_error(
1309 model: &Arc<FakeLanguageModel>,
1310 text_thread: &Entity<TextThread>,
1311 cx: &mut TestAppContext,
1312) {
1313 let message_1 = text_thread.read_with(cx, |text_thread, _cx| {
1314 text_thread.message_anchors[0].clone()
1315 });
1316 text_thread.update(cx, |text_thread, cx| {
1317 text_thread
1318 .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
1319 .unwrap();
1320 });
1321
1322 // Send a message
1323 text_thread.update(cx, |text_thread, cx| {
1324 text_thread.assist(cx);
1325 });
1326
1327 simulate_successful_response(model, cx);
1328
1329 text_thread.read_with(cx, |text_thread, _| {
1330 assert!(!text_thread.summary().content().unwrap().done);
1331 });
1332
1333 // Simulate summary request ending
1334 cx.run_until_parked();
1335 model.end_last_completion_stream();
1336 cx.run_until_parked();
1337
1338 // State is set to Error and default message
1339 text_thread.read_with(cx, |text_thread, _| {
1340 assert_eq!(*text_thread.summary(), TextThreadSummary::Error);
1341 assert_eq!(
1342 text_thread.summary().or_default(),
1343 TextThreadSummary::DEFAULT
1344 );
1345 });
1346}
1347
1348fn setup_context_editor_with_fake_model(
1349 cx: &mut TestAppContext,
1350) -> (Entity<TextThread>, Arc<FakeLanguageModel>) {
1351 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
1352
1353 let fake_provider = Arc::new(FakeLanguageModelProvider::default());
1354 let fake_model = Arc::new(fake_provider.test_model());
1355
1356 cx.update(|cx| {
1357 init_test(cx);
1358 LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
1359 let configured_model = ConfiguredModel {
1360 provider: fake_provider.clone(),
1361 model: fake_model.clone(),
1362 };
1363 registry.set_default_model(Some(configured_model.clone()), cx);
1364 registry.set_thread_summary_model(Some(configured_model), cx);
1365 })
1366 });
1367
1368 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
1369 let context = cx.new(|cx| {
1370 TextThread::local(
1371 registry,
1372 None,
1373 None,
1374 prompt_builder.clone(),
1375 Arc::new(SlashCommandWorkingSet::default()),
1376 cx,
1377 )
1378 });
1379
1380 (context, fake_model)
1381}
1382
1383fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
1384 cx.run_until_parked();
1385 fake_model.send_last_completion_stream_text_chunk("Assistant response");
1386 fake_model.end_last_completion_stream();
1387 cx.run_until_parked();
1388}
1389
1390fn messages(context: &Entity<TextThread>, cx: &App) -> Vec<(MessageId, Role, Range<usize>)> {
1391 context
1392 .read(cx)
1393 .messages(cx)
1394 .map(|message| (message.id, message.role, message.offset_range))
1395 .collect()
1396}
1397
1398fn messages_cache(
1399 context: &Entity<TextThread>,
1400 cx: &App,
1401) -> Vec<(MessageId, Option<MessageCacheMetadata>)> {
1402 context
1403 .read(cx)
1404 .messages(cx)
1405 .map(|message| (message.id, message.cache))
1406 .collect()
1407}
1408
1409fn init_test(cx: &mut App) {
1410 let settings_store = SettingsStore::test(cx);
1411 prompt_store::init(cx);
1412 LanguageModelRegistry::test(cx);
1413 cx.set_global(settings_store);
1414 language::init(cx);
1415 agent_settings::init(cx);
1416 Project::init_settings(cx);
1417}
1418
1419#[derive(Clone)]
1420struct FakeSlashCommand(String);
1421
1422impl SlashCommand for FakeSlashCommand {
1423 fn name(&self) -> String {
1424 self.0.clone()
1425 }
1426
1427 fn description(&self) -> String {
1428 format!("Fake slash command: {}", self.0)
1429 }
1430
1431 fn menu_text(&self) -> String {
1432 format!("Run fake command: {}", self.0)
1433 }
1434
1435 fn complete_argument(
1436 self: Arc<Self>,
1437 _arguments: &[String],
1438 _cancel: Arc<AtomicBool>,
1439 _workspace: Option<WeakEntity<Workspace>>,
1440 _window: &mut Window,
1441 _cx: &mut App,
1442 ) -> Task<Result<Vec<ArgumentCompletion>>> {
1443 Task::ready(Ok(vec![]))
1444 }
1445
1446 fn requires_argument(&self) -> bool {
1447 false
1448 }
1449
1450 fn run(
1451 self: Arc<Self>,
1452 _arguments: &[String],
1453 _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
1454 _context_buffer: BufferSnapshot,
1455 _workspace: WeakEntity<Workspace>,
1456 _delegate: Option<Arc<dyn LspAdapterDelegate>>,
1457 _window: &mut Window,
1458 _cx: &mut App,
1459 ) -> Task<SlashCommandResult> {
1460 Task::ready(Ok(SlashCommandOutput {
1461 text: format!("Executed fake command: {}", self.0),
1462 sections: vec![],
1463 run_commands_in_text: false,
1464 }
1465 .into_event_stream()))
1466 }
1467}