context.rs

  1use std::path::Path;
  2use std::rc::Rc;
  3use std::sync::Arc;
  4
  5use file_icons::FileIcons;
  6use gpui::{AppContext, Model, SharedString};
  7use language::Buffer;
  8use language_model::{LanguageModelRequestMessage, MessageContent};
  9use serde::{Deserialize, Serialize};
 10use text::BufferId;
 11use ui::IconName;
 12use util::post_inc;
 13
 14use crate::thread::Thread;
 15
 16#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
 17pub struct ContextId(pub(crate) usize);
 18
 19impl ContextId {
 20    pub fn post_inc(&mut self) -> Self {
 21        Self(post_inc(&mut self.0))
 22    }
 23}
 24
 25/// Some context attached to a message in a thread.
 26#[derive(Debug, Clone)]
 27pub struct ContextSnapshot {
 28    pub id: ContextId,
 29    pub name: SharedString,
 30    pub parent: Option<SharedString>,
 31    pub tooltip: Option<SharedString>,
 32    pub icon_path: Option<SharedString>,
 33    pub kind: ContextKind,
 34    /// Concatenating these strings yields text to send to the model. Not refreshed by `snapshot`.
 35    pub text: Box<[SharedString]>,
 36}
 37
 38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 39pub enum ContextKind {
 40    File,
 41    Directory,
 42    FetchedUrl,
 43    Thread,
 44}
 45
 46impl ContextKind {
 47    pub fn icon(&self) -> IconName {
 48        match self {
 49            ContextKind::File => IconName::File,
 50            ContextKind::Directory => IconName::Folder,
 51            ContextKind::FetchedUrl => IconName::Globe,
 52            ContextKind::Thread => IconName::MessageCircle,
 53        }
 54    }
 55}
 56
 57#[derive(Debug)]
 58pub enum Context {
 59    File(FileContext),
 60    Directory(DirectoryContext),
 61    FetchedUrl(FetchedUrlContext),
 62    Thread(ThreadContext),
 63}
 64
 65impl Context {
 66    pub fn id(&self) -> ContextId {
 67        match self {
 68            Self::File(file) => file.id,
 69            Self::Directory(directory) => directory.snapshot.id,
 70            Self::FetchedUrl(url) => url.id,
 71            Self::Thread(thread) => thread.id,
 72        }
 73    }
 74}
 75
 76#[derive(Debug)]
 77pub struct FileContext {
 78    pub id: ContextId,
 79    pub buffer: ContextBuffer,
 80}
 81
 82#[derive(Debug)]
 83pub struct DirectoryContext {
 84    #[allow(unused)]
 85    pub path: Rc<Path>,
 86    #[allow(unused)]
 87    pub buffers: Vec<ContextBuffer>,
 88    pub snapshot: ContextSnapshot,
 89}
 90
 91#[derive(Debug)]
 92pub struct FetchedUrlContext {
 93    pub id: ContextId,
 94    pub url: SharedString,
 95    pub text: SharedString,
 96}
 97
 98// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
 99// explicitly or have a WeakModel<Thread> and remove during snapshot.
100
101#[derive(Debug)]
102pub struct ThreadContext {
103    pub id: ContextId,
104    pub thread: Model<Thread>,
105    pub text: SharedString,
106}
107
108// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
109// the context from the message editor in this case.
110
111#[derive(Debug)]
112pub struct ContextBuffer {
113    #[allow(unused)]
114    pub id: BufferId,
115    pub buffer: Model<Buffer>,
116    #[allow(unused)]
117    pub version: clock::Global,
118    pub text: SharedString,
119}
120
121impl Context {
122    pub fn snapshot(&self, cx: &AppContext) -> Option<ContextSnapshot> {
123        match &self {
124            Self::File(file_context) => file_context.snapshot(cx),
125            Self::Directory(directory_context) => Some(directory_context.snapshot()),
126            Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
127            Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
128        }
129    }
130}
131
132impl FileContext {
133    pub fn path(&self, cx: &AppContext) -> Option<Arc<Path>> {
134        let buffer = self.buffer.buffer.read(cx);
135        if let Some(file) = buffer.file() {
136            Some(file.path().clone())
137        } else {
138            log::error!("Buffer that had a path unexpectedly no longer has a path.");
139            None
140        }
141    }
142
143    pub fn snapshot(&self, cx: &AppContext) -> Option<ContextSnapshot> {
144        let path = self.path(cx)?;
145        let full_path: SharedString = path.to_string_lossy().into_owned().into();
146        let name = match path.file_name() {
147            Some(name) => name.to_string_lossy().into_owned().into(),
148            None => full_path.clone(),
149        };
150        let parent = path
151            .parent()
152            .and_then(|p| p.file_name())
153            .map(|p| p.to_string_lossy().into_owned().into());
154
155        let icon_path = FileIcons::get_icon(&path, cx);
156
157        Some(ContextSnapshot {
158            id: self.id,
159            name,
160            parent,
161            tooltip: Some(full_path),
162            icon_path,
163            kind: ContextKind::File,
164            text: Box::new([self.buffer.text.clone()]),
165        })
166    }
167}
168
169impl DirectoryContext {
170    pub fn snapshot(&self) -> ContextSnapshot {
171        self.snapshot.clone()
172    }
173}
174
175impl FetchedUrlContext {
176    pub fn snapshot(&self) -> ContextSnapshot {
177        ContextSnapshot {
178            id: self.id,
179            name: self.url.clone(),
180            parent: None,
181            tooltip: None,
182            icon_path: None,
183            kind: ContextKind::FetchedUrl,
184            text: Box::new([self.text.clone()]),
185        }
186    }
187}
188
189impl ThreadContext {
190    pub fn snapshot(&self, cx: &AppContext) -> ContextSnapshot {
191        let thread = self.thread.read(cx);
192        ContextSnapshot {
193            id: self.id,
194            name: thread.summary().unwrap_or("New thread".into()),
195            parent: None,
196            tooltip: None,
197            icon_path: None,
198            kind: ContextKind::Thread,
199            text: Box::new([self.text.clone()]),
200        }
201    }
202}
203
204pub fn attach_context_to_message(
205    message: &mut LanguageModelRequestMessage,
206    contexts: impl Iterator<Item = ContextSnapshot>,
207) {
208    let mut file_context = Vec::new();
209    let mut directory_context = Vec::new();
210    let mut fetch_context = Vec::new();
211    let mut thread_context = Vec::new();
212
213    for context in contexts {
214        match context.kind {
215            ContextKind::File => file_context.push(context),
216            ContextKind::Directory => directory_context.push(context),
217            ContextKind::FetchedUrl => fetch_context.push(context),
218            ContextKind::Thread => thread_context.push(context),
219        }
220    }
221
222    let mut context_text = String::new();
223
224    if !file_context.is_empty() {
225        context_text.push_str("The following files are available:\n");
226        for context in file_context {
227            for chunk in context.text {
228                context_text.push_str(&chunk);
229            }
230            context_text.push('\n');
231        }
232    }
233
234    if !directory_context.is_empty() {
235        context_text.push_str("The following directories are available:\n");
236        for context in directory_context {
237            for chunk in context.text {
238                context_text.push_str(&chunk);
239            }
240            context_text.push('\n');
241        }
242    }
243
244    if !fetch_context.is_empty() {
245        context_text.push_str("The following fetched results are available\n");
246        for context in fetch_context {
247            context_text.push_str(&context.name);
248            context_text.push('\n');
249            for chunk in context.text {
250                context_text.push_str(&chunk);
251            }
252            context_text.push('\n');
253        }
254    }
255
256    if !thread_context.is_empty() {
257        context_text.push_str("The following previous conversation threads are available\n");
258        for context in thread_context {
259            context_text.push_str(&context.name);
260            context_text.push('\n');
261            for chunk in context.text {
262                context_text.push_str(&chunk);
263            }
264            context_text.push('\n');
265        }
266    }
267
268    if !context_text.is_empty() {
269        message.content.push(MessageContent::Text(context_text));
270    }
271}