assistant2: Do not allow a context entry to be added multiple times (#22712)
https://github.com/user-attachments/assets/81674c88-031b-4d55-b362-43819492b93d Release Notes: - N/A
This commit is contained in:
parent
c74e5f5de2
commit
3a061a91e7
9 changed files with 293 additions and 173 deletions
|
@ -1,11 +1,8 @@
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use language_model::{LanguageModelRequestMessage, MessageContent};
|
use language_model::{LanguageModelRequestMessage, MessageContent};
|
||||||
use project::ProjectEntryId;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
|
|
||||||
use crate::thread::ThreadId;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||||
pub struct ContextId(pub(crate) usize);
|
pub struct ContextId(pub(crate) usize);
|
||||||
|
|
||||||
|
@ -26,10 +23,10 @@ pub struct Context {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum ContextKind {
|
pub enum ContextKind {
|
||||||
File(ProjectEntryId),
|
File,
|
||||||
Directory,
|
Directory,
|
||||||
FetchedUrl,
|
FetchedUrl,
|
||||||
Thread(ThreadId),
|
Thread,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn attach_context_to_message(
|
pub fn attach_context_to_message(
|
||||||
|
@ -43,7 +40,7 @@ pub fn attach_context_to_message(
|
||||||
|
|
||||||
for context in context.into_iter() {
|
for context in context.into_iter() {
|
||||||
match context.kind {
|
match context.kind {
|
||||||
ContextKind::File(_) => {
|
ContextKind::File => {
|
||||||
file_context.push_str(&context.text);
|
file_context.push_str(&context.text);
|
||||||
file_context.push('\n');
|
file_context.push('\n');
|
||||||
}
|
}
|
||||||
|
@ -57,7 +54,7 @@ pub fn attach_context_to_message(
|
||||||
fetch_context.push_str(&context.text);
|
fetch_context.push_str(&context.text);
|
||||||
fetch_context.push('\n');
|
fetch_context.push('\n');
|
||||||
}
|
}
|
||||||
ContextKind::Thread(_) => {
|
ContextKind::Thread => {
|
||||||
thread_context.push_str(&context.name);
|
thread_context.push_str(&context.name);
|
||||||
thread_context.push('\n');
|
thread_context.push('\n');
|
||||||
thread_context.push_str(&context.text);
|
thread_context.push_str(&context.text);
|
||||||
|
|
|
@ -14,6 +14,7 @@ use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::context::ContextKind;
|
||||||
use crate::context_picker::directory_context_picker::DirectoryContextPicker;
|
use crate::context_picker::directory_context_picker::DirectoryContextPicker;
|
||||||
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
||||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||||
|
@ -52,24 +53,24 @@ impl ContextPicker {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
entries.push(ContextPickerEntry {
|
entries.push(ContextPickerEntry {
|
||||||
name: "File".into(),
|
name: "File".into(),
|
||||||
kind: ContextPickerEntryKind::File,
|
kind: ContextKind::File,
|
||||||
icon: IconName::File,
|
icon: IconName::File,
|
||||||
});
|
});
|
||||||
entries.push(ContextPickerEntry {
|
entries.push(ContextPickerEntry {
|
||||||
name: "Folder".into(),
|
name: "Folder".into(),
|
||||||
kind: ContextPickerEntryKind::Directory,
|
kind: ContextKind::Directory,
|
||||||
icon: IconName::Folder,
|
icon: IconName::Folder,
|
||||||
});
|
});
|
||||||
entries.push(ContextPickerEntry {
|
entries.push(ContextPickerEntry {
|
||||||
name: "Fetch".into(),
|
name: "Fetch".into(),
|
||||||
kind: ContextPickerEntryKind::FetchedUrl,
|
kind: ContextKind::FetchedUrl,
|
||||||
icon: IconName::Globe,
|
icon: IconName::Globe,
|
||||||
});
|
});
|
||||||
|
|
||||||
if thread_store.is_some() {
|
if thread_store.is_some() {
|
||||||
entries.push(ContextPickerEntry {
|
entries.push(ContextPickerEntry {
|
||||||
name: "Thread".into(),
|
name: "Thread".into(),
|
||||||
kind: ContextPickerEntryKind::Thread,
|
kind: ContextKind::Thread,
|
||||||
icon: IconName::MessageCircle,
|
icon: IconName::MessageCircle,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -133,18 +134,10 @@ impl Render for ContextPicker {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct ContextPickerEntry {
|
struct ContextPickerEntry {
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
kind: ContextPickerEntryKind,
|
kind: ContextKind,
|
||||||
icon: IconName,
|
icon: IconName,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
enum ContextPickerEntryKind {
|
|
||||||
File,
|
|
||||||
Directory,
|
|
||||||
FetchedUrl,
|
|
||||||
Thread,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct ContextPickerDelegate {
|
pub(crate) struct ContextPickerDelegate {
|
||||||
context_picker: WeakView<ContextPicker>,
|
context_picker: WeakView<ContextPicker>,
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
|
@ -184,7 +177,7 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||||
self.context_picker
|
self.context_picker
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
match entry.kind {
|
match entry.kind {
|
||||||
ContextPickerEntryKind::File => {
|
ContextKind::File => {
|
||||||
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
|
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
|
||||||
FileContextPicker::new(
|
FileContextPicker::new(
|
||||||
self.context_picker.clone(),
|
self.context_picker.clone(),
|
||||||
|
@ -195,7 +188,7 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
ContextPickerEntryKind::Directory => {
|
ContextKind::Directory => {
|
||||||
this.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
|
this.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
|
||||||
DirectoryContextPicker::new(
|
DirectoryContextPicker::new(
|
||||||
self.context_picker.clone(),
|
self.context_picker.clone(),
|
||||||
|
@ -206,7 +199,7 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
ContextPickerEntryKind::FetchedUrl => {
|
ContextKind::FetchedUrl => {
|
||||||
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
|
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
|
||||||
FetchContextPicker::new(
|
FetchContextPicker::new(
|
||||||
self.context_picker.clone(),
|
self.context_picker.clone(),
|
||||||
|
@ -217,7 +210,7 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
ContextPickerEntryKind::Thread => {
|
ContextKind::Thread => {
|
||||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||||
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
|
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
|
||||||
ThreadContextPicker::new(
|
ThreadContextPicker::new(
|
||||||
|
|
|
@ -11,10 +11,8 @@ use ui::{prelude::*, ListItem};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::context::ContextKind;
|
|
||||||
use crate::context_picker::file_context_picker::codeblock_fence_for_path;
|
|
||||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||||
use crate::context_store::ContextStore;
|
use crate::context_store::{push_fenced_codeblock, ContextStore};
|
||||||
|
|
||||||
pub struct DirectoryContextPicker {
|
pub struct DirectoryContextPicker {
|
||||||
picker: View<Picker<DirectoryContextPickerDelegate>>,
|
picker: View<Picker<DirectoryContextPickerDelegate>>,
|
||||||
|
@ -189,6 +187,22 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let path = mat.path.clone();
|
let path = mat.path.clone();
|
||||||
|
|
||||||
|
if self
|
||||||
|
.context_store
|
||||||
|
.update(cx, |context_store, _cx| {
|
||||||
|
if let Some(context_id) = context_store.included_directory(&path) {
|
||||||
|
context_store.remove_context(&context_id);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||||
let confirm_behavior = self.confirm_behavior;
|
let confirm_behavior = self.confirm_behavior;
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
@ -235,23 +249,15 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
|
|
||||||
for buffer in buffers {
|
for buffer in buffers {
|
||||||
text.push_str(&codeblock_fence_for_path(Some(&path), None));
|
let buffer = buffer.read(cx);
|
||||||
text.push_str(&buffer.read(cx).text());
|
let path = buffer.file().map_or(&path, |file| file.path());
|
||||||
if !text.ends_with('\n') {
|
push_fenced_codeblock(&path, buffer.text(), &mut text);
|
||||||
text.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
text.push_str("```\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.delegate
|
this.delegate
|
||||||
.context_store
|
.context_store
|
||||||
.update(cx, |context_store, _cx| {
|
.update(cx, |context_store, _cx| {
|
||||||
context_store.insert_context(
|
context_store.insert_directory(&path, text);
|
||||||
ContextKind::Directory,
|
|
||||||
path.to_string_lossy().to_string(),
|
|
||||||
text,
|
|
||||||
);
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match confirm_behavior {
|
match confirm_behavior {
|
||||||
|
@ -280,16 +286,26 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
_cx: &mut ViewContext<Picker<Self>>,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let path_match = &self.matches[ix];
|
let path_match = &self.matches[ix];
|
||||||
let directory_name = path_match.path.to_string_lossy().to_string();
|
let directory_name = path_match.path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let added = self.context_store.upgrade().map_or(false, |context_store| {
|
||||||
|
context_store
|
||||||
|
.read(cx)
|
||||||
|
.included_directory(&path_match.path)
|
||||||
|
.is_some()
|
||||||
|
});
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(ix)
|
ListItem::new(ix)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.child(h_flex().gap_2().child(Label::new(directory_name))),
|
.child(h_flex().gap_2().child(Label::new(directory_name)))
|
||||||
|
.when(added, |el| {
|
||||||
|
el.end_slot(Label::new("Added").size(LabelSize::XSmall))
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ use picker::{Picker, PickerDelegate};
|
||||||
use ui::{prelude::*, ListItem, ViewContext};
|
use ui::{prelude::*, ListItem, ViewContext};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::context::ContextKind;
|
|
||||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||||
use crate::context_store::ContextStore;
|
use crate::context_store::ContextStore;
|
||||||
|
|
||||||
|
@ -201,7 +200,9 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||||
this.delegate
|
this.delegate
|
||||||
.context_store
|
.context_store
|
||||||
.update(cx, |context_store, _cx| {
|
.update(cx, |context_store, _cx| {
|
||||||
context_store.insert_context(ContextKind::FetchedUrl, url, text);
|
if context_store.included_url(&url).is_none() {
|
||||||
|
context_store.insert_fetched_url(url, text);
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match confirm_behavior {
|
match confirm_behavior {
|
||||||
|
@ -230,13 +231,22 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
_cx: &mut ViewContext<Picker<Self>>,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
|
let added = self.context_store.upgrade().map_or(false, |context_store| {
|
||||||
|
context_store.read(cx).included_url(&self.url).is_some()
|
||||||
|
});
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(ix)
|
ListItem::new(ix)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.child(Label::new(self.url.clone())),
|
.child(Label::new(self.url.clone()))
|
||||||
|
.when(added, |child| {
|
||||||
|
child
|
||||||
|
.disabled(true)
|
||||||
|
.end_slot(Label::new("Added").size(LabelSize::XSmall))
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::fmt::Write as _;
|
|
||||||
use std::ops::RangeInclusive;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -8,13 +6,12 @@ use fuzzy::PathMatch;
|
||||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
||||||
use ui::{prelude::*, ListItem};
|
use ui::{prelude::*, ListItem, Tooltip};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::context::ContextKind;
|
|
||||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||||
use crate::context_store::ContextStore;
|
use crate::context_store::{ContextStore, IncludedFile};
|
||||||
|
|
||||||
pub struct FileContextPicker {
|
pub struct FileContextPicker {
|
||||||
picker: View<Picker<FileContextPickerDelegate>>,
|
picker: View<Picker<FileContextPickerDelegate>>,
|
||||||
|
@ -204,20 +201,37 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let path = mat.path.clone();
|
let path = mat.path.clone();
|
||||||
|
|
||||||
|
if self
|
||||||
|
.context_store
|
||||||
|
.update(cx, |context_store, _cx| {
|
||||||
|
match context_store.included_file(&path) {
|
||||||
|
Some(IncludedFile::Direct(context_id)) => {
|
||||||
|
context_store.remove_context(&context_id);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Some(IncludedFile::InDirectory(_)) => true,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||||
let confirm_behavior = self.confirm_behavior;
|
let confirm_behavior = self.confirm_behavior;
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let Some((entry_id, open_buffer_task)) = project
|
let Some(open_buffer_task) = project
|
||||||
.update(&mut cx, |project, cx| {
|
.update(&mut cx, |project, cx| {
|
||||||
let project_path = ProjectPath {
|
let project_path = ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let entry_id = project.entry_for_path(&project_path, cx)?.id;
|
|
||||||
let task = project.open_buffer(project_path, cx);
|
let task = project.open_buffer(project_path, cx);
|
||||||
|
|
||||||
Some((entry_id, task))
|
Some(task)
|
||||||
})
|
})
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
|
@ -231,20 +245,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||||
this.delegate
|
this.delegate
|
||||||
.context_store
|
.context_store
|
||||||
.update(cx, |context_store, cx| {
|
.update(cx, |context_store, cx| {
|
||||||
let mut text = String::new();
|
context_store.insert_file(buffer.read(cx));
|
||||||
text.push_str(&codeblock_fence_for_path(Some(&path), None));
|
|
||||||
text.push_str(&buffer.read(cx).text());
|
|
||||||
if !text.ends_with('\n') {
|
|
||||||
text.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
text.push_str("```\n");
|
|
||||||
|
|
||||||
context_store.insert_context(
|
|
||||||
ContextKind::File(entry_id),
|
|
||||||
path.to_string_lossy().to_string(),
|
|
||||||
text,
|
|
||||||
);
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match confirm_behavior {
|
match confirm_behavior {
|
||||||
|
@ -273,7 +274,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
_cx: &mut ViewContext<Picker<Self>>,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let path_match = &self.matches[ix];
|
let path_match = &self.matches[ix];
|
||||||
|
|
||||||
|
@ -301,42 +302,36 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||||
(file_name, Some(directory))
|
(file_name, Some(directory))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let added = self
|
||||||
|
.context_store
|
||||||
|
.upgrade()
|
||||||
|
.and_then(|context_store| context_store.read(cx).included_file(&path_match.path));
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(ix).inset(true).toggle_state(selected).child(
|
ListItem::new(ix)
|
||||||
h_flex()
|
.inset(true)
|
||||||
.gap_2()
|
.toggle_state(selected)
|
||||||
.child(Label::new(file_name))
|
.child(
|
||||||
.children(directory.map(|directory| {
|
h_flex()
|
||||||
Label::new(directory)
|
.gap_2()
|
||||||
.size(LabelSize::Small)
|
.child(Label::new(file_name))
|
||||||
.color(Color::Muted)
|
.children(directory.map(|directory| {
|
||||||
})),
|
Label::new(directory)
|
||||||
),
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.when_some(added, |el, added| match added {
|
||||||
|
IncludedFile::Direct(_) => {
|
||||||
|
el.end_slot(Label::new("Added").size(LabelSize::XSmall))
|
||||||
|
}
|
||||||
|
IncludedFile::InDirectory(dir_name) => {
|
||||||
|
let dir_name = dir_name.to_string_lossy().into_owned();
|
||||||
|
|
||||||
|
el.end_slot(Label::new("Included").size(LabelSize::XSmall))
|
||||||
|
.tooltip(move |cx| Tooltip::text(format!("in {dir_name}"), cx))
|
||||||
|
}
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn codeblock_fence_for_path(
|
|
||||||
path: Option<&Path>,
|
|
||||||
row_range: Option<RangeInclusive<u32>>,
|
|
||||||
) -> String {
|
|
||||||
let mut text = String::new();
|
|
||||||
write!(text, "```").unwrap();
|
|
||||||
|
|
||||||
if let Some(path) = path {
|
|
||||||
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
|
|
||||||
write!(text, "{} ", extension).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(text, "{}", path.display()).unwrap();
|
|
||||||
} else {
|
|
||||||
write!(text, "untitled").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(row_range) = row_range {
|
|
||||||
write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
text.push('\n');
|
|
||||||
text
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, Wea
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use ui::{prelude::*, ListItem};
|
use ui::{prelude::*, ListItem};
|
||||||
|
|
||||||
use crate::context::ContextKind;
|
|
||||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||||
use crate::context_store;
|
use crate::context_store;
|
||||||
use crate::thread::ThreadId;
|
use crate::thread::ThreadId;
|
||||||
|
@ -169,11 +168,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||||
|
|
||||||
self.context_store
|
self.context_store
|
||||||
.update(cx, |context_store, cx| {
|
.update(cx, |context_store, cx| {
|
||||||
context_store.insert_context(
|
if let Some(context_id) = context_store.included_thread(&entry.id) {
|
||||||
ContextKind::Thread(thread.read(cx).id().clone()),
|
context_store.remove_context(&context_id);
|
||||||
entry.summary.clone(),
|
} else {
|
||||||
thread.read(cx).text(),
|
context_store.insert_thread(thread.read(cx));
|
||||||
);
|
}
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
|
@ -196,15 +195,22 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
_cx: &mut ViewContext<Picker<Self>>,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let thread = &self.matches[ix];
|
let thread = &self.matches[ix];
|
||||||
|
|
||||||
|
let added = self.context_store.upgrade().map_or(false, |ctx_store| {
|
||||||
|
ctx_store.read(cx).included_thread(&thread.id).is_some()
|
||||||
|
});
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
ListItem::new(ix)
|
ListItem::new(ix)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.toggle_state(selected)
|
.toggle_state(selected)
|
||||||
.child(Label::new(thread.summary.clone())),
|
.child(Label::new(thread.summary.clone()))
|
||||||
|
.when(added, |el| {
|
||||||
|
el.end_slot(Label::new("Added").size(LabelSize::XSmall))
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
use gpui::SharedString;
|
use std::fmt::Write as _;
|
||||||
use project::ProjectEntryId;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use collections::HashMap;
|
||||||
|
use gpui::SharedString;
|
||||||
|
use language::Buffer;
|
||||||
|
|
||||||
|
use crate::thread::Thread;
|
||||||
use crate::{
|
use crate::{
|
||||||
context::{Context, ContextId, ContextKind},
|
context::{Context, ContextId, ContextKind},
|
||||||
thread::ThreadId,
|
thread::ThreadId,
|
||||||
|
@ -9,6 +14,10 @@ use crate::{
|
||||||
pub struct ContextStore {
|
pub struct ContextStore {
|
||||||
context: Vec<Context>,
|
context: Vec<Context>,
|
||||||
next_context_id: ContextId,
|
next_context_id: ContextId,
|
||||||
|
files: HashMap<PathBuf, ContextId>,
|
||||||
|
directories: HashMap<PathBuf, ContextId>,
|
||||||
|
threads: HashMap<ThreadId, ContextId>,
|
||||||
|
fetched_urls: HashMap<String, ContextId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextStore {
|
impl ContextStore {
|
||||||
|
@ -16,6 +25,10 @@ impl ContextStore {
|
||||||
Self {
|
Self {
|
||||||
context: Vec::new(),
|
context: Vec::new(),
|
||||||
next_context_id: ContextId(0),
|
next_context_id: ContextId(0),
|
||||||
|
files: HashMap::default(),
|
||||||
|
directories: HashMap::default(),
|
||||||
|
threads: HashMap::default(),
|
||||||
|
fetched_urls: HashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,42 +37,154 @@ impl ContextStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn drain(&mut self) -> Vec<Context> {
|
pub fn drain(&mut self) -> Vec<Context> {
|
||||||
|
self.files.clear();
|
||||||
|
self.directories.clear();
|
||||||
self.context.drain(..).collect()
|
self.context.drain(..).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.context.clear();
|
self.context.clear();
|
||||||
|
self.files.clear();
|
||||||
|
self.directories.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_context(
|
pub fn insert_file(&mut self, buffer: &Buffer) {
|
||||||
&mut self,
|
let Some(file) = buffer.file() else {
|
||||||
kind: ContextKind,
|
return;
|
||||||
name: impl Into<SharedString>,
|
};
|
||||||
text: impl Into<SharedString>,
|
|
||||||
) {
|
let path = file.path();
|
||||||
|
|
||||||
|
let id = self.next_context_id.post_inc();
|
||||||
|
self.files.insert(path.to_path_buf(), id);
|
||||||
|
|
||||||
|
let name = path.to_string_lossy().into_owned().into();
|
||||||
|
|
||||||
|
let mut text = String::new();
|
||||||
|
push_fenced_codeblock(path, buffer.text(), &mut text);
|
||||||
|
|
||||||
self.context.push(Context {
|
self.context.push(Context {
|
||||||
id: self.next_context_id.post_inc(),
|
id,
|
||||||
name: name.into(),
|
name,
|
||||||
kind,
|
kind: ContextKind::File,
|
||||||
|
text: text.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_directory(&mut self, path: &Path, text: impl Into<SharedString>) {
|
||||||
|
let id = self.next_context_id.post_inc();
|
||||||
|
self.directories.insert(path.to_path_buf(), id);
|
||||||
|
|
||||||
|
let name = path.to_string_lossy().into_owned().into();
|
||||||
|
|
||||||
|
self.context.push(Context {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
kind: ContextKind::Directory,
|
||||||
|
text: text.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_thread(&mut self, thread: &Thread) {
|
||||||
|
let context_id = self.next_context_id.post_inc();
|
||||||
|
self.threads.insert(thread.id().clone(), context_id);
|
||||||
|
|
||||||
|
self.context.push(Context {
|
||||||
|
id: context_id,
|
||||||
|
name: thread.summary().unwrap_or("New thread".into()),
|
||||||
|
kind: ContextKind::Thread,
|
||||||
|
text: thread.text().into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
|
||||||
|
let context_id = self.next_context_id.post_inc();
|
||||||
|
self.fetched_urls.insert(url.clone(), context_id);
|
||||||
|
|
||||||
|
self.context.push(Context {
|
||||||
|
id: context_id,
|
||||||
|
name: url.into(),
|
||||||
|
kind: ContextKind::FetchedUrl,
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_context(&mut self, id: &ContextId) {
|
pub fn remove_context(&mut self, id: &ContextId) {
|
||||||
self.context.retain(|context| context.id != *id);
|
let Some(ix) = self.context.iter().position(|c| c.id == *id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.context.remove(ix).kind {
|
||||||
|
ContextKind::File => {
|
||||||
|
self.files.retain(|_, p_id| p_id != id);
|
||||||
|
}
|
||||||
|
ContextKind::Directory => {
|
||||||
|
self.directories.retain(|_, p_id| p_id != id);
|
||||||
|
}
|
||||||
|
ContextKind::FetchedUrl => {
|
||||||
|
self.fetched_urls.retain(|_, p_id| p_id != id);
|
||||||
|
}
|
||||||
|
ContextKind::Thread => {
|
||||||
|
self.threads.retain(|_, p_id| p_id != id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains_project_entry(&self, entry_id: ProjectEntryId) -> bool {
|
pub fn included_file(&self, path: &Path) -> Option<IncludedFile> {
|
||||||
self.context.iter().any(|probe| match probe.kind {
|
if let Some(id) = self.files.get(path) {
|
||||||
ContextKind::File(probe_entry_id) => probe_entry_id == entry_id,
|
return Some(IncludedFile::Direct(*id));
|
||||||
ContextKind::Directory | ContextKind::FetchedUrl | ContextKind::Thread(_) => false,
|
}
|
||||||
})
|
|
||||||
|
if self.directories.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = path.to_path_buf();
|
||||||
|
|
||||||
|
while buf.pop() {
|
||||||
|
if let Some(_) = self.directories.get(&buf) {
|
||||||
|
return Some(IncludedFile::InDirectory(buf));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains_thread(&self, thread_id: &ThreadId) -> bool {
|
pub fn included_directory(&self, path: &Path) -> Option<ContextId> {
|
||||||
self.context.iter().any(|probe| match probe.kind {
|
self.directories.get(path).copied()
|
||||||
ContextKind::Thread(ref probe_thread_id) => probe_thread_id == thread_id,
|
}
|
||||||
ContextKind::File(_) | ContextKind::Directory | ContextKind::FetchedUrl => false,
|
|
||||||
})
|
pub fn included_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
|
||||||
|
self.threads.get(thread_id).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn included_url(&self, url: &str) -> Option<ContextId> {
|
||||||
|
self.fetched_urls.get(url).copied()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum IncludedFile {
|
||||||
|
Direct(ContextId),
|
||||||
|
InDirectory(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn push_fenced_codeblock(path: &Path, content: String, buf: &mut String) {
|
||||||
|
buf.reserve(content.len() + 64);
|
||||||
|
|
||||||
|
write!(buf, "```").unwrap();
|
||||||
|
|
||||||
|
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
|
||||||
|
write!(buf, "{} ", extension).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(buf, "{}", path.display()).unwrap();
|
||||||
|
|
||||||
|
buf.push('\n');
|
||||||
|
buf.push_str(&content);
|
||||||
|
|
||||||
|
if !buf.ends_with('\n') {
|
||||||
|
buf.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.push_str("```\n");
|
||||||
|
}
|
||||||
|
|
|
@ -3,14 +3,12 @@ use std::rc::Rc;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{AppContext, FocusHandle, Model, View, WeakModel, WeakView};
|
use gpui::{AppContext, FocusHandle, Model, View, WeakModel, WeakView};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use project::ProjectEntryId;
|
|
||||||
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
|
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::context::ContextKind;
|
|
||||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||||
use crate::context_store::ContextStore;
|
use crate::context_store::ContextStore;
|
||||||
use crate::thread::{Thread, ThreadId};
|
use crate::thread::Thread;
|
||||||
use crate::thread_store::ThreadStore;
|
use crate::thread_store::ThreadStore;
|
||||||
use crate::ui::ContextPill;
|
use crate::ui::ContextPill;
|
||||||
use crate::{AssistantPanel, ToggleContextPicker};
|
use crate::{AssistantPanel, ToggleContextPicker};
|
||||||
|
@ -62,20 +60,19 @@ impl ContextStrip {
|
||||||
fn suggested_file(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
|
fn suggested_file(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
|
||||||
let workspace = self.workspace.upgrade()?;
|
let workspace = self.workspace.upgrade()?;
|
||||||
let active_item = workspace.read(cx).active_item(cx)?;
|
let active_item = workspace.read(cx).active_item(cx)?;
|
||||||
let entry_id = *active_item.project_entry_ids(cx).first()?;
|
|
||||||
|
|
||||||
if self.context_store.read(cx).contains_project_entry(entry_id) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
||||||
let active_buffer = editor.buffer().read(cx).as_singleton()?;
|
let active_buffer = editor.buffer().read(cx).as_singleton()?;
|
||||||
|
|
||||||
let file = active_buffer.read(cx).file()?;
|
let path = active_buffer.read(cx).file()?.path();
|
||||||
let title = file.path().to_string_lossy().into_owned().into();
|
|
||||||
|
if self.context_store.read(cx).included_file(path).is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = path.to_string_lossy().into_owned().into();
|
||||||
|
|
||||||
Some(SuggestedContext::File {
|
Some(SuggestedContext::File {
|
||||||
entry_id,
|
|
||||||
title,
|
title,
|
||||||
buffer: active_buffer.downgrade(),
|
buffer: active_buffer.downgrade(),
|
||||||
})
|
})
|
||||||
|
@ -95,13 +92,13 @@ impl ContextStrip {
|
||||||
if self
|
if self
|
||||||
.context_store
|
.context_store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.contains_thread(active_thread.id())
|
.included_thread(active_thread.id())
|
||||||
|
.is_some()
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(SuggestedContext::Thread {
|
Some(SuggestedContext::Thread {
|
||||||
id: active_thread.id().clone(),
|
|
||||||
title: active_thread.summary().unwrap_or("Active Thread".into()),
|
title: active_thread.summary().unwrap_or("Active Thread".into()),
|
||||||
thread: weak_active_thread,
|
thread: weak_active_thread,
|
||||||
})
|
})
|
||||||
|
@ -230,12 +227,10 @@ pub enum SuggestContextKind {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum SuggestedContext {
|
pub enum SuggestedContext {
|
||||||
File {
|
File {
|
||||||
entry_id: ProjectEntryId,
|
|
||||||
title: SharedString,
|
title: SharedString,
|
||||||
buffer: WeakModel<Buffer>,
|
buffer: WeakModel<Buffer>,
|
||||||
},
|
},
|
||||||
Thread {
|
Thread {
|
||||||
id: ThreadId,
|
|
||||||
title: SharedString,
|
title: SharedString,
|
||||||
thread: WeakModel<Thread>,
|
thread: WeakModel<Thread>,
|
||||||
},
|
},
|
||||||
|
@ -251,32 +246,15 @@ impl SuggestedContext {
|
||||||
|
|
||||||
pub fn accept(&self, context_store: &mut ContextStore, cx: &mut AppContext) {
|
pub fn accept(&self, context_store: &mut ContextStore, cx: &mut AppContext) {
|
||||||
match self {
|
match self {
|
||||||
Self::File {
|
Self::File { buffer, title: _ } => {
|
||||||
entry_id,
|
if let Some(buffer) = buffer.upgrade() {
|
||||||
title,
|
context_store.insert_file(buffer.read(cx));
|
||||||
buffer,
|
|
||||||
} => {
|
|
||||||
let Some(buffer) = buffer.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
let text = buffer.read(cx).text();
|
|
||||||
|
|
||||||
context_store.insert_context(
|
|
||||||
ContextKind::File(*entry_id),
|
|
||||||
title.clone(),
|
|
||||||
text.clone(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Self::Thread { id, title, thread } => {
|
Self::Thread { thread, title: _ } => {
|
||||||
let Some(thread) = thread.upgrade() else {
|
if let Some(thread) = thread.upgrade() {
|
||||||
return;
|
context_store.insert_thread(thread.read(cx));
|
||||||
};
|
};
|
||||||
|
|
||||||
context_store.insert_context(
|
|
||||||
ContextKind::Thread(id.clone()),
|
|
||||||
title.clone(),
|
|
||||||
thread.read(cx).text(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,10 +33,10 @@ impl RenderOnce for ContextPill {
|
||||||
px(4.)
|
px(4.)
|
||||||
};
|
};
|
||||||
let icon = match self.context.kind {
|
let icon = match self.context.kind {
|
||||||
ContextKind::File(_) => IconName::File,
|
ContextKind::File => IconName::File,
|
||||||
ContextKind::Directory => IconName::Folder,
|
ContextKind::Directory => IconName::Folder,
|
||||||
ContextKind::FetchedUrl => IconName::Globe,
|
ContextKind::FetchedUrl => IconName::Globe,
|
||||||
ContextKind::Thread(_) => IconName::MessageCircle,
|
ContextKind::Thread => IconName::MessageCircle,
|
||||||
};
|
};
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue