Detailed changes
@@ -756,6 +756,10 @@ impl ActiveThread {
this
}
+ pub fn context_store(&self) -> &Entity<ContextStore> {
+ &self.context_store
+ }
+
pub fn thread(&self) -> &Entity<Thread> {
&self.thread
}
@@ -3145,28 +3149,21 @@ pub(crate) fn open_context(
.start
.to_point(&snapshot);
- let open_task = workspace.update(cx, |workspace, cx| {
- workspace.open_path(project_path, None, true, window, cx)
- });
- window
- .spawn(cx, async move |cx| {
- if let Some(active_editor) = open_task
- .await
- .log_err()
- .and_then(|item| item.downcast::<Editor>())
- {
- active_editor
- .downgrade()
- .update_in(cx, |editor, window, cx| {
- editor.go_to_singleton_buffer_point(
- target_position,
- window,
- cx,
- );
- })
- .log_err();
- }
- })
+ open_editor_at_position(project_path, target_position, &workspace, window, cx)
+ .detach();
+ }
+ }
+ AssistantContext::Excerpt(excerpt_context) => {
+ if let Some(project_path) = excerpt_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);
+
+ open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
}
}
@@ -3187,3 +3184,29 @@ pub(crate) fn open_context(
}
}
}
+
+fn open_editor_at_position(
+ project_path: project::ProjectPath,
+ target_position: Point,
+ workspace: &Entity<Workspace>,
+ window: &mut Window,
+ cx: &mut App,
+) -> Task<()> {
+ let open_task = workspace.update(cx, |workspace, cx| {
+ workspace.open_path(project_path, None, true, window, cx)
+ });
+ window.spawn(cx, async move |cx| {
+ if let Some(active_editor) = open_task
+ .await
+ .log_err()
+ .and_then(|item| item.downcast::<Editor>())
+ {
+ active_editor
+ .downgrade()
+ .update_in(cx, |editor, window, cx| {
+ editor.go_to_singleton_buffer_point(target_position, window, cx);
+ })
+ .log_err();
+ }
+ })
+}
@@ -1,3 +1,4 @@
+use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -12,7 +13,7 @@ use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
-use editor::{Editor, EditorEvent, MultiBuffer};
+use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
@@ -112,7 +113,9 @@ enum ActiveView {
change_title_editor: Entity<Editor>,
_subscriptions: Vec<gpui::Subscription>,
},
- PromptEditor,
+ PromptEditor {
+ context_editor: Entity<ContextEditor>,
+ },
History,
Configuration,
}
@@ -184,7 +187,6 @@ pub struct AssistantPanel {
message_editor: Entity<MessageEditor>,
_active_thread_subscriptions: Vec<Subscription>,
context_store: Entity<assistant_context_editor::ContextStore>,
- context_editor: Option<Entity<ContextEditor>>,
configuration: Option<Entity<AssistantConfiguration>>,
configuration_subscription: Option<Subscription>,
local_timezone: UtcOffset,
@@ -316,7 +318,6 @@ impl AssistantPanel {
message_editor_subscription,
],
context_store,
- context_editor: None,
configuration: None,
configuration_subscription: None,
local_timezone: UtcOffset::from_whole_seconds(
@@ -453,8 +454,6 @@ impl AssistantPanel {
}
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.set_active_view(ActiveView::PromptEditor, window, cx);
-
let context = self
.context_store
.update(cx, |context_store, cx| context_store.create(cx));
@@ -462,7 +461,7 @@ impl AssistantPanel {
.log_err()
.flatten();
- self.context_editor = Some(cx.new(|cx| {
+ let context_editor = cx.new(|cx| {
let mut editor = ContextEditor::for_context(
context,
self.fs.clone(),
@@ -474,11 +473,16 @@ impl AssistantPanel {
);
editor.insert_default_prompt(window, cx);
editor
- }));
+ });
- if let Some(context_editor) = self.context_editor.as_ref() {
- context_editor.focus_handle(cx).focus(window);
- }
+ self.set_active_view(
+ ActiveView::PromptEditor {
+ context_editor: context_editor.clone(),
+ },
+ window,
+ cx,
+ );
+ context_editor.focus_handle(cx).focus(window);
}
fn deploy_prompt_library(
@@ -545,8 +549,13 @@ impl AssistantPanel {
cx,
)
});
- this.set_active_view(ActiveView::PromptEditor, window, cx);
- this.context_editor = Some(editor);
+ this.set_active_view(
+ ActiveView::PromptEditor {
+ context_editor: editor,
+ },
+ window,
+ cx,
+ );
anyhow::Ok(())
})??;
@@ -777,8 +786,15 @@ impl AssistantPanel {
.update(cx, |this, cx| this.delete_thread(thread_id, cx))
}
+ pub(crate) fn has_active_thread(&self) -> bool {
+ matches!(self.active_view, ActiveView::Thread { .. })
+ }
+
pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
- self.context_editor.clone()
+ match &self.active_view {
+ ActiveView::PromptEditor { context_editor } => Some(context_editor.clone()),
+ _ => None,
+ }
}
pub(crate) fn delete_context(
@@ -816,16 +832,10 @@ impl AssistantPanel {
impl Focusable for AssistantPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle {
- match self.active_view {
+ match &self.active_view {
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
ActiveView::History => self.history.focus_handle(cx),
- ActiveView::PromptEditor => {
- if let Some(context_editor) = self.context_editor.as_ref() {
- context_editor.focus_handle(cx)
- } else {
- cx.focus_handle()
- }
- }
+ ActiveView::PromptEditor { context_editor } => context_editor.focus_handle(cx),
ActiveView::Configuration => {
if let Some(configuration) = self.configuration.as_ref() {
configuration.focus_handle(cx)
@@ -949,15 +959,8 @@ impl AssistantPanel {
.into_any_element()
}
}
- ActiveView::PromptEditor => {
- let title = self
- .context_editor
- .as_ref()
- .map(|context_editor| {
- SharedString::from(context_editor.read(cx).title(cx).to_string())
- })
- .unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
-
+ ActiveView::PromptEditor { context_editor } => {
+ let title = SharedString::from(context_editor.read(cx).title(cx).to_string());
Label::new(title).ml_2().truncate().into_any_element()
}
ActiveView::History => Label::new("History").truncate().into_any_element(),
@@ -984,7 +987,7 @@ impl AssistantPanel {
let show_token_count = match &self.active_view {
ActiveView::Thread { .. } => !is_empty,
- ActiveView::PromptEditor => self.context_editor.is_some(),
+ ActiveView::PromptEditor { .. } => true,
_ => false,
};
@@ -1156,7 +1159,7 @@ impl AssistantPanel {
let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
- match self.active_view {
+ match &self.active_view {
ActiveView::Thread { .. } => {
if total_token_usage.total == 0 {
return None;
@@ -1229,9 +1232,8 @@ impl AssistantPanel {
Some(token_count)
}
- ActiveView::PromptEditor => {
- let editor = self.context_editor.as_ref()?;
- let element = render_remaining_tokens(editor, cx)?;
+ ActiveView::PromptEditor { context_editor } => {
+ let element = render_remaining_tokens(context_editor, cx)?;
Some(element.into_any_element())
}
@@ -1769,7 +1771,7 @@ impl AssistantPanel {
fn key_context(&self) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("AgentPanel");
- if matches!(self.active_view, ActiveView::PromptEditor) {
+ if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
key_context.add("prompt_editor");
}
key_context
@@ -1797,13 +1799,13 @@ impl Render for AssistantPanel {
.on_action(cx.listener(Self::open_agent_diff))
.on_action(cx.listener(Self::go_back))
.child(self.render_toolbar(window, cx))
- .map(|parent| match self.active_view {
+ .map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent
.child(self.render_active_thread_or_empty_state(window, cx))
.child(h_flex().child(self.message_editor.clone()))
.children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()),
- ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
+ ActiveView::PromptEditor { context_editor } => parent.child(context_editor.clone()),
ActiveView::Configuration => parent.children(self.configuration.clone()),
})
}
@@ -1868,7 +1870,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
cx: &mut Context<Workspace>,
) -> Option<Entity<ContextEditor>> {
let panel = workspace.panel::<AssistantPanel>(cx)?;
- panel.update(cx, |panel, _cx| panel.context_editor.clone())
+ panel.read(cx).active_context_editor()
}
fn open_saved_context(
@@ -1900,7 +1902,8 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn quote_selection(
&self,
workspace: &mut Workspace,
- creases: Vec<(String, String)>,
+ selection_ranges: Vec<Range<Anchor>>,
+ buffer: Entity<MultiBuffer>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
@@ -1916,9 +1919,40 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
// Wait to create a new context until the workspace is no longer
// being updated.
cx.defer_in(window, move |panel, window, cx| {
- if let Some(context) = panel.active_context_editor() {
- context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
- };
+ if panel.has_active_thread() {
+ panel.thread.update(cx, |thread, cx| {
+ thread.context_store().update(cx, |store, cx| {
+ let buffer = buffer.read(cx);
+ let selection_ranges = selection_ranges
+ .into_iter()
+ .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<_>>();
+
+ for (buffer, range) in selection_ranges {
+ store.add_excerpt(range, buffer, cx).detach_and_log_err(cx);
+ }
+ })
+ })
+ } else if let Some(context_editor) = panel.active_context_editor() {
+ let snapshot = buffer.read(cx).snapshot(cx);
+ let selection_ranges = selection_ranges
+ .into_iter()
+ .map(|range| range.to_point(&snapshot))
+ .collect::<Vec<_>>();
+
+ context_editor.update(cx, |context_editor, cx| {
+ context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
+ });
+ }
});
});
}
@@ -4,6 +4,7 @@ use gpui::{App, Entity, SharedString};
use language::{Buffer, File};
use language_model::LanguageModelRequestMessage;
use project::{ProjectPath, Worktree};
+use rope::Point;
use serde::{Deserialize, Serialize};
use text::{Anchor, BufferId};
use ui::IconName;
@@ -23,6 +24,7 @@ pub enum ContextKind {
File,
Directory,
Symbol,
+ Excerpt,
FetchedUrl,
Thread,
}
@@ -33,6 +35,7 @@ impl ContextKind {
ContextKind::File => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::Symbol => IconName::Code,
+ ContextKind::Excerpt => IconName::Code,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageBubbles,
}
@@ -46,6 +49,7 @@ pub enum AssistantContext {
Symbol(SymbolContext),
FetchedUrl(FetchedUrlContext),
Thread(ThreadContext),
+ Excerpt(ExcerptContext),
}
impl AssistantContext {
@@ -56,6 +60,7 @@ impl AssistantContext {
Self::Symbol(symbol) => symbol.id,
Self::FetchedUrl(url) => url.id,
Self::Thread(thread) => thread.id,
+ Self::Excerpt(excerpt) => excerpt.id,
}
}
}
@@ -155,6 +160,14 @@ pub struct ContextSymbolId {
pub range: Range<Anchor>,
}
+#[derive(Debug, Clone)]
+pub struct ExcerptContext {
+ pub id: ContextId,
+ pub range: Range<Anchor>,
+ pub line_range: Range<Point>,
+ pub context_buffer: ContextBuffer,
+}
+
/// Formats a collection of contexts into a string representation
pub fn format_context_as_string<'a>(
contexts: impl Iterator<Item = &'a AssistantContext>,
@@ -163,6 +176,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 fetch_context = Vec::new();
let mut thread_context = Vec::new();
@@ -171,6 +185,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::FetchedUrl(context) => fetch_context.push(context),
AssistantContext::Thread(context) => thread_context.push(context),
}
@@ -179,6 +194,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()
&& fetch_context.is_empty()
&& thread_context.is_empty()
{
@@ -216,6 +232,15 @@ 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 {
+ result.push_str(&context.context_buffer.text);
+ result.push('\n');
+ }
+ result.push_str("</excerpts>\n");
+ }
+
if !fetch_context.is_empty() {
result.push_str("<fetched_urls>\n");
for context in &fetch_context {
@@ -9,14 +9,14 @@ use futures::{self, Future, FutureExt, future};
use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
use language::{Buffer, File};
use project::{Project, ProjectItem, ProjectPath, Worktree};
-use rope::Rope;
+use rope::{Point, Rope};
use text::{Anchor, BufferId, OffsetRangeExt};
use util::{ResultExt as _, maybe};
use crate::ThreadStore;
use crate::context::{
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
- FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
+ ExcerptContext, FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
};
use crate::context_strip::SuggestedContext;
use crate::thread::{Thread, ThreadId};
@@ -110,7 +110,7 @@ impl ContextStore {
}
let (buffer_info, text_task) =
- this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
+ this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
let text = text_task.await;
@@ -129,7 +129,7 @@ impl ContextStore {
) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
let (buffer_info, text_task) =
- this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
+ this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
let text = text_task.await;
@@ -206,7 +206,7 @@ impl ContextStore {
// Skip all binary files and other non-UTF8 files
for buffer in buffers.into_iter().flatten() {
if let Some((buffer_info, text_task)) =
- collect_buffer_info_and_text(buffer, None, cx).log_err()
+ collect_buffer_info_and_text(buffer, cx).log_err()
{
buffer_infos.push(buffer_info);
text_tasks.push(text_task);
@@ -290,11 +290,14 @@ impl ContextStore {
}
}
- let (buffer_info, collect_content_task) =
- match collect_buffer_info_and_text(buffer, Some(symbol_enclosing_range.clone()), cx) {
- Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
- Err(err) => return Task::ready(Err(err)),
- };
+ let (buffer_info, collect_content_task) = match collect_buffer_info_and_text_for_range(
+ buffer,
+ symbol_enclosing_range.clone(),
+ cx,
+ ) {
+ Ok((_, buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
+ Err(err) => return Task::ready(Err(err)),
+ };
cx.spawn(async move |this, cx| {
let content = collect_content_task.await;
@@ -416,6 +419,49 @@ impl ContextStore {
cx.notify();
}
+ pub fn add_excerpt(
+ &mut self,
+ range: Range<Anchor>,
+ buffer: Entity<Buffer>,
+ cx: &mut Context<ContextStore>,
+ ) -> Task<Result<()>> {
+ cx.spawn(async move |this, cx| {
+ let (line_range, buffer_info, text_task) = this.update(cx, |_, cx| {
+ collect_buffer_info_and_text_for_range(buffer, range.clone(), cx)
+ })??;
+
+ let text = text_task.await;
+
+ this.update(cx, |this, cx| {
+ this.insert_excerpt(
+ make_context_buffer(buffer_info, text),
+ range,
+ line_range,
+ cx,
+ )
+ })?;
+
+ anyhow::Ok(())
+ })
+ }
+
+ fn insert_excerpt(
+ &mut self,
+ context_buffer: ContextBuffer,
+ range: Range<Anchor>,
+ line_range: Range<Point>,
+ cx: &mut Context<Self>,
+ ) {
+ let id = self.next_context_id.post_inc();
+ self.context.push(AssistantContext::Excerpt(ExcerptContext {
+ id,
+ range,
+ line_range,
+ context_buffer,
+ }));
+ cx.notify();
+ }
+
pub fn accept_suggested_context(
&mut self,
suggested: &SuggestedContext,
@@ -465,6 +511,7 @@ impl ContextStore {
self.symbol_buffers.remove(&symbol.context_symbol.id);
self.symbols.retain(|_, context_id| *context_id != id);
}
+ AssistantContext::Excerpt(_) => {}
AssistantContext::FetchedUrl(_) => {
self.fetched_urls.retain(|_, context_id| *context_id != id);
}
@@ -592,6 +639,7 @@ impl ContextStore {
}
AssistantContext::Directory(_)
| AssistantContext::Symbol(_)
+ | AssistantContext::Excerpt(_)
| AssistantContext::FetchedUrl(_)
| AssistantContext::Thread(_) => None,
})
@@ -643,41 +691,78 @@ fn make_context_symbol(
}
}
+fn collect_buffer_info_and_text_for_range(
+ buffer: Entity<Buffer>,
+ range: Range<Anchor>,
+ cx: &App,
+) -> Result<(Range<Point>, BufferInfo, Task<SharedString>)> {
+ let content = buffer
+ .read(cx)
+ .text_for_range(range.clone())
+ .collect::<Rope>();
+
+ let line_range = range.to_point(&buffer.read(cx).snapshot());
+
+ let buffer_info = collect_buffer_info(buffer, cx)?;
+ let full_path = buffer_info.file.full_path(cx);
+
+ let text_task = cx.background_spawn({
+ let line_range = line_range.clone();
+ async move { to_fenced_codeblock(&full_path, content, Some(line_range)) }
+ });
+
+ Ok((line_range, buffer_info, text_task))
+}
+
fn collect_buffer_info_and_text(
buffer: Entity<Buffer>,
- range: Option<Range<Anchor>>,
cx: &App,
) -> Result<(BufferInfo, Task<SharedString>)> {
+ let content = buffer.read(cx).as_rope().clone();
+
+ let buffer_info = collect_buffer_info(buffer, cx)?;
+ let full_path = buffer_info.file.full_path(cx);
+
+ let text_task =
+ cx.background_spawn(async move { to_fenced_codeblock(&full_path, content, None) });
+
+ Ok((buffer_info, text_task))
+}
+
+fn collect_buffer_info(buffer: Entity<Buffer>, cx: &App) -> Result<BufferInfo> {
let buffer_ref = buffer.read(cx);
let file = buffer_ref.file().context("file context must have a path")?;
// Important to collect version at the same time as content so that staleness logic is correct.
let version = buffer_ref.version();
- let content = if let Some(range) = range {
- buffer_ref.text_for_range(range).collect::<Rope>()
- } else {
- buffer_ref.as_rope().clone()
- };
- let buffer_info = BufferInfo {
+ Ok(BufferInfo {
buffer,
id: buffer_ref.remote_id(),
file: file.clone(),
version,
- };
-
- let full_path = file.full_path(cx);
- let text_task = cx.background_spawn(async move { to_fenced_codeblock(&full_path, content) });
-
- Ok((buffer_info, text_task))
+ })
}
-fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
+fn to_fenced_codeblock(
+ path: &Path,
+ content: Rope,
+ line_range: Option<Range<Point>>,
+) -> SharedString {
+ let line_range_text = line_range.map(|range| {
+ if range.start.row == range.end.row {
+ format!(":{}", range.start.row + 1)
+ } else {
+ format!(":{}-{}", range.start.row + 1, range.end.row + 1)
+ }
+ });
+
let path_extension = path.extension().and_then(|ext| ext.to_str());
let path_string = path.to_string_lossy();
let capacity = 3
+ path_extension.map_or(0, |extension| extension.len() + 1)
+ path_string.len()
+ + line_range_text.as_ref().map_or(0, |text| text.len())
+ 1
+ content.len()
+ 5;
@@ -691,6 +776,10 @@ fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
}
buffer.push_str(&path_string);
+ if let Some(line_range_text) = line_range_text {
+ buffer.push_str(&line_range_text);
+ }
+
buffer.push('\n');
for chunk in content.chunks() {
buffer.push_str(&chunk);
@@ -769,6 +858,14 @@ pub fn refresh_context_store_text(
return refresh_symbol_text(context_store, symbol_context, cx);
}
}
+ AssistantContext::Excerpt(excerpt_context) => {
+ if changed_buffers.is_empty()
+ || changed_buffers.contains(&excerpt_context.context_buffer.buffer)
+ {
+ let context_store = context_store.clone();
+ return refresh_excerpt_text(context_store, excerpt_context, cx);
+ }
+ }
AssistantContext::Thread(thread_context) => {
if changed_buffers.is_empty() {
let context_store = context_store.clone();
@@ -880,6 +977,34 @@ fn refresh_symbol_text(
}
}
+fn refresh_excerpt_text(
+ context_store: Entity<ContextStore>,
+ excerpt_context: &ExcerptContext,
+ 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);
+ 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 {
+ id,
+ range,
+ line_range,
+ context_buffer,
+ };
+ context_store.replace_context(AssistantContext::Excerpt(new_excerpt_context));
+ })
+ .ok();
+ }))
+ } else {
+ None
+ }
+}
+
fn refresh_thread_text(
context_store: Entity<ContextStore>,
thread_context: &ThreadContext,
@@ -908,13 +1033,29 @@ fn refresh_context_buffer(
let buffer = context_buffer.buffer.read(cx);
if buffer.version.changed_since(&context_buffer.version) {
let (buffer_info, text_task) =
- collect_buffer_info_and_text(context_buffer.buffer.clone(), None, cx).log_err()?;
+ collect_buffer_info_and_text(context_buffer.buffer.clone(), cx).log_err()?;
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
} else {
None
}
}
+fn refresh_context_excerpt(
+ context_buffer: &ContextBuffer,
+ range: Range<Anchor>,
+ cx: &App,
+) -> Option<impl Future<Output = (Range<Point>, ContextBuffer)> + use<>> {
+ let buffer = context_buffer.buffer.read(cx);
+ if buffer.version.changed_since(&context_buffer.version) {
+ let (line_range, buffer_info, text_task) =
+ collect_buffer_info_and_text_for_range(context_buffer.buffer.clone(), range, cx)
+ .log_err()?;
+ Some(text_task.map(move |text| (line_range, make_context_buffer(buffer_info, text))))
+ } else {
+ None
+ }
+}
+
fn refresh_context_symbol(
context_symbol: &ContextSymbol,
cx: &App,
@@ -922,9 +1063,9 @@ fn refresh_context_symbol(
let buffer = context_symbol.buffer.read(cx);
let project_path = buffer.project_path(cx)?;
if buffer.version.changed_since(&context_symbol.buffer_version) {
- let (buffer_info, text_task) = collect_buffer_info_and_text(
+ let (_, buffer_info, text_task) = collect_buffer_info_and_text_for_range(
context_symbol.buffer.clone(),
- Some(context_symbol.enclosing_range.clone()),
+ context_symbol.enclosing_range.clone(),
cx,
)
.log_err()?;
@@ -725,6 +725,12 @@ impl Thread {
cx,
);
}
+ AssistantContext::Excerpt(excerpt_context) => {
+ log.buffer_added_as_context(
+ excerpt_context.context_buffer.buffer.clone(),
+ cx,
+ );
+ }
AssistantContext::FetchedUrl(_) | AssistantContext::Thread(_) => {}
}
}
@@ -299,6 +299,39 @@ impl AddedContext {
summarizing: false,
},
+ AssistantContext::Excerpt(excerpt_context) => {
+ let full_path = excerpt_context.context_buffer.file.full_path(cx);
+ let mut full_path_string = full_path.to_string_lossy().into_owned();
+ let mut name = full_path
+ .file_name()
+ .map(|n| n.to_string_lossy().into_owned())
+ .unwrap_or_else(|| full_path_string.clone());
+
+ let line_range_text = format!(
+ " ({}-{})",
+ excerpt_context.line_range.start.row + 1,
+ excerpt_context.line_range.end.row + 1
+ );
+
+ full_path_string.push_str(&line_range_text);
+ name.push_str(&line_range_text);
+
+ let parent = full_path
+ .parent()
+ .and_then(|p| p.file_name())
+ .map(|n| n.to_string_lossy().into_owned().into());
+
+ AddedContext {
+ id: excerpt_context.id,
+ kind: ContextKind::File, // Use File icon for excerpts
+ name: name.into(),
+ parent,
+ tooltip: Some(full_path_string.into()),
+ icon_path: FileIcons::get_icon(&full_path, cx),
+ summarizing: false,
+ }
+ }
+
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
id: fetched_url_context.id,
kind: ContextKind::FetchedUrl,
@@ -13,7 +13,7 @@ use assistant_context_editor::{
use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use client::{Client, Status, proto};
-use editor::{Editor, EditorEvent};
+use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
@@ -28,9 +28,12 @@ use language_model::{
use project::Project;
use prompt_library::{PromptLibrary, open_prompt_library};
use prompt_store::PromptBuilder;
+
use search::{BufferSearchBar, buffer_search::DivRegistrar};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
+
+use std::ops::Range;
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
@@ -1413,7 +1416,8 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn quote_selection(
&self,
workspace: &mut Workspace,
- creases: Vec<(String, String)>,
+ selection_ranges: Vec<Range<Anchor>>,
+ buffer: Entity<MultiBuffer>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
@@ -1425,6 +1429,12 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
}
+ let snapshot = buffer.read(cx).snapshot(cx);
+ let selection_ranges = selection_ranges
+ .into_iter()
+ .map(|range| range.to_point(&snapshot))
+ .collect::<Vec<_>>();
+
panel.update(cx, |_, cx| {
// Wait to create a new context until the workspace is no longer
// being updated.
@@ -1433,7 +1443,9 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
.active_context_editor(cx)
.or_else(|| panel.new_context(window, cx))
{
- context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
+ context.update(cx, |context, cx| {
+ context.quote_ranges(selection_ranges, snapshot, window, cx)
+ });
};
});
});
@@ -8,8 +8,8 @@ use assistant_slash_commands::{
use client::{proto, zed_urls};
use collections::{BTreeSet, HashMap, HashSet, hash_map};
use editor::{
- Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
- ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
+ Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot,
+ ProposedChangeLocation, ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
actions::{MoveToEndOfLine, Newline, ShowCompletions},
display_map::{
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
@@ -155,7 +155,8 @@ pub trait AssistantPanelDelegate {
fn quote_selection(
&self,
workspace: &mut Workspace,
- creases: Vec<(String, String)>,
+ selection_ranges: Vec<Range<Anchor>>,
+ buffer: Entity<MultiBuffer>,
window: &mut Window,
cx: &mut Context<Workspace>,
);
@@ -1800,23 +1801,42 @@ impl ContextEditor {
return;
};
- let Some(creases) = selections_creases(workspace, cx) else {
+ let Some((selections, buffer)) = maybe!({
+ let editor = workspace
+ .active_item(cx)
+ .and_then(|item| item.act_as::<Editor>(cx))?;
+
+ let buffer = editor.read(cx).buffer().clone();
+ let snapshot = buffer.read(cx).snapshot(cx);
+ let selections = editor.update(cx, |editor, cx| {
+ editor
+ .selections
+ .all_adjusted(cx)
+ .into_iter()
+ .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
+ .collect::<Vec<_>>()
+ });
+ Some((selections, buffer))
+ }) else {
return;
};
- if creases.is_empty() {
+ if selections.is_empty() {
return;
}
- assistant_panel_delegate.quote_selection(workspace, creases, window, cx);
+ assistant_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
}
- pub fn quote_creases(
+ pub fn quote_ranges(
&mut self,
- creases: Vec<(String, String)>,
+ ranges: Vec<Range<Point>>,
+ snapshot: MultiBufferSnapshot,
window: &mut Window,
cx: &mut Context<Self>,
) {
+ let creases = selections_creases(ranges, snapshot, cx);
+
self.editor.update(cx, |editor, cx| {
editor.insert("\n", window, cx);
for (text, crease_title) in creases {
@@ -3,10 +3,12 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
SlashCommandOutputSection, SlashCommandResult,
};
-use editor::Editor;
+use editor::{Editor, MultiBufferSnapshot};
use futures::StreamExt;
-use gpui::{App, Context, SharedString, Task, WeakEntity, Window};
+use gpui::{App, SharedString, Task, WeakEntity, Window};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
+use rope::Point;
+use std::ops::Range;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use ui::IconName;
@@ -69,7 +71,22 @@ impl SlashCommand for SelectionCommand {
let mut events = vec![];
let Some(creases) = workspace
- .update(cx, selections_creases)
+ .update(cx, |workspace, cx| {
+ let editor = workspace
+ .active_item(cx)
+ .and_then(|item| item.act_as::<Editor>(cx))?;
+
+ editor.update(cx, |editor, cx| {
+ let selection_ranges = editor
+ .selections
+ .all_adjusted(cx)
+ .iter()
+ .map(|selection| selection.range())
+ .collect::<Vec<_>>();
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ Some(selections_creases(selection_ranges, snapshot, cx))
+ })
+ })
.unwrap_or_else(|e| {
events.push(Err(e));
None
@@ -102,94 +119,82 @@ impl SlashCommand for SelectionCommand {
}
pub fn selections_creases(
- workspace: &mut workspace::Workspace,
- cx: &mut Context<Workspace>,
-) -> Option<Vec<(String, String)>> {
- let editor = workspace
- .active_item(cx)
- .and_then(|item| item.act_as::<Editor>(cx))?;
-
- let mut creases = vec![];
- editor.update(cx, |editor, cx| {
- let selections = editor.selections.all_adjusted(cx);
- let buffer = editor.buffer().read(cx).snapshot(cx);
- for selection in selections {
- let range = editor::ToOffset::to_offset(&selection.start, &buffer)
- ..editor::ToOffset::to_offset(&selection.end, &buffer);
- let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
- if selected_text.is_empty() {
- continue;
- }
- let start_language = buffer.language_at(range.start);
- let end_language = buffer.language_at(range.end);
- let language_name = if start_language == end_language {
- start_language.map(|language| language.code_fence_block_name())
- } else {
- None
- };
- let language_name = language_name.as_deref().unwrap_or("");
- let filename = buffer
- .file_at(selection.start)
- .map(|file| file.full_path(cx));
- let text = if language_name == "markdown" {
- selected_text
- .lines()
- .map(|line| format!("> {}", line))
- .collect::<Vec<_>>()
- .join("\n")
- } else {
- let start_symbols = buffer
- .symbols_containing(selection.start, None)
- .map(|(_, symbols)| symbols);
- let end_symbols = buffer
- .symbols_containing(selection.end, None)
- .map(|(_, symbols)| symbols);
-
- let outline_text =
- if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
- Some(
- start_symbols
- .into_iter()
- .zip(end_symbols)
- .take_while(|(a, b)| a == b)
- .map(|(a, _)| a.text)
- .collect::<Vec<_>>()
- .join(" > "),
- )
- } else {
- None
- };
-
- let line_comment_prefix = start_language
- .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
-
- let fence = codeblock_fence_for_path(
- filename.as_deref(),
- Some(selection.start.row..=selection.end.row),
- );
-
- if let Some((line_comment_prefix, outline_text)) =
- line_comment_prefix.zip(outline_text)
- {
- let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
- format!("{fence}{breadcrumb}{selected_text}\n```")
- } else {
- format!("{fence}{selected_text}\n```")
- }
- };
- let crease_title = if let Some(path) = filename {
- let start_line = selection.start.row + 1;
- let end_line = selection.end.row + 1;
- if start_line == end_line {
- format!("{}, Line {}", path.display(), start_line)
+ selection_ranges: Vec<Range<Point>>,
+ snapshot: MultiBufferSnapshot,
+ cx: &App,
+) -> Vec<(String, String)> {
+ let mut creases = Vec::new();
+ for range in selection_ranges {
+ let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
+ if selected_text.is_empty() {
+ continue;
+ }
+ let start_language = snapshot.language_at(range.start);
+ let end_language = snapshot.language_at(range.end);
+ let language_name = if start_language == end_language {
+ start_language.map(|language| language.code_fence_block_name())
+ } else {
+ None
+ };
+ let language_name = language_name.as_deref().unwrap_or("");
+ let filename = snapshot.file_at(range.start).map(|file| file.full_path(cx));
+ let text = if language_name == "markdown" {
+ selected_text
+ .lines()
+ .map(|line| format!("> {}", line))
+ .collect::<Vec<_>>()
+ .join("\n")
+ } else {
+ let start_symbols = snapshot
+ .symbols_containing(range.start, None)
+ .map(|(_, symbols)| symbols);
+ let end_symbols = snapshot
+ .symbols_containing(range.end, None)
+ .map(|(_, symbols)| symbols);
+
+ let outline_text =
+ if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
+ Some(
+ start_symbols
+ .into_iter()
+ .zip(end_symbols)
+ .take_while(|(a, b)| a == b)
+ .map(|(a, _)| a.text)
+ .collect::<Vec<_>>()
+ .join(" > "),
+ )
} else {
- format!("{}, Lines {} to {}", path.display(), start_line, end_line)
- }
+ None
+ };
+
+ let line_comment_prefix = start_language
+ .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
+
+ let fence = codeblock_fence_for_path(
+ filename.as_deref(),
+ Some(range.start.row..=range.end.row),
+ );
+
+ if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text)
+ {
+ let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
+ format!("{fence}{breadcrumb}{selected_text}\n```")
} else {
- "Quoted selection".to_string()
- };
- creases.push((text, crease_title));
- }
- });
- Some(creases)
+ format!("{fence}{selected_text}\n```")
+ }
+ };
+ let crease_title = if let Some(path) = filename {
+ let start_line = range.start.row + 1;
+ let end_line = range.end.row + 1;
+ if start_line == end_line {
+ format!("{}, Line {}", path.display(), start_line)
+ } else {
+ format!("{}, Lines {} to {}", path.display(), start_line, end_line)
+ }
+ } else {
+ "Quoted selection".to_string()
+ };
+ creases.push((text, crease_title));
+ }
+ creases
}