recent_buffers.rs

  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}