1use std::fmt::Write;
2use std::iter;
3use std::path::PathBuf;
4use std::time::Duration;
5
6use gpui::{ModelContext, Subscription, Task, WeakModel};
7use language::{Buffer, BufferSnapshot, DiagnosticEntry, Point};
8
9use crate::ambient_context::ContextUpdated;
10use crate::assistant_panel::Conversation;
11use crate::{LanguageModelRequestMessage, Role};
12
13pub struct RecentBuffersContext {
14 pub enabled: bool,
15 pub buffers: Vec<RecentBuffer>,
16 pub message: String,
17 pub pending_message: Option<Task<()>>,
18}
19
20pub struct RecentBuffer {
21 pub buffer: WeakModel<Buffer>,
22 pub _subscription: Subscription,
23}
24
25impl Default for RecentBuffersContext {
26 fn default() -> Self {
27 Self {
28 enabled: true,
29 buffers: Vec::new(),
30 message: String::new(),
31 pending_message: None,
32 }
33 }
34}
35
36impl RecentBuffersContext {
37 /// Returns the [`RecentBuffersContext`] as a message to the language model.
38 pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
39 self.enabled.then(|| LanguageModelRequestMessage {
40 role: Role::System,
41 content: self.message.clone(),
42 })
43 }
44
45 pub fn update(&mut self, cx: &mut ModelContext<Conversation>) -> ContextUpdated {
46 let buffers = self
47 .buffers
48 .iter()
49 .filter_map(|recent| {
50 recent
51 .buffer
52 .read_with(cx, |buffer, cx| {
53 (
54 buffer.file().map(|file| file.full_path(cx)),
55 buffer.snapshot(),
56 )
57 })
58 .ok()
59 })
60 .collect::<Vec<_>>();
61
62 if !self.enabled || buffers.is_empty() {
63 self.message.clear();
64 self.pending_message = None;
65 cx.notify();
66 ContextUpdated::Disabled
67 } else {
68 self.pending_message = Some(cx.spawn(|this, mut cx| async move {
69 const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
70 cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
71
72 let message = cx
73 .background_executor()
74 .spawn(async move { Self::build_message(&buffers) })
75 .await;
76 this.update(&mut cx, |conversation, cx| {
77 conversation.ambient_context.recent_buffers.message = message;
78 conversation.count_remaining_tokens(cx);
79 cx.notify();
80 })
81 .ok();
82 }));
83
84 ContextUpdated::Updating
85 }
86 }
87
88 fn build_message(buffers: &[(Option<PathBuf>, BufferSnapshot)]) -> String {
89 let mut message = String::new();
90 writeln!(
91 message,
92 "The following is a list of recent buffers that the user has opened."
93 )
94 .unwrap();
95 writeln!(
96 message,
97 "For every line in the buffer, I will include a row number that line corresponds to."
98 )
99 .unwrap();
100 writeln!(
101 message,
102 "Lines that don't have a number correspond to errors and warnings. For example:"
103 )
104 .unwrap();
105 writeln!(message, "path/to/file.md").unwrap();
106 writeln!(message, "```markdown").unwrap();
107 writeln!(message, "1 The quick brown fox").unwrap();
108 writeln!(message, "2 jumps over one active").unwrap();
109 writeln!(message, " --- error: should be 'the'").unwrap();
110 writeln!(message, " ------ error: should be 'lazy'").unwrap();
111 writeln!(message, "3 dog").unwrap();
112 writeln!(message, "```").unwrap();
113
114 message.push('\n');
115 writeln!(message, "Here's the actual recent buffer list:").unwrap();
116 for (path, buffer) in buffers {
117 if let Some(path) = path {
118 writeln!(message, "{}", path.display()).unwrap();
119 } else {
120 writeln!(message, "untitled").unwrap();
121 }
122
123 if let Some(language) = buffer.language() {
124 writeln!(message, "```{}", language.name().to_lowercase()).unwrap();
125 } else {
126 writeln!(message, "```").unwrap();
127 }
128
129 let mut diagnostics = buffer
130 .diagnostics_in_range::<_, Point>(
131 language::Anchor::MIN..language::Anchor::MAX,
132 false,
133 )
134 .peekable();
135
136 let mut active_diagnostics = Vec::<DiagnosticEntry<Point>>::new();
137 const GUTTER_PADDING: usize = 4;
138 let gutter_width =
139 ((buffer.max_point().row + 1) as f32).log10() as usize + 1 + GUTTER_PADDING;
140 for buffer_row in 0..=buffer.max_point().row {
141 let display_row = buffer_row + 1;
142 active_diagnostics.retain(|diagnostic| {
143 (diagnostic.range.start.row..=diagnostic.range.end.row).contains(&buffer_row)
144 });
145 while diagnostics.peek().map_or(false, |diagnostic| {
146 (diagnostic.range.start.row..=diagnostic.range.end.row).contains(&buffer_row)
147 }) {
148 active_diagnostics.push(diagnostics.next().unwrap());
149 }
150
151 let row_width = (display_row as f32).log10() as usize + 1;
152 write!(message, "{}", display_row).unwrap();
153 if row_width < gutter_width {
154 message.extend(iter::repeat(' ').take(gutter_width - row_width));
155 }
156
157 for chunk in buffer.text_for_range(
158 Point::new(buffer_row, 0)..Point::new(buffer_row, buffer.line_len(buffer_row)),
159 ) {
160 message.push_str(chunk);
161 }
162 message.push('\n');
163
164 for diagnostic in &active_diagnostics {
165 message.extend(iter::repeat(' ').take(gutter_width));
166
167 let start_column = if diagnostic.range.start.row == buffer_row {
168 message
169 .extend(iter::repeat(' ').take(diagnostic.range.start.column as usize));
170 diagnostic.range.start.column
171 } else {
172 0
173 };
174 let end_column = if diagnostic.range.end.row == buffer_row {
175 diagnostic.range.end.column
176 } else {
177 buffer.line_len(buffer_row)
178 };
179
180 message.extend(iter::repeat('-').take((end_column - start_column) as usize));
181 writeln!(message, " {}", diagnostic.diagnostic.message).unwrap();
182 }
183 }
184
185 message.push('\n');
186 }
187
188 writeln!(
189 message,
190 "When quoting the above code, mention which rows the code occurs at."
191 )
192 .unwrap();
193 writeln!(
194 message,
195 "Never include rows in the quoted code itself and only report lines that didn't start with a row number."
196 )
197 .unwrap();
198
199 message
200 }
201}