
There's still a bit more work to do on this, but this PR is compiling (with warnings) after eliminating the key types. When the tasks below are complete, this will be the new narrative for GPUI: - `Entity<T>` - This replaces `View<T>`/`Model<T>`. It represents a unit of state, and if `T` implements `Render`, then `Entity<T>` implements `Element`. - `&mut App` This replaces `AppContext` and represents the app. - `&mut Context<T>` This replaces `ModelContext` and derefs to `App`. It is provided by the framework when updating an entity. - `&mut Window` Broken out of `&mut WindowContext` which no longer exists. Every method that once took `&mut WindowContext` now takes `&mut Window, &mut App` and every method that took `&mut ViewContext<T>` now takes `&mut Window, &mut Context<T>` Not pictured here are the two other failed attempts. It's been quite a month! Tasks: - [x] Remove `View`, `ViewContext`, `WindowContext` and thread through `Window` - [x] [@cole-miller @mikayla-maki] Redraw window when entities change - [x] [@cole-miller @mikayla-maki] Get examples and Zed running - [x] [@cole-miller @mikayla-maki] Fix Zed rendering - [x] [@mikayla-maki] Fix todo! macros and comments - [x] Fix a bug where the editor would not be redrawn because of view caching - [x] remove publicness window.notify() and replace with `AppContext::notify` - [x] remove `observe_new_window_models`, replace with `observe_new_models` with an optional window - [x] Fix a bug where the project panel would not be redrawn because of the wrong refresh() call being used - [x] Fix the tests - [x] Fix warnings by eliminating `Window` params or using `_` - [x] Fix conflicts - [x] Simplify generic code where possible - [x] Rename types - [ ] Update docs ### issues post merge - [x] Issues switching between normal and insert mode - [x] Assistant re-rendering failure - [x] Vim test failures - [x] Mac build issue Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Mikayla <mikayla@zed.dev> Co-authored-by: Joseph <joseph@zed.dev> Co-authored-by: max <max@zed.dev> Co-authored-by: Michael Sloan <michael@zed.dev> Co-authored-by: Mikayla Maki <mikaylamaki@Mikaylas-MacBook-Pro.local> Co-authored-by: Mikayla <mikayla.c.maki@gmail.com> Co-authored-by: joão <joao@zed.dev>
325 lines
9 KiB
Rust
325 lines
9 KiB
Rust
use std::path::Path;
|
|
use std::rc::Rc;
|
|
|
|
use file_icons::FileIcons;
|
|
use gpui::{App, Entity, SharedString};
|
|
use language::Buffer;
|
|
use language_model::{LanguageModelRequestMessage, MessageContent};
|
|
use serde::{Deserialize, Serialize};
|
|
use text::BufferId;
|
|
use ui::IconName;
|
|
use util::post_inc;
|
|
|
|
use crate::{context_store::buffer_path_log_err, 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))
|
|
}
|
|
}
|
|
|
|
/// Some context attached to a message in a thread.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ContextSnapshot {
|
|
pub id: ContextId,
|
|
pub name: SharedString,
|
|
pub parent: Option<SharedString>,
|
|
pub tooltip: Option<SharedString>,
|
|
pub icon_path: Option<SharedString>,
|
|
pub kind: ContextKind,
|
|
/// Joining these strings separated by \n yields text for model. Not refreshed by `snapshot`.
|
|
pub text: Box<[SharedString]>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ContextKind {
|
|
File,
|
|
Directory,
|
|
FetchedUrl,
|
|
Thread,
|
|
}
|
|
|
|
impl ContextKind {
|
|
pub fn label(&self) -> &'static str {
|
|
match self {
|
|
ContextKind::File => "File",
|
|
ContextKind::Directory => "Folder",
|
|
ContextKind::FetchedUrl => "Fetch",
|
|
ContextKind::Thread => "Thread",
|
|
}
|
|
}
|
|
|
|
pub fn icon(&self) -> IconName {
|
|
match self {
|
|
ContextKind::File => IconName::File,
|
|
ContextKind::Directory => IconName::Folder,
|
|
ContextKind::FetchedUrl => IconName::Globe,
|
|
ContextKind::Thread => IconName::MessageCircle,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum AssistantContext {
|
|
File(FileContext),
|
|
Directory(DirectoryContext),
|
|
FetchedUrl(FetchedUrlContext),
|
|
Thread(ThreadContext),
|
|
}
|
|
|
|
impl AssistantContext {
|
|
pub fn id(&self) -> ContextId {
|
|
match self {
|
|
Self::File(file) => file.id,
|
|
Self::Directory(directory) => directory.snapshot.id,
|
|
Self::FetchedUrl(url) => url.id,
|
|
Self::Thread(thread) => thread.id,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct FileContext {
|
|
pub id: ContextId,
|
|
pub context_buffer: ContextBuffer,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct DirectoryContext {
|
|
pub path: Rc<Path>,
|
|
pub context_buffers: Vec<ContextBuffer>,
|
|
pub snapshot: ContextSnapshot,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct FetchedUrlContext {
|
|
pub id: ContextId,
|
|
pub url: SharedString,
|
|
pub text: SharedString,
|
|
}
|
|
|
|
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
|
|
// explicitly or have a WeakModel<Thread> and remove during snapshot.
|
|
|
|
#[derive(Debug)]
|
|
pub struct ThreadContext {
|
|
pub id: ContextId,
|
|
pub thread: Entity<Thread>,
|
|
pub text: SharedString,
|
|
}
|
|
|
|
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
|
|
// the context from the message editor in this case.
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ContextBuffer {
|
|
pub id: BufferId,
|
|
pub buffer: Entity<Buffer>,
|
|
pub version: clock::Global,
|
|
pub text: SharedString,
|
|
}
|
|
|
|
impl AssistantContext {
|
|
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
|
|
match &self {
|
|
Self::File(file_context) => file_context.snapshot(cx),
|
|
Self::Directory(directory_context) => Some(directory_context.snapshot()),
|
|
Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
|
|
Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FileContext {
|
|
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
|
|
let buffer = self.context_buffer.buffer.read(cx);
|
|
let path = buffer_path_log_err(buffer)?;
|
|
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
|
let name = match path.file_name() {
|
|
Some(name) => name.to_string_lossy().into_owned().into(),
|
|
None => full_path.clone(),
|
|
};
|
|
let parent = path
|
|
.parent()
|
|
.and_then(|p| p.file_name())
|
|
.map(|p| p.to_string_lossy().into_owned().into());
|
|
|
|
let icon_path = FileIcons::get_icon(&path, cx);
|
|
|
|
Some(ContextSnapshot {
|
|
id: self.id,
|
|
name,
|
|
parent,
|
|
tooltip: Some(full_path),
|
|
icon_path,
|
|
kind: ContextKind::File,
|
|
text: Box::new([self.context_buffer.text.clone()]),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl DirectoryContext {
|
|
pub fn new(
|
|
id: ContextId,
|
|
path: &Path,
|
|
context_buffers: Vec<ContextBuffer>,
|
|
) -> DirectoryContext {
|
|
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
|
|
|
let name = match path.file_name() {
|
|
Some(name) => name.to_string_lossy().into_owned().into(),
|
|
None => full_path.clone(),
|
|
};
|
|
|
|
let parent = path
|
|
.parent()
|
|
.and_then(|p| p.file_name())
|
|
.map(|p| p.to_string_lossy().into_owned().into());
|
|
|
|
// TODO: include directory path in text?
|
|
let text = context_buffers
|
|
.iter()
|
|
.map(|b| b.text.clone())
|
|
.collect::<Vec<_>>()
|
|
.into();
|
|
|
|
DirectoryContext {
|
|
path: path.into(),
|
|
context_buffers,
|
|
snapshot: ContextSnapshot {
|
|
id,
|
|
name,
|
|
parent,
|
|
tooltip: Some(full_path),
|
|
icon_path: None,
|
|
kind: ContextKind::Directory,
|
|
text,
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn snapshot(&self) -> ContextSnapshot {
|
|
self.snapshot.clone()
|
|
}
|
|
}
|
|
|
|
impl FetchedUrlContext {
|
|
pub fn snapshot(&self) -> ContextSnapshot {
|
|
ContextSnapshot {
|
|
id: self.id,
|
|
name: self.url.clone(),
|
|
parent: None,
|
|
tooltip: None,
|
|
icon_path: None,
|
|
kind: ContextKind::FetchedUrl,
|
|
text: Box::new([self.text.clone()]),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ThreadContext {
|
|
pub fn snapshot(&self, cx: &App) -> ContextSnapshot {
|
|
let thread = self.thread.read(cx);
|
|
ContextSnapshot {
|
|
id: self.id,
|
|
name: thread.summary().unwrap_or("New thread".into()),
|
|
parent: None,
|
|
tooltip: None,
|
|
icon_path: None,
|
|
kind: ContextKind::Thread,
|
|
text: Box::new([self.text.clone()]),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn attach_context_to_message(
|
|
message: &mut LanguageModelRequestMessage,
|
|
contexts: impl Iterator<Item = ContextSnapshot>,
|
|
) {
|
|
let mut file_context = Vec::new();
|
|
let mut directory_context = Vec::new();
|
|
let mut fetch_context = Vec::new();
|
|
let mut thread_context = Vec::new();
|
|
|
|
let mut capacity = 0;
|
|
for context in contexts {
|
|
capacity += context.text.len();
|
|
match context.kind {
|
|
ContextKind::File => file_context.push(context),
|
|
ContextKind::Directory => directory_context.push(context),
|
|
ContextKind::FetchedUrl => fetch_context.push(context),
|
|
ContextKind::Thread => thread_context.push(context),
|
|
}
|
|
}
|
|
if !file_context.is_empty() {
|
|
capacity += 1;
|
|
}
|
|
if !directory_context.is_empty() {
|
|
capacity += 1;
|
|
}
|
|
if !fetch_context.is_empty() {
|
|
capacity += 1 + fetch_context.len();
|
|
}
|
|
if !thread_context.is_empty() {
|
|
capacity += 1 + thread_context.len();
|
|
}
|
|
if capacity == 0 {
|
|
return;
|
|
}
|
|
|
|
let mut context_chunks = Vec::with_capacity(capacity);
|
|
|
|
if !file_context.is_empty() {
|
|
context_chunks.push("The following files are available:\n");
|
|
for context in &file_context {
|
|
for chunk in &context.text {
|
|
context_chunks.push(&chunk);
|
|
}
|
|
}
|
|
}
|
|
|
|
if !directory_context.is_empty() {
|
|
context_chunks.push("The following directories are available:\n");
|
|
for context in &directory_context {
|
|
for chunk in &context.text {
|
|
context_chunks.push(&chunk);
|
|
}
|
|
}
|
|
}
|
|
|
|
if !fetch_context.is_empty() {
|
|
context_chunks.push("The following fetched results are available:\n");
|
|
for context in &fetch_context {
|
|
context_chunks.push(&context.name);
|
|
for chunk in &context.text {
|
|
context_chunks.push(&chunk);
|
|
}
|
|
}
|
|
}
|
|
|
|
if !thread_context.is_empty() {
|
|
context_chunks.push("The following previous conversation threads are available:\n");
|
|
for context in &thread_context {
|
|
context_chunks.push(&context.name);
|
|
for chunk in &context.text {
|
|
context_chunks.push(&chunk);
|
|
}
|
|
}
|
|
}
|
|
|
|
debug_assert!(
|
|
context_chunks.len() == capacity,
|
|
"attach_context_message calculated capacity of {}, but length was {}",
|
|
capacity,
|
|
context_chunks.len()
|
|
);
|
|
|
|
if !context_chunks.is_empty() {
|
|
message
|
|
.content
|
|
.push(MessageContent::Text(context_chunks.join("\n")));
|
|
}
|
|
}
|