recent_buffers.rs

  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}