Detailed changes
@@ -670,6 +670,26 @@ fn open_markdown_link(
})
.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| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
panel.update(cx, |panel, cx| {
@@ -3309,15 +3329,15 @@ pub(crate) fn open_context(
.detach();
}
}
- AssistantContext::Excerpt(excerpt_context) => {
- if let Some(project_path) = excerpt_context
+ AssistantContext::Selection(selection_context) => {
+ if let Some(project_path) = selection_context
.context_buffer
.buffer
.read(cx)
.project_path(cx)
{
- let snapshot = excerpt_context.context_buffer.buffer.read(cx).snapshot();
- let target_position = excerpt_context.range.start.to_point(&snapshot);
+ let snapshot = selection_context.context_buffer.buffer.read(cx).snapshot();
+ let target_position = selection_context.range.start.to_point(&snapshot);
open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
@@ -1951,7 +1951,9 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
.collect::<Vec<_>>();
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,
Directory,
Symbol,
- Excerpt,
+ Selection,
FetchedUrl,
Thread,
Rules,
@@ -46,7 +46,7 @@ impl ContextKind {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::Symbol => IconName::Code,
- ContextKind::Excerpt => IconName::Code,
+ ContextKind::Selection => IconName::Context,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageBubbles,
ContextKind::Rules => RULES_ICON,
@@ -62,7 +62,7 @@ pub enum AssistantContext {
Symbol(SymbolContext),
FetchedUrl(FetchedUrlContext),
Thread(ThreadContext),
- Excerpt(ExcerptContext),
+ Selection(SelectionContext),
Rules(RulesContext),
Image(ImageContext),
}
@@ -75,7 +75,7 @@ impl AssistantContext {
Self::Symbol(symbol) => symbol.id,
Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id,
- Self::Excerpt(excerpt) => excerpt.id,
+ Self::Selection(selection) => selection.id,
Self::Rules(rules) => rules.id,
Self::Image(image) => image.id,
}
@@ -220,7 +220,7 @@ pub struct ContextSymbolId {
}
#[derive(Debug, Clone)]
-pub struct ExcerptContext {
+pub struct SelectionContext {
pub id: ContextId,
pub range: Range<Anchor>,
pub line_range: Range<Point>,
@@ -243,7 +243,7 @@ pub fn format_context_as_string<'a>(
let mut file_context = Vec::new();
let mut directory_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 thread_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::Directory(context) => directory_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::Thread(context) => thread_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()
&& directory_context.is_empty()
&& symbol_context.is_empty()
- && excerpt_context.is_empty()
+ && selection_context.is_empty()
&& fetch_context.is_empty()
&& thread_context.is_empty()
&& rules_context.is_empty()
@@ -303,13 +303,13 @@ pub fn format_context_as_string<'a>(
result.push_str("</symbols>\n");
}
- if !excerpt_context.is_empty() {
- result.push_str("<excerpts>\n");
- for context in excerpt_context {
+ if !selection_context.is_empty() {
+ result.push_str("<selections>\n");
+ for context in selection_context {
result.push_str(&context.context_buffer.text);
result.push('\n');
}
- result.push_str("</excerpts>\n");
+ result.push_str("</selections>\n");
}
if !fetch_context.is_empty() {
@@ -17,6 +17,7 @@ use gpui::{
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
WeakEntity,
};
+use language::Buffer;
use multi_buffer::MultiBufferRow;
use project::{Entry, ProjectPath};
use prompt_store::UserPromptId;
@@ -40,6 +41,35 @@ use crate::context_store::ContextStore;
use crate::thread::ThreadId;
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)]
enum ContextPickerMode {
File,
@@ -49,6 +79,31 @@ enum ContextPickerMode {
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 {
type Error = String;
@@ -65,7 +120,7 @@ impl TryFrom<&str> for ContextPickerMode {
}
impl ContextPickerMode {
- pub fn mention_prefix(&self) -> &'static str {
+ pub fn keyword(&self) -> &'static str {
match self {
Self::File => "file",
Self::Symbol => "symbol",
@@ -167,7 +222,13 @@ impl ContextPicker {
.enumerate()
.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.custom_row(|_, _| {
@@ -183,15 +244,15 @@ impl ContextPicker {
})
.extend(recent_entries)
.when(has_recent, |menu| menu.separator())
- .extend(modes.into_iter().map(|mode| {
+ .extend(entries.into_iter().map(|entry| {
let context_picker = context_picker.clone();
- ContextMenuEntry::new(mode.label())
- .icon(mode.icon())
+ ContextMenuEntry::new(entry.label())
+ .icon(entry.icon())
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.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()
@@ -210,74 +271,87 @@ impl ContextPicker {
self.thread_store.is_some()
}
- fn select_mode(
+ fn select_entry(
&mut self,
- mode: ContextPickerMode,
+ entry: ContextPickerEntry,
window: &mut Window,
cx: &mut Context<Self>,
) {
let context_picker = cx.entity().downgrade();
- match mode {
- ContextPickerMode::File => {
- self.mode = ContextPickerState::File(cx.new(|cx| {
- FileContextPicker::new(
- context_picker.clone(),
- self.workspace.clone(),
- self.context_store.clone(),
- window,
- cx,
- )
- }));
- }
- ContextPickerMode::Symbol => {
- self.mode = ContextPickerState::Symbol(cx.new(|cx| {
- SymbolContextPicker::new(
- context_picker.clone(),
- self.workspace.clone(),
- self.context_store.clone(),
- window,
- cx,
- )
- }));
- }
- ContextPickerMode::Fetch => {
- self.mode = ContextPickerState::Fetch(cx.new(|cx| {
- FetchContextPicker::new(
- context_picker.clone(),
- self.workspace.clone(),
- self.context_store.clone(),
- window,
- cx,
- )
- }));
- }
- ContextPickerMode::Thread => {
- if let Some(thread_store) = self.thread_store.as_ref() {
- self.mode = ContextPickerState::Thread(cx.new(|cx| {
- ThreadContextPicker::new(
- thread_store.clone(),
+ match entry {
+ ContextPickerEntry::Mode(mode) => match mode {
+ ContextPickerMode::File => {
+ self.mode = ContextPickerState::File(cx.new(|cx| {
+ FileContextPicker::new(
context_picker.clone(),
+ self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
- }
- ContextPickerMode::Rules => {
- if let Some(thread_store) = self.thread_store.as_ref() {
- self.mode = ContextPickerState::Rules(cx.new(|cx| {
- RulesContextPicker::new(
- thread_store.clone(),
+ ContextPickerMode::Symbol => {
+ self.mode = ContextPickerState::Symbol(cx.new(|cx| {
+ SymbolContextPicker::new(
context_picker.clone(),
+ self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
- }
+ 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 => {
+ self.mode = ContextPickerState::Fetch(cx.new(|cx| {
+ FetchContextPicker::new(
+ context_picker.clone(),
+ self.workspace.clone(),
+ self.context_store.clone(),
+ window,
+ cx,
+ )
+ }));
+ }
+ ContextPickerMode::Thread => {
+ if let Some(thread_store) = self.thread_store.as_ref() {
+ self.mode = ContextPickerState::Thread(cx.new(|cx| {
+ ThreadContextPicker::new(
+ thread_store.clone(),
+ context_picker.clone(),
+ self.context_store.clone(),
+ window,
+ cx,
+ )
+ }));
+ }
+ }
+ },
+ ContextPickerEntry::Action(action) => match action {
+ ContextPickerAction::AddSelections => {
+ if let Some((context_store, workspace)) =
+ self.context_store.upgrade().zip(self.workspace.upgrade())
+ {
+ add_selections_as_context(&context_store, &workspace, cx);
+ }
+
+ cx.emit(DismissEvent);
+ }
+ },
}
cx.notify();
@@ -451,19 +525,37 @@ enum RecentEntry {
Thread(ThreadContextEntry),
}
-fn supported_context_picker_modes(
+fn available_context_picker_entries(
thread_store: &Option<WeakEntity<ThreadStore>>,
-) -> Vec<ContextPickerMode> {
- let mut modes = vec![
- ContextPickerMode::File,
- ContextPickerMode::Symbol,
- ContextPickerMode::Fetch,
+ workspace: &Entity<Workspace>,
+ cx: &mut App,
+) -> Vec<ContextPickerEntry> {
+ let mut entries = vec![
+ ContextPickerEntry::Mode(ContextPickerMode::File),
+ ContextPickerEntry::Mode(ContextPickerMode::Symbol),
];
+
+ let has_selection = workspace
+ .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,
+ ));
+ }
+
if thread_store.is_some() {
- modes.push(ContextPickerMode::Thread);
- modes.push(ContextPickerMode::Rules);
+ entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
+ entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
}
- modes
+
+ entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
+
+ entries
}
fn recent_context_picker_entries(
@@ -522,6 +614,54 @@ fn recent_context_picker_entries(
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(
excerpt_id: ExcerptId,
crease_start: text::Anchor,
@@ -541,24 +681,11 @@ pub(crate) fn insert_fold_for_mention(
let start = start.bias_right(&snapshot);
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
- let placeholder = FoldPlaceholder {
- render: render_fold_icon_button(
- crease_icon_path,
- crease_label,
- editor_entity.downgrade(),
- ),
- merge_adjacent: false,
- ..Default::default()
- };
-
- let render_trailer =
- move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
-
- let crease = Crease::inline(
+ let crease = crease_for_mention(
+ crease_label,
+ crease_icon_path,
start..end,
- placeholder.clone(),
- fold_toggle("mention"),
- render_trailer,
+ editor_entity.downgrade(),
);
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(
icon_path: SharedString,
label: SharedString,
@@ -655,6 +805,7 @@ fn fold_toggle(
pub enum MentionLink {
File(ProjectPath, Entry),
Symbol(ProjectPath, String),
+ Selection(ProjectPath, Range<usize>),
Fetch(String),
Thread(ThreadId),
Rules(UserPromptId),
@@ -663,6 +814,7 @@ pub enum MentionLink {
impl MentionLink {
const FILE: &str = "@file";
const SYMBOL: &str = "@symbol";
+ const SELECTION: &str = "@selection";
const THREAD: &str = "@thread";
const FETCH: &str = "@fetch";
const RULES: &str = "@rules";
@@ -672,8 +824,9 @@ impl MentionLink {
pub fn is_valid(url: &str) -> bool {
url.starts_with(Self::FILE)
|| url.starts_with(Self::SYMBOL)
- || url.starts_with(Self::THREAD)
|| url.starts_with(Self::FETCH)
+ || url.starts_with(Self::SELECTION)
+ || url.starts_with(Self::THREAD)
|| 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 {
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
}
@@ -739,6 +905,20 @@ impl MentionLink {
let project_path = extract_project_path_from_link(path, workspace, cx)?;
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 => {
let thread_id = ThreadId::from(argument);
Some(MentionLink::Thread(thread_id))
@@ -1,22 +1,23 @@
use std::cell::RefCell;
use std::ops::Range;
-use std::path::Path;
+use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::Result;
-use editor::{CompletionProvider, Editor, ExcerptId};
+use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
use file_icons::FileIcons;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use http_client::HttpClientWithUrl;
+use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
use prompt_store::PromptId;
use rope::Point;
-use text::{Anchor, ToPoint};
+use text::{Anchor, OffsetRangeExt, ToPoint};
use ui::prelude::*;
use workspace::Workspace;
@@ -32,8 +33,8 @@ use super::rules_context_picker::{RulesContextEntry, search_rules};
use super::symbol_context_picker::SymbolMatch;
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
use super::{
- ContextPickerMode, MentionLink, RecentEntry, recent_context_picker_entries,
- supported_context_picker_modes,
+ ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
+ available_context_picker_entries, recent_context_picker_entries, selection_ranges,
};
pub(crate) enum Match {
@@ -42,19 +43,19 @@ pub(crate) enum Match {
Thread(ThreadMatch),
Fetch(SharedString),
Rules(RulesContextEntry),
- Mode(ModeMatch),
+ Entry(EntryMatch),
}
-pub struct ModeMatch {
+pub struct EntryMatch {
mat: Option<StringMatch>,
- mode: ContextPickerMode,
+ entry: ContextPickerEntry,
}
impl Match {
pub fn score(&self) -> f64 {
match self {
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::Symbol(_) => 1.,
Match::Fetch(_) => 1.,
@@ -162,9 +163,14 @@ fn search(
.collect::<Vec<_>>();
matches.extend(
- supported_context_picker_modes(&thread_store)
+ available_context_picker_entries(&thread_store, &workspace, cx)
.into_iter()
- .map(|mode| Match::Mode(ModeMatch { mode, mat: None })),
+ .map(|mode| {
+ Match::Entry(EntryMatch {
+ entry: mode,
+ mat: None,
+ })
+ }),
);
Task::ready(matches)
@@ -174,11 +180,11 @@ fn search(
let search_files_task =
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
- let modes = supported_context_picker_modes(&thread_store);
- let mode_candidates = modes
+ let entries = available_context_picker_entries(&thread_store, &workspace, cx);
+ let entry_candidates = entries
.iter()
.enumerate()
- .map(|(ix, mode)| StringMatchCandidate::new(ix, mode.mention_prefix()))
+ .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
.collect::<Vec<_>>();
cx.background_spawn(async move {
@@ -188,8 +194,8 @@ fn search(
.map(Match::File)
.collect::<Vec<_>>();
- let mode_matches = fuzzy::match_strings(
- &mode_candidates,
+ let entry_matches = fuzzy::match_strings(
+ &entry_candidates,
&query,
false,
100,
@@ -198,9 +204,9 @@ fn search(
)
.await;
- matches.extend(mode_matches.into_iter().map(|mat| {
- Match::Mode(ModeMatch {
- mode: modes[mat.candidate_id],
+ matches.extend(entry_matches.into_iter().map(|mat| {
+ Match::Entry(EntryMatch {
+ entry: entries[mat.candidate_id],
mat: Some(mat),
})
}));
@@ -240,19 +246,137 @@ impl ContextPickerCompletionProvider {
}
}
- fn completion_for_mode(source_range: Range<Anchor>, mode: ContextPickerMode) -> Completion {
- Completion {
- replace_range: source_range.clone(),
- new_text: format!("@{} ", mode.mention_prefix()),
- label: CodeLabel::plain(mode.label().to_string(), None),
- icon_path: Some(mode.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(Arc::new(|_, _, _| true)),
+ fn completion_for_entry(
+ 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(),
+ new_text: format!("@{} ", mode.keyword()),
+ label: CodeLabel::plain(mode.label().to_string(), None),
+ icon_path: Some(mode.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(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(),
http_client.clone(),
)),
- Match::Mode(ModeMatch { mode, .. }) => {
- Some(Self::completion_for_mode(source_range.clone(), mode))
- }
+ Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
+ entry,
+ excerpt_id,
+ source_range.clone(),
+ editor.clone(),
+ context_store.clone(),
+ &workspace,
+ cx,
+ ),
})
.collect()
})?))
@@ -18,7 +18,7 @@ use util::{ResultExt as _, maybe};
use crate::ThreadStore;
use crate::context::{
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
- ExcerptContext, FetchedUrlContext, FileContext, ImageContext, RulesContext, SymbolContext,
+ FetchedUrlContext, FileContext, ImageContext, RulesContext, SelectionContext, SymbolContext,
ThreadContext,
};
use crate::context_strip::SuggestedContext;
@@ -476,10 +476,10 @@ impl ContextStore {
})
}
- pub fn add_excerpt(
+ pub fn add_selection(
&mut self,
- range: Range<Anchor>,
buffer: Entity<Buffer>,
+ range: Range<Anchor>,
cx: &mut Context<ContextStore>,
) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
@@ -490,14 +490,14 @@ impl ContextStore {
let context_buffer = context_buffer_task.await;
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(())
})
}
- fn insert_excerpt(
+ fn insert_selection(
&mut self,
context_buffer: ContextBuffer,
range: Range<Anchor>,
@@ -505,12 +505,13 @@ impl ContextStore {
cx: &mut Context<Self>,
) {
let id = self.next_context_id.post_inc();
- self.context.push(AssistantContext::Excerpt(ExcerptContext {
- id,
- range,
- line_range,
- context_buffer,
- }));
+ self.context
+ .push(AssistantContext::Selection(SelectionContext {
+ id,
+ range,
+ line_range,
+ context_buffer,
+ }));
cx.notify();
}
@@ -563,7 +564,7 @@ impl ContextStore {
self.symbol_buffers.remove(&symbol.context_symbol.id);
self.symbols.retain(|_, context_id| *context_id != id);
}
- AssistantContext::Excerpt(_) => {}
+ AssistantContext::Selection(_) => {}
AssistantContext::FetchedUrl(_) => {
self.fetched_urls.retain(|_, context_id| *context_id != id);
}
@@ -699,7 +700,7 @@ impl ContextStore {
}
AssistantContext::Directory(_)
| AssistantContext::Symbol(_)
- | AssistantContext::Excerpt(_)
+ | AssistantContext::Selection(_)
| AssistantContext::FetchedUrl(_)
| AssistantContext::Thread(_)
| AssistantContext::Rules(_)
@@ -914,13 +915,13 @@ pub fn refresh_context_store_text(
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.
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();
- return refresh_excerpt_text(context_store, excerpt_context, cx);
+ return refresh_selection_text(context_store, selection_context, cx);
}
}
AssistantContext::Thread(thread_context) => {
@@ -1042,26 +1043,27 @@ fn refresh_symbol_text(
}
}
-fn refresh_excerpt_text(
+fn refresh_selection_text(
context_store: Entity<ContextStore>,
- excerpt_context: &ExcerptContext,
+ selection_context: &SelectionContext,
cx: &App,
) -> Option<Task<()>> {
- let id = excerpt_context.id;
- let range = excerpt_context.range.clone();
- let task = refresh_context_excerpt(&excerpt_context.context_buffer, range.clone(), cx);
+ let id = selection_context.id;
+ let range = selection_context.range.clone();
+ let task = refresh_context_excerpt(&selection_context.context_buffer, range.clone(), cx);
if let Some(task) = task {
Some(cx.spawn(async move |cx| {
let (line_range, context_buffer) = task.await;
context_store
.update(cx, |context_store, _| {
- let new_excerpt_context = ExcerptContext {
+ let new_selection_context = SelectionContext {
id,
range,
line_range,
context_buffer,
};
- context_store.replace_context(AssistantContext::Excerpt(new_excerpt_context));
+ context_store
+ .replace_context(AssistantContext::Selection(new_selection_context));
})
.ok();
}))
@@ -298,7 +298,7 @@ impl MessageEditor {
.filter(|ctx| {
matches!(
ctx,
- AssistantContext::Excerpt(_) | AssistantContext::Image(_)
+ AssistantContext::Selection(_) | AssistantContext::Image(_)
)
})
.map(|ctx| ctx.id())
@@ -780,9 +780,9 @@ impl Thread {
cx,
);
}
- AssistantContext::Excerpt(excerpt_context) => {
+ AssistantContext::Selection(selection_context) => {
log.buffer_added_as_context(
- excerpt_context.context_buffer.buffer.clone(),
+ selection_context.context_buffer.buffer.clone(),
cx,
);
}
@@ -3,7 +3,7 @@ use std::{rc::Rc, time::Duration};
use file_icons::FileIcons;
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 language_model::LanguageModelImage;
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
@@ -168,11 +168,16 @@ impl RenderOnce for ContextPill {
.map(|element| match &context.status {
ContextStatus::Ready => element
.when_some(
- context.show_preview.as_ref(),
- |element, show_preview| {
+ context.render_preview.as_ref(),
+ |element, render_preview| {
element.hoverable_tooltip({
- let show_preview = show_preview.clone();
- move |window, cx| show_preview(window, cx)
+ let render_preview = render_preview.clone();
+ move |_, cx| {
+ cx.new(|_| ContextPillPreview {
+ render_preview: render_preview.clone(),
+ })
+ .into()
+ }
})
},
)
@@ -266,7 +271,7 @@ pub struct AddedContext {
pub tooltip: Option<SharedString>,
pub icon_path: Option<SharedString>,
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 {
@@ -292,7 +297,7 @@ impl AddedContext {
tooltip: Some(full_path_string),
icon_path: FileIcons::get_icon(&full_path, cx),
status: ContextStatus::Ready,
- show_preview: None,
+ render_preview: None,
}
}
@@ -323,7 +328,7 @@ impl AddedContext {
tooltip: Some(full_path_string),
icon_path: None,
status: ContextStatus::Ready,
- show_preview: None,
+ render_preview: None,
}
}
@@ -335,11 +340,11 @@ impl AddedContext {
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
- show_preview: None,
+ render_preview: None,
},
- AssistantContext::Excerpt(excerpt_context) => {
- let full_path = excerpt_context.context_buffer.full_path(cx);
+ AssistantContext::Selection(selection_context) => {
+ let full_path = selection_context.context_buffer.full_path(cx);
let mut full_path_string = full_path.to_string_lossy().into_owned();
let mut name = full_path
.file_name()
@@ -348,8 +353,8 @@ impl AddedContext {
let line_range_text = format!(
" ({}-{})",
- excerpt_context.line_range.start.row + 1,
- excerpt_context.line_range.end.row + 1
+ selection_context.line_range.start.row + 1,
+ selection_context.line_range.end.row + 1
);
full_path_string.push_str(&line_range_text);
@@ -361,14 +366,25 @@ impl AddedContext {
.map(|n| n.to_string_lossy().into_owned().into());
AddedContext {
- id: excerpt_context.id,
- kind: ContextKind::File,
+ id: selection_context.id,
+ kind: ContextKind::Selection,
name: name.into(),
parent,
- tooltip: Some(full_path_string.into()),
+ tooltip: None,
icon_path: FileIcons::get_icon(&full_path, cx),
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,
icon_path: None,
status: ContextStatus::Ready,
- show_preview: None,
+ render_preview: None,
},
AssistantContext::Thread(thread_context) => AddedContext {
@@ -401,7 +417,7 @@ impl AddedContext {
} else {
ContextStatus::Ready
},
- show_preview: None,
+ render_preview: None,
},
AssistantContext::Rules(user_rules_context) => AddedContext {
@@ -412,7 +428,7 @@ impl AddedContext {
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
- show_preview: None,
+ render_preview: None,
},
AssistantContext::Image(image_context) => AddedContext {
@@ -433,13 +449,13 @@ impl AddedContext {
} else {
ContextStatus::Ready
},
- show_preview: Some(Rc::new({
+ render_preview: Some(Rc::new({
let image = image_context.original_image.clone();
- move |_, cx| {
- cx.new(|_| ImagePreview {
- image: image.clone(),
- })
- .into()
+ move |_, _| {
+ gpui::img(image.clone())
+ .max_w_96()
+ .max_h_96()
+ .into_any_element()
}
})),
},
@@ -447,17 +463,17 @@ impl AddedContext {
}
}
-struct ImagePreview {
- image: Arc<Image>,
+struct ContextPillPreview {
+ 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 {
- tooltip_container(window, cx, move |this, _, _| {
+ tooltip_container(window, cx, move |this, window, cx| {
this.occlude()
.on_mouse_move(|_, _, 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();
}
+ 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 {
let pending_nonempty_selection = match self.selections.pending_anchor() {
Some(Selection { start, end, .. }) => start != end,