use std::{ops::Range, path::Path, sync::Arc}; use gpui::{App, Entity, SharedString}; use language::{Buffer, File}; use language_model::LanguageModelRequestMessage; use project::{ProjectPath, Worktree}; use serde::{Deserialize, Serialize}; use text::{Anchor, BufferId}; use ui::IconName; use util::post_inc; use crate::thread::Thread; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] pub struct ContextId(pub(crate) usize); impl ContextId { pub fn post_inc(&mut self) -> Self { Self(post_inc(&mut self.0)) } } pub enum ContextKind { File, Directory, Symbol, FetchedUrl, Thread, } impl ContextKind { pub fn icon(&self) -> IconName { match self { ContextKind::File => IconName::File, ContextKind::Directory => IconName::Folder, ContextKind::Symbol => IconName::Code, ContextKind::FetchedUrl => IconName::Globe, ContextKind::Thread => IconName::MessageBubbles, } } } #[derive(Debug, Clone)] pub enum AssistantContext { File(FileContext), Directory(DirectoryContext), Symbol(SymbolContext), FetchedUrl(FetchedUrlContext), Thread(ThreadContext), } impl AssistantContext { pub fn id(&self) -> ContextId { match self { Self::File(file) => file.id, Self::Directory(directory) => directory.id, Self::Symbol(symbol) => symbol.id, Self::FetchedUrl(url) => url.id, Self::Thread(thread) => thread.id, } } } #[derive(Debug, Clone)] pub struct FileContext { pub id: ContextId, pub context_buffer: ContextBuffer, } #[derive(Debug, Clone)] pub struct DirectoryContext { pub id: ContextId, pub worktree: Entity, pub path: Arc, /// Buffers of the files within the directory. pub context_buffers: Vec, } impl DirectoryContext { pub fn project_path(&self, cx: &App) -> ProjectPath { ProjectPath { worktree_id: self.worktree.read(cx).id(), path: self.path.clone(), } } } #[derive(Debug, Clone)] pub struct SymbolContext { pub id: ContextId, pub context_symbol: ContextSymbol, } #[derive(Debug, Clone)] pub struct FetchedUrlContext { pub id: ContextId, pub url: SharedString, pub text: SharedString, } #[derive(Debug, Clone)] pub struct ThreadContext { pub id: ContextId, // TODO: Entity holds onto the thread even if the thread is deleted. Should probably be // a WeakEntity and handle removal from the UI when it has dropped. pub thread: Entity, pub text: SharedString, } impl ThreadContext { pub fn summary(&self, cx: &App) -> SharedString { self.thread .read(cx) .summary() .unwrap_or("New thread".into()) } } #[derive(Clone)] pub struct ContextBuffer { pub id: BufferId, // TODO: Entity holds onto the thread even if the thread is deleted. Should probably be // a WeakEntity and handle removal from the UI when it has dropped. pub buffer: Entity, pub file: Arc, pub version: clock::Global, pub text: SharedString, } impl std::fmt::Debug for ContextBuffer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ContextBuffer") .field("id", &self.id) .field("buffer", &self.buffer) .field("version", &self.version) .field("text", &self.text) .finish() } } #[derive(Debug, Clone)] pub struct ContextSymbol { pub id: ContextSymbolId, pub buffer: Entity, pub buffer_version: clock::Global, /// The range that the symbol encloses, e.g. for function symbol, this will /// include not only the signature, but also the body pub enclosing_range: Range, pub text: SharedString, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ContextSymbolId { pub path: ProjectPath, pub name: SharedString, pub range: Range, } /// Formats a collection of contexts into a string representation pub fn format_context_as_string<'a>( contexts: impl Iterator, cx: &App, ) -> Option { let mut file_context = Vec::new(); let mut directory_context = Vec::new(); let mut symbol_context = Vec::new(); let mut fetch_context = Vec::new(); let mut thread_context = Vec::new(); for context in contexts { match context { AssistantContext::File(context) => file_context.push(context), AssistantContext::Directory(context) => directory_context.push(context), AssistantContext::Symbol(context) => symbol_context.push(context), AssistantContext::FetchedUrl(context) => fetch_context.push(context), AssistantContext::Thread(context) => thread_context.push(context), } } if file_context.is_empty() && directory_context.is_empty() && symbol_context.is_empty() && fetch_context.is_empty() && thread_context.is_empty() { return None; } let mut result = String::new(); result.push_str("\n\n\ The following items were attached by the user. You don't need to use other tools to read them.\n\n"); if !file_context.is_empty() { result.push_str("\n"); for context in file_context { result.push_str(&context.context_buffer.text); } result.push_str("\n"); } if !directory_context.is_empty() { result.push_str("\n"); for context in directory_context { for context_buffer in &context.context_buffers { result.push_str(&context_buffer.text); } } result.push_str("\n"); } if !symbol_context.is_empty() { result.push_str("\n"); for context in symbol_context { result.push_str(&context.context_symbol.text); result.push('\n'); } result.push_str("\n"); } if !fetch_context.is_empty() { result.push_str("\n"); for context in &fetch_context { result.push_str(&context.url); result.push('\n'); result.push_str(&context.text); result.push('\n'); } result.push_str("\n"); } if !thread_context.is_empty() { result.push_str("\n"); for context in &thread_context { result.push_str(&context.summary(cx)); result.push('\n'); result.push_str(&context.text); result.push('\n'); } result.push_str("\n"); } result.push_str("\n"); Some(result) } pub fn attach_context_to_message<'a>( message: &mut LanguageModelRequestMessage, contexts: impl Iterator, cx: &App, ) { if let Some(context_string) = format_context_as_string(contexts, cx) { message.content.push(context_string.into()); } }