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