agent: Support inserting selections as context via @selection
(#29045)
WIP Release Notes: - N/A
This commit is contained in:
parent
10ded0ab75
commit
a5852d4537
10 changed files with 548 additions and 191 deletions
|
@ -670,6 +670,26 @@ fn open_markdown_link(
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
Some(MentionLink::Selection(path, line_range)) => {
|
||||||
|
let open_task = workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.open_path(path, None, true, window, cx)
|
||||||
|
});
|
||||||
|
window
|
||||||
|
.spawn(cx, async move |cx| {
|
||||||
|
let active_editor = open_task
|
||||||
|
.await?
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.context("Item is not an editor")?;
|
||||||
|
active_editor.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
|
||||||
|
s.select_ranges([Point::new(line_range.start as u32, 0)
|
||||||
|
..Point::new(line_range.start as u32, 0)])
|
||||||
|
});
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
Some(MentionLink::Thread(thread_id)) => workspace.update(cx, |workspace, cx| {
|
Some(MentionLink::Thread(thread_id)) => workspace.update(cx, |workspace, cx| {
|
||||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
|
@ -3309,15 +3329,15 @@ pub(crate) fn open_context(
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AssistantContext::Excerpt(excerpt_context) => {
|
AssistantContext::Selection(selection_context) => {
|
||||||
if let Some(project_path) = excerpt_context
|
if let Some(project_path) = selection_context
|
||||||
.context_buffer
|
.context_buffer
|
||||||
.buffer
|
.buffer
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.project_path(cx)
|
.project_path(cx)
|
||||||
{
|
{
|
||||||
let snapshot = excerpt_context.context_buffer.buffer.read(cx).snapshot();
|
let snapshot = selection_context.context_buffer.buffer.read(cx).snapshot();
|
||||||
let target_position = excerpt_context.range.start.to_point(&snapshot);
|
let target_position = selection_context.range.start.to_point(&snapshot);
|
||||||
|
|
||||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
|
@ -1951,7 +1951,9 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for (buffer, range) in selection_ranges {
|
for (buffer, range) in selection_ranges {
|
||||||
store.add_excerpt(range, buffer, cx).detach_and_log_err(cx);
|
store
|
||||||
|
.add_selection(buffer, range, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -33,7 +33,7 @@ pub enum ContextKind {
|
||||||
File,
|
File,
|
||||||
Directory,
|
Directory,
|
||||||
Symbol,
|
Symbol,
|
||||||
Excerpt,
|
Selection,
|
||||||
FetchedUrl,
|
FetchedUrl,
|
||||||
Thread,
|
Thread,
|
||||||
Rules,
|
Rules,
|
||||||
|
@ -46,7 +46,7 @@ impl ContextKind {
|
||||||
ContextKind::File => IconName::File,
|
ContextKind::File => IconName::File,
|
||||||
ContextKind::Directory => IconName::Folder,
|
ContextKind::Directory => IconName::Folder,
|
||||||
ContextKind::Symbol => IconName::Code,
|
ContextKind::Symbol => IconName::Code,
|
||||||
ContextKind::Excerpt => IconName::Code,
|
ContextKind::Selection => IconName::Context,
|
||||||
ContextKind::FetchedUrl => IconName::Globe,
|
ContextKind::FetchedUrl => IconName::Globe,
|
||||||
ContextKind::Thread => IconName::MessageBubbles,
|
ContextKind::Thread => IconName::MessageBubbles,
|
||||||
ContextKind::Rules => RULES_ICON,
|
ContextKind::Rules => RULES_ICON,
|
||||||
|
@ -62,7 +62,7 @@ pub enum AssistantContext {
|
||||||
Symbol(SymbolContext),
|
Symbol(SymbolContext),
|
||||||
FetchedUrl(FetchedUrlContext),
|
FetchedUrl(FetchedUrlContext),
|
||||||
Thread(ThreadContext),
|
Thread(ThreadContext),
|
||||||
Excerpt(ExcerptContext),
|
Selection(SelectionContext),
|
||||||
Rules(RulesContext),
|
Rules(RulesContext),
|
||||||
Image(ImageContext),
|
Image(ImageContext),
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ impl AssistantContext {
|
||||||
Self::Symbol(symbol) => symbol.id,
|
Self::Symbol(symbol) => symbol.id,
|
||||||
Self::FetchedUrl(url) => url.id,
|
Self::FetchedUrl(url) => url.id,
|
||||||
Self::Thread(thread) => thread.id,
|
Self::Thread(thread) => thread.id,
|
||||||
Self::Excerpt(excerpt) => excerpt.id,
|
Self::Selection(selection) => selection.id,
|
||||||
Self::Rules(rules) => rules.id,
|
Self::Rules(rules) => rules.id,
|
||||||
Self::Image(image) => image.id,
|
Self::Image(image) => image.id,
|
||||||
}
|
}
|
||||||
|
@ -220,7 +220,7 @@ pub struct ContextSymbolId {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ExcerptContext {
|
pub struct SelectionContext {
|
||||||
pub id: ContextId,
|
pub id: ContextId,
|
||||||
pub range: Range<Anchor>,
|
pub range: Range<Anchor>,
|
||||||
pub line_range: Range<Point>,
|
pub line_range: Range<Point>,
|
||||||
|
@ -243,7 +243,7 @@ pub fn format_context_as_string<'a>(
|
||||||
let mut file_context = Vec::new();
|
let mut file_context = Vec::new();
|
||||||
let mut directory_context = Vec::new();
|
let mut directory_context = Vec::new();
|
||||||
let mut symbol_context = Vec::new();
|
let mut symbol_context = Vec::new();
|
||||||
let mut excerpt_context = Vec::new();
|
let mut selection_context = Vec::new();
|
||||||
let mut fetch_context = Vec::new();
|
let mut fetch_context = Vec::new();
|
||||||
let mut thread_context = Vec::new();
|
let mut thread_context = Vec::new();
|
||||||
let mut rules_context = Vec::new();
|
let mut rules_context = Vec::new();
|
||||||
|
@ -253,7 +253,7 @@ pub fn format_context_as_string<'a>(
|
||||||
AssistantContext::File(context) => file_context.push(context),
|
AssistantContext::File(context) => file_context.push(context),
|
||||||
AssistantContext::Directory(context) => directory_context.push(context),
|
AssistantContext::Directory(context) => directory_context.push(context),
|
||||||
AssistantContext::Symbol(context) => symbol_context.push(context),
|
AssistantContext::Symbol(context) => symbol_context.push(context),
|
||||||
AssistantContext::Excerpt(context) => excerpt_context.push(context),
|
AssistantContext::Selection(context) => selection_context.push(context),
|
||||||
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
|
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
|
||||||
AssistantContext::Thread(context) => thread_context.push(context),
|
AssistantContext::Thread(context) => thread_context.push(context),
|
||||||
AssistantContext::Rules(context) => rules_context.push(context),
|
AssistantContext::Rules(context) => rules_context.push(context),
|
||||||
|
@ -264,7 +264,7 @@ pub fn format_context_as_string<'a>(
|
||||||
if file_context.is_empty()
|
if file_context.is_empty()
|
||||||
&& directory_context.is_empty()
|
&& directory_context.is_empty()
|
||||||
&& symbol_context.is_empty()
|
&& symbol_context.is_empty()
|
||||||
&& excerpt_context.is_empty()
|
&& selection_context.is_empty()
|
||||||
&& fetch_context.is_empty()
|
&& fetch_context.is_empty()
|
||||||
&& thread_context.is_empty()
|
&& thread_context.is_empty()
|
||||||
&& rules_context.is_empty()
|
&& rules_context.is_empty()
|
||||||
|
@ -303,13 +303,13 @@ pub fn format_context_as_string<'a>(
|
||||||
result.push_str("</symbols>\n");
|
result.push_str("</symbols>\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !excerpt_context.is_empty() {
|
if !selection_context.is_empty() {
|
||||||
result.push_str("<excerpts>\n");
|
result.push_str("<selections>\n");
|
||||||
for context in excerpt_context {
|
for context in selection_context {
|
||||||
result.push_str(&context.context_buffer.text);
|
result.push_str(&context.context_buffer.text);
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
}
|
}
|
||||||
result.push_str("</excerpts>\n");
|
result.push_str("</selections>\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fetch_context.is_empty() {
|
if !fetch_context.is_empty() {
|
||||||
|
|
|
@ -17,6 +17,7 @@ use gpui::{
|
||||||
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
|
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
|
||||||
WeakEntity,
|
WeakEntity,
|
||||||
};
|
};
|
||||||
|
use language::Buffer;
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use project::{Entry, ProjectPath};
|
use project::{Entry, ProjectPath};
|
||||||
use prompt_store::UserPromptId;
|
use prompt_store::UserPromptId;
|
||||||
|
@ -40,6 +41,35 @@ use crate::context_store::ContextStore;
|
||||||
use crate::thread::ThreadId;
|
use crate::thread::ThreadId;
|
||||||
use crate::thread_store::ThreadStore;
|
use crate::thread_store::ThreadStore;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum ContextPickerEntry {
|
||||||
|
Mode(ContextPickerMode),
|
||||||
|
Action(ContextPickerAction),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextPickerEntry {
|
||||||
|
pub fn keyword(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mode(mode) => mode.keyword(),
|
||||||
|
Self::Action(action) => action.keyword(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mode(mode) => mode.label(),
|
||||||
|
Self::Action(action) => action.label(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon(&self) -> IconName {
|
||||||
|
match self {
|
||||||
|
Self::Mode(mode) => mode.icon(),
|
||||||
|
Self::Action(action) => action.icon(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum ContextPickerMode {
|
enum ContextPickerMode {
|
||||||
File,
|
File,
|
||||||
|
@ -49,6 +79,31 @@ enum ContextPickerMode {
|
||||||
Rules,
|
Rules,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum ContextPickerAction {
|
||||||
|
AddSelections,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextPickerAction {
|
||||||
|
pub fn keyword(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::AddSelections => "selection",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::AddSelections => "Selection",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon(&self) -> IconName {
|
||||||
|
match self {
|
||||||
|
Self::AddSelections => IconName::Context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for ContextPickerMode {
|
impl TryFrom<&str> for ContextPickerMode {
|
||||||
type Error = String;
|
type Error = String;
|
||||||
|
|
||||||
|
@ -65,7 +120,7 @@ impl TryFrom<&str> for ContextPickerMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextPickerMode {
|
impl ContextPickerMode {
|
||||||
pub fn mention_prefix(&self) -> &'static str {
|
pub fn keyword(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::File => "file",
|
Self::File => "file",
|
||||||
Self::Symbol => "symbol",
|
Self::Symbol => "symbol",
|
||||||
|
@ -167,7 +222,13 @@ impl ContextPicker {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
|
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
|
||||||
|
|
||||||
let modes = supported_context_picker_modes(&self.thread_store);
|
let entries = self
|
||||||
|
.workspace
|
||||||
|
.upgrade()
|
||||||
|
.map(|workspace| {
|
||||||
|
available_context_picker_entries(&self.thread_store, &workspace, cx)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
menu.when(has_recent, |menu| {
|
menu.when(has_recent, |menu| {
|
||||||
menu.custom_row(|_, _| {
|
menu.custom_row(|_, _| {
|
||||||
|
@ -183,15 +244,15 @@ impl ContextPicker {
|
||||||
})
|
})
|
||||||
.extend(recent_entries)
|
.extend(recent_entries)
|
||||||
.when(has_recent, |menu| menu.separator())
|
.when(has_recent, |menu| menu.separator())
|
||||||
.extend(modes.into_iter().map(|mode| {
|
.extend(entries.into_iter().map(|entry| {
|
||||||
let context_picker = context_picker.clone();
|
let context_picker = context_picker.clone();
|
||||||
|
|
||||||
ContextMenuEntry::new(mode.label())
|
ContextMenuEntry::new(entry.label())
|
||||||
.icon(mode.icon())
|
.icon(entry.icon())
|
||||||
.icon_size(IconSize::XSmall)
|
.icon_size(IconSize::XSmall)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.handler(move |window, cx| {
|
.handler(move |window, cx| {
|
||||||
context_picker.update(cx, |this, cx| this.select_mode(mode, window, cx))
|
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
.keep_open_on_confirm()
|
.keep_open_on_confirm()
|
||||||
|
@ -210,15 +271,16 @@ impl ContextPicker {
|
||||||
self.thread_store.is_some()
|
self.thread_store.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_mode(
|
fn select_entry(
|
||||||
&mut self,
|
&mut self,
|
||||||
mode: ContextPickerMode,
|
entry: ContextPickerEntry,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let context_picker = cx.entity().downgrade();
|
let context_picker = cx.entity().downgrade();
|
||||||
|
|
||||||
match mode {
|
match entry {
|
||||||
|
ContextPickerEntry::Mode(mode) => match mode {
|
||||||
ContextPickerMode::File => {
|
ContextPickerMode::File => {
|
||||||
self.mode = ContextPickerState::File(cx.new(|cx| {
|
self.mode = ContextPickerState::File(cx.new(|cx| {
|
||||||
FileContextPicker::new(
|
FileContextPicker::new(
|
||||||
|
@ -241,6 +303,19 @@ impl ContextPicker {
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
ContextPickerMode::Rules => {
|
||||||
|
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||||
|
self.mode = ContextPickerState::Rules(cx.new(|cx| {
|
||||||
|
RulesContextPicker::new(
|
||||||
|
thread_store.clone(),
|
||||||
|
context_picker.clone(),
|
||||||
|
self.context_store.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
ContextPickerMode::Fetch => {
|
ContextPickerMode::Fetch => {
|
||||||
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
|
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
|
||||||
FetchContextPicker::new(
|
FetchContextPicker::new(
|
||||||
|
@ -265,19 +340,18 @@ impl ContextPicker {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContextPickerMode::Rules => {
|
},
|
||||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
ContextPickerEntry::Action(action) => match action {
|
||||||
self.mode = ContextPickerState::Rules(cx.new(|cx| {
|
ContextPickerAction::AddSelections => {
|
||||||
RulesContextPicker::new(
|
if let Some((context_store, workspace)) =
|
||||||
thread_store.clone(),
|
self.context_store.upgrade().zip(self.workspace.upgrade())
|
||||||
context_picker.clone(),
|
{
|
||||||
self.context_store.clone(),
|
add_selections_as_context(&context_store, &workspace, cx);
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cx.emit(DismissEvent);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -451,19 +525,37 @@ enum RecentEntry {
|
||||||
Thread(ThreadContextEntry),
|
Thread(ThreadContextEntry),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn supported_context_picker_modes(
|
fn available_context_picker_entries(
|
||||||
thread_store: &Option<WeakEntity<ThreadStore>>,
|
thread_store: &Option<WeakEntity<ThreadStore>>,
|
||||||
) -> Vec<ContextPickerMode> {
|
workspace: &Entity<Workspace>,
|
||||||
let mut modes = vec![
|
cx: &mut App,
|
||||||
ContextPickerMode::File,
|
) -> Vec<ContextPickerEntry> {
|
||||||
ContextPickerMode::Symbol,
|
let mut entries = vec![
|
||||||
ContextPickerMode::Fetch,
|
ContextPickerEntry::Mode(ContextPickerMode::File),
|
||||||
|
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
|
||||||
];
|
];
|
||||||
if thread_store.is_some() {
|
|
||||||
modes.push(ContextPickerMode::Thread);
|
let has_selection = workspace
|
||||||
modes.push(ContextPickerMode::Rules);
|
.read(cx)
|
||||||
|
.active_item(cx)
|
||||||
|
.and_then(|item| item.downcast::<Editor>())
|
||||||
|
.map_or(false, |editor| {
|
||||||
|
editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
|
||||||
|
});
|
||||||
|
if has_selection {
|
||||||
|
entries.push(ContextPickerEntry::Action(
|
||||||
|
ContextPickerAction::AddSelections,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
modes
|
|
||||||
|
if thread_store.is_some() {
|
||||||
|
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
|
||||||
|
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
|
||||||
|
|
||||||
|
entries
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recent_context_picker_entries(
|
fn recent_context_picker_entries(
|
||||||
|
@ -522,6 +614,54 @@ fn recent_context_picker_entries(
|
||||||
recent
|
recent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_selections_as_context(
|
||||||
|
context_store: &Entity<ContextStore>,
|
||||||
|
workspace: &Entity<Workspace>,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let selection_ranges = selection_ranges(workspace, cx);
|
||||||
|
context_store.update(cx, |context_store, cx| {
|
||||||
|
for (buffer, range) in selection_ranges {
|
||||||
|
context_store
|
||||||
|
.add_selection(buffer, range, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection_ranges(
|
||||||
|
workspace: &Entity<Workspace>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
|
||||||
|
let Some(editor) = workspace
|
||||||
|
.read(cx)
|
||||||
|
.active_item(cx)
|
||||||
|
.and_then(|item| item.act_as::<Editor>(cx))
|
||||||
|
else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
let selections = editor.selections.all_adjusted(cx);
|
||||||
|
|
||||||
|
let buffer = editor.buffer().clone().read(cx);
|
||||||
|
let snapshot = buffer.snapshot(cx);
|
||||||
|
|
||||||
|
selections
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
|
||||||
|
.flat_map(|range| {
|
||||||
|
let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
|
||||||
|
let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
|
||||||
|
if start_buffer != end_buffer {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((start_buffer, start..end))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn insert_fold_for_mention(
|
pub(crate) fn insert_fold_for_mention(
|
||||||
excerpt_id: ExcerptId,
|
excerpt_id: ExcerptId,
|
||||||
crease_start: text::Anchor,
|
crease_start: text::Anchor,
|
||||||
|
@ -541,24 +681,11 @@ pub(crate) fn insert_fold_for_mention(
|
||||||
let start = start.bias_right(&snapshot);
|
let start = start.bias_right(&snapshot);
|
||||||
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
||||||
|
|
||||||
let placeholder = FoldPlaceholder {
|
let crease = crease_for_mention(
|
||||||
render: render_fold_icon_button(
|
|
||||||
crease_icon_path,
|
|
||||||
crease_label,
|
crease_label,
|
||||||
editor_entity.downgrade(),
|
crease_icon_path,
|
||||||
),
|
|
||||||
merge_adjacent: false,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let render_trailer =
|
|
||||||
move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
|
||||||
|
|
||||||
let crease = Crease::inline(
|
|
||||||
start..end,
|
start..end,
|
||||||
placeholder.clone(),
|
editor_entity.downgrade(),
|
||||||
fold_toggle("mention"),
|
|
||||||
render_trailer,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
editor.display_map.update(cx, |display_map, cx| {
|
editor.display_map.update(cx, |display_map, cx| {
|
||||||
|
@ -567,6 +694,29 @@ pub(crate) fn insert_fold_for_mention(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn crease_for_mention(
|
||||||
|
label: SharedString,
|
||||||
|
icon_path: SharedString,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
editor_entity: WeakEntity<Editor>,
|
||||||
|
) -> Crease<Anchor> {
|
||||||
|
let placeholder = FoldPlaceholder {
|
||||||
|
render: render_fold_icon_button(icon_path, label, editor_entity),
|
||||||
|
merge_adjacent: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
|
||||||
|
|
||||||
|
let crease = Crease::inline(
|
||||||
|
range,
|
||||||
|
placeholder.clone(),
|
||||||
|
fold_toggle("mention"),
|
||||||
|
render_trailer,
|
||||||
|
);
|
||||||
|
crease
|
||||||
|
}
|
||||||
|
|
||||||
fn render_fold_icon_button(
|
fn render_fold_icon_button(
|
||||||
icon_path: SharedString,
|
icon_path: SharedString,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
|
@ -655,6 +805,7 @@ fn fold_toggle(
|
||||||
pub enum MentionLink {
|
pub enum MentionLink {
|
||||||
File(ProjectPath, Entry),
|
File(ProjectPath, Entry),
|
||||||
Symbol(ProjectPath, String),
|
Symbol(ProjectPath, String),
|
||||||
|
Selection(ProjectPath, Range<usize>),
|
||||||
Fetch(String),
|
Fetch(String),
|
||||||
Thread(ThreadId),
|
Thread(ThreadId),
|
||||||
Rules(UserPromptId),
|
Rules(UserPromptId),
|
||||||
|
@ -663,6 +814,7 @@ pub enum MentionLink {
|
||||||
impl MentionLink {
|
impl MentionLink {
|
||||||
const FILE: &str = "@file";
|
const FILE: &str = "@file";
|
||||||
const SYMBOL: &str = "@symbol";
|
const SYMBOL: &str = "@symbol";
|
||||||
|
const SELECTION: &str = "@selection";
|
||||||
const THREAD: &str = "@thread";
|
const THREAD: &str = "@thread";
|
||||||
const FETCH: &str = "@fetch";
|
const FETCH: &str = "@fetch";
|
||||||
const RULES: &str = "@rules";
|
const RULES: &str = "@rules";
|
||||||
|
@ -672,8 +824,9 @@ impl MentionLink {
|
||||||
pub fn is_valid(url: &str) -> bool {
|
pub fn is_valid(url: &str) -> bool {
|
||||||
url.starts_with(Self::FILE)
|
url.starts_with(Self::FILE)
|
||||||
|| url.starts_with(Self::SYMBOL)
|
|| url.starts_with(Self::SYMBOL)
|
||||||
|| url.starts_with(Self::THREAD)
|
|
||||||
|| url.starts_with(Self::FETCH)
|
|| url.starts_with(Self::FETCH)
|
||||||
|
|| url.starts_with(Self::SELECTION)
|
||||||
|
|| url.starts_with(Self::THREAD)
|
||||||
|| url.starts_with(Self::RULES)
|
|| url.starts_with(Self::RULES)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -691,6 +844,19 @@ impl MentionLink {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
|
||||||
|
format!(
|
||||||
|
"[@{} ({}-{})]({}:{}:{}-{})",
|
||||||
|
file_name,
|
||||||
|
line_range.start,
|
||||||
|
line_range.end,
|
||||||
|
Self::SELECTION,
|
||||||
|
full_path,
|
||||||
|
line_range.start,
|
||||||
|
line_range.end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn for_thread(thread: &ThreadContextEntry) -> String {
|
pub fn for_thread(thread: &ThreadContextEntry) -> String {
|
||||||
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
|
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
|
||||||
}
|
}
|
||||||
|
@ -739,6 +905,20 @@ impl MentionLink {
|
||||||
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||||
Some(MentionLink::Symbol(project_path, symbol.to_string()))
|
Some(MentionLink::Symbol(project_path, symbol.to_string()))
|
||||||
}
|
}
|
||||||
|
Self::SELECTION => {
|
||||||
|
let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
|
||||||
|
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||||
|
|
||||||
|
let line_range = {
|
||||||
|
let (start, end) = line_args
|
||||||
|
.trim_start_matches('(')
|
||||||
|
.trim_end_matches(')')
|
||||||
|
.split_once('-')?;
|
||||||
|
start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(MentionLink::Selection(project_path, line_range))
|
||||||
|
}
|
||||||
Self::THREAD => {
|
Self::THREAD => {
|
||||||
let thread_id = ThreadId::from(argument);
|
let thread_id = ThreadId::from(argument);
|
||||||
Some(MentionLink::Thread(thread_id))
|
Some(MentionLink::Thread(thread_id))
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{App, Entity, Task, WeakEntity};
|
use gpui::{App, Entity, Task, WeakEntity};
|
||||||
use http_client::HttpClientWithUrl;
|
use http_client::HttpClientWithUrl;
|
||||||
|
use itertools::Itertools;
|
||||||
use language::{Buffer, CodeLabel, HighlightId};
|
use language::{Buffer, CodeLabel, HighlightId};
|
||||||
use lsp::CompletionContext;
|
use lsp::CompletionContext;
|
||||||
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
|
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
|
||||||
use prompt_store::PromptId;
|
use prompt_store::PromptId;
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use text::{Anchor, ToPoint};
|
use text::{Anchor, OffsetRangeExt, ToPoint};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -32,8 +33,8 @@ use super::rules_context_picker::{RulesContextEntry, search_rules};
|
||||||
use super::symbol_context_picker::SymbolMatch;
|
use super::symbol_context_picker::SymbolMatch;
|
||||||
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
|
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
|
||||||
use super::{
|
use super::{
|
||||||
ContextPickerMode, MentionLink, RecentEntry, recent_context_picker_entries,
|
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
|
||||||
supported_context_picker_modes,
|
available_context_picker_entries, recent_context_picker_entries, selection_ranges,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) enum Match {
|
pub(crate) enum Match {
|
||||||
|
@ -42,19 +43,19 @@ pub(crate) enum Match {
|
||||||
Thread(ThreadMatch),
|
Thread(ThreadMatch),
|
||||||
Fetch(SharedString),
|
Fetch(SharedString),
|
||||||
Rules(RulesContextEntry),
|
Rules(RulesContextEntry),
|
||||||
Mode(ModeMatch),
|
Entry(EntryMatch),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ModeMatch {
|
pub struct EntryMatch {
|
||||||
mat: Option<StringMatch>,
|
mat: Option<StringMatch>,
|
||||||
mode: ContextPickerMode,
|
entry: ContextPickerEntry,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Match {
|
impl Match {
|
||||||
pub fn score(&self) -> f64 {
|
pub fn score(&self) -> f64 {
|
||||||
match self {
|
match self {
|
||||||
Match::File(file) => file.mat.score,
|
Match::File(file) => file.mat.score,
|
||||||
Match::Mode(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
|
Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
|
||||||
Match::Thread(_) => 1.,
|
Match::Thread(_) => 1.,
|
||||||
Match::Symbol(_) => 1.,
|
Match::Symbol(_) => 1.,
|
||||||
Match::Fetch(_) => 1.,
|
Match::Fetch(_) => 1.,
|
||||||
|
@ -162,9 +163,14 @@ fn search(
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
matches.extend(
|
matches.extend(
|
||||||
supported_context_picker_modes(&thread_store)
|
available_context_picker_entries(&thread_store, &workspace, cx)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|mode| Match::Mode(ModeMatch { mode, mat: None })),
|
.map(|mode| {
|
||||||
|
Match::Entry(EntryMatch {
|
||||||
|
entry: mode,
|
||||||
|
mat: None,
|
||||||
|
})
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
Task::ready(matches)
|
Task::ready(matches)
|
||||||
|
@ -174,11 +180,11 @@ fn search(
|
||||||
let search_files_task =
|
let search_files_task =
|
||||||
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||||
|
|
||||||
let modes = supported_context_picker_modes(&thread_store);
|
let entries = available_context_picker_entries(&thread_store, &workspace, cx);
|
||||||
let mode_candidates = modes
|
let entry_candidates = entries
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ix, mode)| StringMatchCandidate::new(ix, mode.mention_prefix()))
|
.map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
|
@ -188,8 +194,8 @@ fn search(
|
||||||
.map(Match::File)
|
.map(Match::File)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mode_matches = fuzzy::match_strings(
|
let entry_matches = fuzzy::match_strings(
|
||||||
&mode_candidates,
|
&entry_candidates,
|
||||||
&query,
|
&query,
|
||||||
false,
|
false,
|
||||||
100,
|
100,
|
||||||
|
@ -198,9 +204,9 @@ fn search(
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
matches.extend(mode_matches.into_iter().map(|mat| {
|
matches.extend(entry_matches.into_iter().map(|mat| {
|
||||||
Match::Mode(ModeMatch {
|
Match::Entry(EntryMatch {
|
||||||
mode: modes[mat.candidate_id],
|
entry: entries[mat.candidate_id],
|
||||||
mat: Some(mat),
|
mat: Some(mat),
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
@ -240,10 +246,19 @@ impl ContextPickerCompletionProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn completion_for_mode(source_range: Range<Anchor>, mode: ContextPickerMode) -> Completion {
|
fn completion_for_entry(
|
||||||
Completion {
|
entry: ContextPickerEntry,
|
||||||
|
excerpt_id: ExcerptId,
|
||||||
|
source_range: Range<Anchor>,
|
||||||
|
editor: Entity<Editor>,
|
||||||
|
context_store: Entity<ContextStore>,
|
||||||
|
workspace: &Entity<Workspace>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<Completion> {
|
||||||
|
match entry {
|
||||||
|
ContextPickerEntry::Mode(mode) => Some(Completion {
|
||||||
replace_range: source_range.clone(),
|
replace_range: source_range.clone(),
|
||||||
new_text: format!("@{} ", mode.mention_prefix()),
|
new_text: format!("@{} ", mode.keyword()),
|
||||||
label: CodeLabel::plain(mode.label().to_string(), None),
|
label: CodeLabel::plain(mode.label().to_string(), None),
|
||||||
icon_path: Some(mode.icon().path().into()),
|
icon_path: Some(mode.icon().path().into()),
|
||||||
documentation: None,
|
documentation: None,
|
||||||
|
@ -253,6 +268,115 @@ impl ContextPickerCompletionProvider {
|
||||||
// completion menu will still be shown after "@category " is
|
// completion menu will still be shown after "@category " is
|
||||||
// inserted
|
// inserted
|
||||||
confirm: Some(Arc::new(|_, _, _| true)),
|
confirm: Some(Arc::new(|_, _, _| true)),
|
||||||
|
}),
|
||||||
|
ContextPickerEntry::Action(action) => {
|
||||||
|
let (new_text, on_action) = match action {
|
||||||
|
ContextPickerAction::AddSelections => {
|
||||||
|
let selections = selection_ranges(workspace, cx);
|
||||||
|
|
||||||
|
let selection_infos = selections
|
||||||
|
.iter()
|
||||||
|
.map(|(buffer, range)| {
|
||||||
|
let full_path = buffer
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.map(|file| file.full_path(cx))
|
||||||
|
.unwrap_or_else(|| PathBuf::from("untitled"));
|
||||||
|
let file_name = full_path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
let line_range = range.to_point(&buffer.read(cx).snapshot());
|
||||||
|
|
||||||
|
let link = MentionLink::for_selection(
|
||||||
|
&file_name,
|
||||||
|
&full_path.to_string_lossy(),
|
||||||
|
line_range.start.row as usize..line_range.end.row as usize,
|
||||||
|
);
|
||||||
|
(file_name, link, line_range)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let new_text = selection_infos.iter().map(|(_, link, _)| link).join(" ");
|
||||||
|
|
||||||
|
let callback = Arc::new({
|
||||||
|
let context_store = context_store.clone();
|
||||||
|
let selections = selections.clone();
|
||||||
|
let selection_infos = selection_infos.clone();
|
||||||
|
move |_, _: &mut Window, cx: &mut App| {
|
||||||
|
context_store.update(cx, |context_store, cx| {
|
||||||
|
for (buffer, range) in &selections {
|
||||||
|
context_store
|
||||||
|
.add_selection(buffer.clone(), range.clone(), cx)
|
||||||
|
.detach_and_log_err(cx)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let editor = editor.clone();
|
||||||
|
let selection_infos = selection_infos.clone();
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
let mut current_offset = 0;
|
||||||
|
for (file_name, link, line_range) in selection_infos.iter() {
|
||||||
|
let snapshot =
|
||||||
|
editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||||
|
let Some(start) = snapshot
|
||||||
|
.anchor_in_excerpt(excerpt_id, source_range.start)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset = start.to_offset(&snapshot) + current_offset;
|
||||||
|
let text_len = link.len();
|
||||||
|
|
||||||
|
let range = snapshot.anchor_after(offset)
|
||||||
|
..snapshot.anchor_after(offset + text_len);
|
||||||
|
|
||||||
|
let crease = super::crease_for_mention(
|
||||||
|
format!(
|
||||||
|
"{} ({}-{})",
|
||||||
|
file_name,
|
||||||
|
line_range.start.row + 1,
|
||||||
|
line_range.end.row + 1
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
IconName::Context.path().into(),
|
||||||
|
range,
|
||||||
|
editor.downgrade(),
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.display_map.update(cx, |display_map, cx| {
|
||||||
|
display_map.fold(vec![crease], cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
current_offset += text_len + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(new_text, callback)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Completion {
|
||||||
|
replace_range: source_range.clone(),
|
||||||
|
new_text,
|
||||||
|
label: CodeLabel::plain(action.label().to_string(), None),
|
||||||
|
icon_path: Some(action.icon().path().into()),
|
||||||
|
documentation: None,
|
||||||
|
source: project::CompletionSource::Custom,
|
||||||
|
insert_text_mode: None,
|
||||||
|
// This ensures that when a user accepts this completion, the
|
||||||
|
// completion menu will still be shown after "@category " is
|
||||||
|
// inserted
|
||||||
|
confirm: Some(on_action),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -686,9 +810,15 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||||
context_store.clone(),
|
context_store.clone(),
|
||||||
http_client.clone(),
|
http_client.clone(),
|
||||||
)),
|
)),
|
||||||
Match::Mode(ModeMatch { mode, .. }) => {
|
Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
|
||||||
Some(Self::completion_for_mode(source_range.clone(), mode))
|
entry,
|
||||||
}
|
excerpt_id,
|
||||||
|
source_range.clone(),
|
||||||
|
editor.clone(),
|
||||||
|
context_store.clone(),
|
||||||
|
&workspace,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
})?))
|
})?))
|
||||||
|
|
|
@ -18,7 +18,7 @@ use util::{ResultExt as _, maybe};
|
||||||
use crate::ThreadStore;
|
use crate::ThreadStore;
|
||||||
use crate::context::{
|
use crate::context::{
|
||||||
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
|
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
|
||||||
ExcerptContext, FetchedUrlContext, FileContext, ImageContext, RulesContext, SymbolContext,
|
FetchedUrlContext, FileContext, ImageContext, RulesContext, SelectionContext, SymbolContext,
|
||||||
ThreadContext,
|
ThreadContext,
|
||||||
};
|
};
|
||||||
use crate::context_strip::SuggestedContext;
|
use crate::context_strip::SuggestedContext;
|
||||||
|
@ -476,10 +476,10 @@ impl ContextStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_excerpt(
|
pub fn add_selection(
|
||||||
&mut self,
|
&mut self,
|
||||||
range: Range<Anchor>,
|
|
||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
|
range: Range<Anchor>,
|
||||||
cx: &mut Context<ContextStore>,
|
cx: &mut Context<ContextStore>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
|
@ -490,14 +490,14 @@ impl ContextStore {
|
||||||
let context_buffer = context_buffer_task.await;
|
let context_buffer = context_buffer_task.await;
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.insert_excerpt(context_buffer, range, line_range, cx)
|
this.insert_selection(context_buffer, range, line_range, cx)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_excerpt(
|
fn insert_selection(
|
||||||
&mut self,
|
&mut self,
|
||||||
context_buffer: ContextBuffer,
|
context_buffer: ContextBuffer,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
|
@ -505,7 +505,8 @@ impl ContextStore {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let id = self.next_context_id.post_inc();
|
let id = self.next_context_id.post_inc();
|
||||||
self.context.push(AssistantContext::Excerpt(ExcerptContext {
|
self.context
|
||||||
|
.push(AssistantContext::Selection(SelectionContext {
|
||||||
id,
|
id,
|
||||||
range,
|
range,
|
||||||
line_range,
|
line_range,
|
||||||
|
@ -563,7 +564,7 @@ impl ContextStore {
|
||||||
self.symbol_buffers.remove(&symbol.context_symbol.id);
|
self.symbol_buffers.remove(&symbol.context_symbol.id);
|
||||||
self.symbols.retain(|_, context_id| *context_id != id);
|
self.symbols.retain(|_, context_id| *context_id != id);
|
||||||
}
|
}
|
||||||
AssistantContext::Excerpt(_) => {}
|
AssistantContext::Selection(_) => {}
|
||||||
AssistantContext::FetchedUrl(_) => {
|
AssistantContext::FetchedUrl(_) => {
|
||||||
self.fetched_urls.retain(|_, context_id| *context_id != id);
|
self.fetched_urls.retain(|_, context_id| *context_id != id);
|
||||||
}
|
}
|
||||||
|
@ -699,7 +700,7 @@ impl ContextStore {
|
||||||
}
|
}
|
||||||
AssistantContext::Directory(_)
|
AssistantContext::Directory(_)
|
||||||
| AssistantContext::Symbol(_)
|
| AssistantContext::Symbol(_)
|
||||||
| AssistantContext::Excerpt(_)
|
| AssistantContext::Selection(_)
|
||||||
| AssistantContext::FetchedUrl(_)
|
| AssistantContext::FetchedUrl(_)
|
||||||
| AssistantContext::Thread(_)
|
| AssistantContext::Thread(_)
|
||||||
| AssistantContext::Rules(_)
|
| AssistantContext::Rules(_)
|
||||||
|
@ -914,13 +915,13 @@ pub fn refresh_context_store_text(
|
||||||
return refresh_symbol_text(context_store, symbol_context, cx);
|
return refresh_symbol_text(context_store, symbol_context, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AssistantContext::Excerpt(excerpt_context) => {
|
AssistantContext::Selection(selection_context) => {
|
||||||
// TODO: Should refresh if the path has changed, as it's in the text.
|
// TODO: Should refresh if the path has changed, as it's in the text.
|
||||||
if changed_buffers.is_empty()
|
if changed_buffers.is_empty()
|
||||||
|| changed_buffers.contains(&excerpt_context.context_buffer.buffer)
|
|| changed_buffers.contains(&selection_context.context_buffer.buffer)
|
||||||
{
|
{
|
||||||
let context_store = context_store.clone();
|
let context_store = context_store.clone();
|
||||||
return refresh_excerpt_text(context_store, excerpt_context, cx);
|
return refresh_selection_text(context_store, selection_context, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AssistantContext::Thread(thread_context) => {
|
AssistantContext::Thread(thread_context) => {
|
||||||
|
@ -1042,26 +1043,27 @@ fn refresh_symbol_text(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_excerpt_text(
|
fn refresh_selection_text(
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
excerpt_context: &ExcerptContext,
|
selection_context: &SelectionContext,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Option<Task<()>> {
|
) -> Option<Task<()>> {
|
||||||
let id = excerpt_context.id;
|
let id = selection_context.id;
|
||||||
let range = excerpt_context.range.clone();
|
let range = selection_context.range.clone();
|
||||||
let task = refresh_context_excerpt(&excerpt_context.context_buffer, range.clone(), cx);
|
let task = refresh_context_excerpt(&selection_context.context_buffer, range.clone(), cx);
|
||||||
if let Some(task) = task {
|
if let Some(task) = task {
|
||||||
Some(cx.spawn(async move |cx| {
|
Some(cx.spawn(async move |cx| {
|
||||||
let (line_range, context_buffer) = task.await;
|
let (line_range, context_buffer) = task.await;
|
||||||
context_store
|
context_store
|
||||||
.update(cx, |context_store, _| {
|
.update(cx, |context_store, _| {
|
||||||
let new_excerpt_context = ExcerptContext {
|
let new_selection_context = SelectionContext {
|
||||||
id,
|
id,
|
||||||
range,
|
range,
|
||||||
line_range,
|
line_range,
|
||||||
context_buffer,
|
context_buffer,
|
||||||
};
|
};
|
||||||
context_store.replace_context(AssistantContext::Excerpt(new_excerpt_context));
|
context_store
|
||||||
|
.replace_context(AssistantContext::Selection(new_selection_context));
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -298,7 +298,7 @@ impl MessageEditor {
|
||||||
.filter(|ctx| {
|
.filter(|ctx| {
|
||||||
matches!(
|
matches!(
|
||||||
ctx,
|
ctx,
|
||||||
AssistantContext::Excerpt(_) | AssistantContext::Image(_)
|
AssistantContext::Selection(_) | AssistantContext::Image(_)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.map(|ctx| ctx.id())
|
.map(|ctx| ctx.id())
|
||||||
|
|
|
@ -780,9 +780,9 @@ impl Thread {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
AssistantContext::Excerpt(excerpt_context) => {
|
AssistantContext::Selection(selection_context) => {
|
||||||
log.buffer_added_as_context(
|
log.buffer_added_as_context(
|
||||||
excerpt_context.context_buffer.buffer.clone(),
|
selection_context.context_buffer.buffer.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::{rc::Rc, time::Duration};
|
||||||
|
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use gpui::{Animation, AnimationExt as _, AnyView, Image, MouseButton, pulsating_between};
|
use gpui::{Animation, AnimationExt as _, Image, MouseButton, pulsating_between};
|
||||||
use gpui::{ClickEvent, Task};
|
use gpui::{ClickEvent, Task};
|
||||||
use language_model::LanguageModelImage;
|
use language_model::LanguageModelImage;
|
||||||
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
|
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
|
||||||
|
@ -168,11 +168,16 @@ impl RenderOnce for ContextPill {
|
||||||
.map(|element| match &context.status {
|
.map(|element| match &context.status {
|
||||||
ContextStatus::Ready => element
|
ContextStatus::Ready => element
|
||||||
.when_some(
|
.when_some(
|
||||||
context.show_preview.as_ref(),
|
context.render_preview.as_ref(),
|
||||||
|element, show_preview| {
|
|element, render_preview| {
|
||||||
element.hoverable_tooltip({
|
element.hoverable_tooltip({
|
||||||
let show_preview = show_preview.clone();
|
let render_preview = render_preview.clone();
|
||||||
move |window, cx| show_preview(window, cx)
|
move |_, cx| {
|
||||||
|
cx.new(|_| ContextPillPreview {
|
||||||
|
render_preview: render_preview.clone(),
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -266,7 +271,7 @@ pub struct AddedContext {
|
||||||
pub tooltip: Option<SharedString>,
|
pub tooltip: Option<SharedString>,
|
||||||
pub icon_path: Option<SharedString>,
|
pub icon_path: Option<SharedString>,
|
||||||
pub status: ContextStatus,
|
pub status: ContextStatus,
|
||||||
pub show_preview: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
|
pub render_preview: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AddedContext {
|
impl AddedContext {
|
||||||
|
@ -292,7 +297,7 @@ impl AddedContext {
|
||||||
tooltip: Some(full_path_string),
|
tooltip: Some(full_path_string),
|
||||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||||
status: ContextStatus::Ready,
|
status: ContextStatus::Ready,
|
||||||
show_preview: None,
|
render_preview: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,7 +328,7 @@ impl AddedContext {
|
||||||
tooltip: Some(full_path_string),
|
tooltip: Some(full_path_string),
|
||||||
icon_path: None,
|
icon_path: None,
|
||||||
status: ContextStatus::Ready,
|
status: ContextStatus::Ready,
|
||||||
show_preview: None,
|
render_preview: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,11 +340,11 @@ impl AddedContext {
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
icon_path: None,
|
icon_path: None,
|
||||||
status: ContextStatus::Ready,
|
status: ContextStatus::Ready,
|
||||||
show_preview: None,
|
render_preview: None,
|
||||||
},
|
},
|
||||||
|
|
||||||
AssistantContext::Excerpt(excerpt_context) => {
|
AssistantContext::Selection(selection_context) => {
|
||||||
let full_path = excerpt_context.context_buffer.full_path(cx);
|
let full_path = selection_context.context_buffer.full_path(cx);
|
||||||
let mut full_path_string = full_path.to_string_lossy().into_owned();
|
let mut full_path_string = full_path.to_string_lossy().into_owned();
|
||||||
let mut name = full_path
|
let mut name = full_path
|
||||||
.file_name()
|
.file_name()
|
||||||
|
@ -348,8 +353,8 @@ impl AddedContext {
|
||||||
|
|
||||||
let line_range_text = format!(
|
let line_range_text = format!(
|
||||||
" ({}-{})",
|
" ({}-{})",
|
||||||
excerpt_context.line_range.start.row + 1,
|
selection_context.line_range.start.row + 1,
|
||||||
excerpt_context.line_range.end.row + 1
|
selection_context.line_range.end.row + 1
|
||||||
);
|
);
|
||||||
|
|
||||||
full_path_string.push_str(&line_range_text);
|
full_path_string.push_str(&line_range_text);
|
||||||
|
@ -361,14 +366,25 @@ impl AddedContext {
|
||||||
.map(|n| n.to_string_lossy().into_owned().into());
|
.map(|n| n.to_string_lossy().into_owned().into());
|
||||||
|
|
||||||
AddedContext {
|
AddedContext {
|
||||||
id: excerpt_context.id,
|
id: selection_context.id,
|
||||||
kind: ContextKind::File,
|
kind: ContextKind::Selection,
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
parent,
|
parent,
|
||||||
tooltip: Some(full_path_string.into()),
|
tooltip: None,
|
||||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||||
status: ContextStatus::Ready,
|
status: ContextStatus::Ready,
|
||||||
show_preview: None,
|
render_preview: Some(Rc::new({
|
||||||
|
let content = selection_context.context_buffer.text.clone();
|
||||||
|
move |_, cx| {
|
||||||
|
div()
|
||||||
|
.id("context-pill-selection-preview")
|
||||||
|
.overflow_scroll()
|
||||||
|
.max_w_128()
|
||||||
|
.max_h_96()
|
||||||
|
.child(Label::new(content.clone()).buffer_font(cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,7 +396,7 @@ impl AddedContext {
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
icon_path: None,
|
icon_path: None,
|
||||||
status: ContextStatus::Ready,
|
status: ContextStatus::Ready,
|
||||||
show_preview: None,
|
render_preview: None,
|
||||||
},
|
},
|
||||||
|
|
||||||
AssistantContext::Thread(thread_context) => AddedContext {
|
AssistantContext::Thread(thread_context) => AddedContext {
|
||||||
|
@ -401,7 +417,7 @@ impl AddedContext {
|
||||||
} else {
|
} else {
|
||||||
ContextStatus::Ready
|
ContextStatus::Ready
|
||||||
},
|
},
|
||||||
show_preview: None,
|
render_preview: None,
|
||||||
},
|
},
|
||||||
|
|
||||||
AssistantContext::Rules(user_rules_context) => AddedContext {
|
AssistantContext::Rules(user_rules_context) => AddedContext {
|
||||||
|
@ -412,7 +428,7 @@ impl AddedContext {
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
icon_path: None,
|
icon_path: None,
|
||||||
status: ContextStatus::Ready,
|
status: ContextStatus::Ready,
|
||||||
show_preview: None,
|
render_preview: None,
|
||||||
},
|
},
|
||||||
|
|
||||||
AssistantContext::Image(image_context) => AddedContext {
|
AssistantContext::Image(image_context) => AddedContext {
|
||||||
|
@ -433,13 +449,13 @@ impl AddedContext {
|
||||||
} else {
|
} else {
|
||||||
ContextStatus::Ready
|
ContextStatus::Ready
|
||||||
},
|
},
|
||||||
show_preview: Some(Rc::new({
|
render_preview: Some(Rc::new({
|
||||||
let image = image_context.original_image.clone();
|
let image = image_context.original_image.clone();
|
||||||
move |_, cx| {
|
move |_, _| {
|
||||||
cx.new(|_| ImagePreview {
|
gpui::img(image.clone())
|
||||||
image: image.clone(),
|
.max_w_96()
|
||||||
})
|
.max_h_96()
|
||||||
.into()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
@ -447,17 +463,17 @@ impl AddedContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ImagePreview {
|
struct ContextPillPreview {
|
||||||
image: Arc<Image>,
|
render_preview: Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for ImagePreview {
|
impl Render for ContextPillPreview {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
tooltip_container(window, cx, move |this, _, _| {
|
tooltip_container(window, cx, move |this, window, cx| {
|
||||||
this.occlude()
|
this.occlude()
|
||||||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||||
.child(gpui::img(self.image.clone()).max_w_96().max_h_96())
|
.child((self.render_preview)(window, cx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3108,6 +3108,13 @@ impl Editor {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_non_empty_selection(&self, cx: &mut App) -> bool {
|
||||||
|
self.selections
|
||||||
|
.all_adjusted(cx)
|
||||||
|
.iter()
|
||||||
|
.any(|selection| !selection.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_pending_nonempty_selection(&self) -> bool {
|
pub fn has_pending_nonempty_selection(&self) -> bool {
|
||||||
let pending_nonempty_selection = match self.selections.pending_anchor() {
|
let pending_nonempty_selection = match self.selections.pending_anchor() {
|
||||||
Some(Selection { start, end, .. }) => start != end,
|
Some(Selection { start, end, .. }) => start != end,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue