Detailed changes
@@ -6,7 +6,7 @@ use futures::future;
use futures::{FutureExt, future::Shared};
use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task};
use icons::IconName;
-use language::{Buffer, ParseStatus};
+use language::Buffer;
use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
use project::{Project, ProjectEntryId, ProjectPath, Worktree};
use prompt_store::{PromptStore, UserPromptId};
@@ -191,45 +191,19 @@ impl FileContextHandle {
let buffer = self.buffer.clone();
cx.spawn(async move |cx| {
- // For large files, use outline instead of full content
- if rope.len() > outline::AUTO_OUTLINE_SIZE {
- // Wait until the buffer has been fully parsed, so we can read its outline
- if let Ok(mut parse_status) =
- buffer.read_with(cx, |buffer, _| buffer.parse_status())
- {
- while *parse_status.borrow() != ParseStatus::Idle {
- parse_status.changed().await.log_err();
- }
-
- if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot()) {
- let items = snapshot
- .outline(None)
- .items
- .into_iter()
- .map(|item| item.to_point(&snapshot));
-
- if let Ok(outline_text) =
- outline::render_outline(items, None, 0, usize::MAX).await
- {
- let context = AgentContext::File(FileContext {
- handle: self,
- full_path,
- text: outline_text.into(),
- is_outline: true,
- });
- return Some((context, vec![buffer]));
- }
- }
- }
- }
+ let buffer_content =
+ outline::get_buffer_content_or_outline(buffer.clone(), Some(&full_path), &cx)
+ .await
+ .unwrap_or_else(|_| outline::BufferContent {
+ text: rope.to_string(),
+ is_outline: false,
+ });
- // Fallback to full content if we couldn't build an outline
- // (or didn't need to because the file was small enough)
let context = AgentContext::File(FileContext {
handle: self,
full_path,
- text: rope.to_string().into(),
- is_outline: false,
+ text: buffer_content.text.into(),
+ is_outline: buffer_content.is_outline,
});
Some((context, vec![buffer]))
})
@@ -147,7 +147,7 @@ impl AgentTool for ReadFileTool {
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
- path: abs_path,
+ path: abs_path.clone(),
line: input.start_line.map(|line| line.saturating_sub(1)),
}]),
..Default::default()
@@ -225,38 +225,30 @@ impl AgentTool for ReadFileTool {
Ok(result.into())
} else {
// No line ranges specified, so check file size to see if it's too big.
- let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
+ let buffer_content =
+ outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), cx)
+ .await?;
- if file_size <= outline::AUTO_OUTLINE_SIZE {
- // File is small enough, so return its contents.
- let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
-
- action_log.update(cx, |log, cx| {
- log.buffer_read(buffer.clone(), cx);
- })?;
+ action_log.update(cx, |log, cx| {
+ log.buffer_read(buffer.clone(), cx);
+ })?;
- Ok(result.into())
- } else {
- // File is too big, so return the outline
- // and a suggestion to read again with line numbers.
- let outline =
- outline::file_outline(project.clone(), file_path, action_log, None, cx)
- .await?;
+ if buffer_content.is_outline {
Ok(formatdoc! {"
This file was too big to read all at once.
- Here is an outline of its symbols:
-
- {outline}
+ {}
Using the line numbers in this outline, you can call this tool again
while specifying the start_line and end_line fields to see the
implementations of symbols in the outline.
Alternatively, you can fall back to the `grep` tool (if available)
- to search the file for specific content."
+ to search the file for specific content.", buffer_content.text
}
.into())
+ } else {
+ Ok(buffer_content.text.into())
}
};
@@ -8,6 +8,7 @@ use agent_servers::{AgentServer, AgentServerDelegate};
use agent2::HistoryStore;
use anyhow::{Result, anyhow};
use assistant_slash_commands::codeblock_fence_for_path;
+use assistant_tool::outline;
use collections::{HashMap, HashSet};
use editor::{
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
@@ -456,11 +457,14 @@ impl MessageEditor {
.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(async move |_, cx| {
let buffer = buffer.await?;
- let mention = buffer.update(cx, |buffer, cx| Mention::Text {
- content: buffer.text(),
- tracked_buffers: vec![cx.entity()],
- })?;
- anyhow::Ok(mention)
+ let buffer_content =
+ outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), &cx)
+ .await?;
+
+ Ok(Mention::Text {
+ content: buffer_content.text,
+ tracked_buffers: vec![buffer],
+ })
})
}
@@ -520,18 +524,17 @@ impl MessageEditor {
})
});
- // TODO: report load errors instead of just logging
- let rope_task = cx.spawn(async move |cx| {
+ cx.spawn(async move |cx| {
let buffer = open_task.await.log_err()?;
- let rope = buffer
- .read_with(cx, |buffer, _cx| buffer.as_rope().clone())
- .log_err()?;
- Some((rope, buffer))
- });
+ let buffer_content = outline::get_buffer_content_or_outline(
+ buffer.clone(),
+ Some(&full_path),
+ &cx,
+ )
+ .await
+ .ok()?;
- cx.background_spawn(async move {
- let (rope, buffer) = rope_task.await?;
- Some((rel_path, full_path, rope.to_string(), buffer))
+ Some((rel_path, full_path, buffer_content.text, buffer))
})
}))
})?;
@@ -1580,6 +1583,7 @@ mod tests {
use agent_client_protocol as acp;
use agent2::HistoryStore;
use assistant_context::ContextStore;
+ use assistant_tool::outline;
use editor::{AnchorRangeExt as _, Editor, EditorMode};
use fs::FakeFs;
use futures::StreamExt as _;
@@ -2584,4 +2588,109 @@ mod tests {
})
.collect::<Vec<_>>()
}
+
+ #[gpui::test]
+ async fn test_large_file_mention_uses_outline(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+
+ // Create a large file that exceeds AUTO_OUTLINE_SIZE
+ const LINE: &str = "fn example_function() { /* some code */ }\n";
+ let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
+ assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
+
+ // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
+ let small_content = "fn small_function() { /* small */ }\n";
+ assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
+
+ fs.insert_tree(
+ "/project",
+ json!({
+ "large_file.rs": large_content.clone(),
+ "small_file.rs": small_content,
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+ let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
+ let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
+
+ let message_editor = cx.update(|window, cx| {
+ cx.new(|cx| {
+ let editor = MessageEditor::new(
+ workspace.downgrade(),
+ project.clone(),
+ history_store.clone(),
+ None,
+ Default::default(),
+ Default::default(),
+ "Test Agent".into(),
+ "Test",
+ EditorMode::AutoHeight {
+ min_lines: 1,
+ max_lines: None,
+ },
+ window,
+ cx,
+ );
+ // Enable embedded context so files are actually included
+ editor.prompt_capabilities.set(acp::PromptCapabilities {
+ embedded_context: true,
+ ..Default::default()
+ });
+ editor
+ })
+ });
+
+ // Test large file mention
+ // Get the absolute path using the project's worktree
+ let large_file_abs_path = project.read_with(cx, |project, cx| {
+ let worktree = project.worktrees(cx).next().unwrap();
+ let worktree_root = worktree.read(cx).abs_path();
+ worktree_root.join("large_file.rs")
+ });
+ let large_file_task = message_editor.update(cx, |editor, cx| {
+ editor.confirm_mention_for_file(large_file_abs_path, cx)
+ });
+
+ let large_file_mention = large_file_task.await.unwrap();
+ match large_file_mention {
+ Mention::Text { content, .. } => {
+ // Should contain outline header for large files
+ assert!(content.contains("File outline for"));
+ assert!(content.contains("file too large to show full content"));
+ // Should not contain the full repeated content
+ assert!(!content.contains(&LINE.repeat(100)));
+ }
+ _ => panic!("Expected Text mention for large file"),
+ }
+
+ // Test small file mention
+ // Get the absolute path using the project's worktree
+ let small_file_abs_path = project.read_with(cx, |project, cx| {
+ let worktree = project.worktrees(cx).next().unwrap();
+ let worktree_root = worktree.read(cx).abs_path();
+ worktree_root.join("small_file.rs")
+ });
+ let small_file_task = message_editor.update(cx, |editor, cx| {
+ editor.confirm_mention_for_file(small_file_abs_path, cx)
+ });
+
+ let small_file_mention = small_file_task.await.unwrap();
+ match small_file_mention {
+ Mention::Text { content, .. } => {
+ // Should contain the actual content
+ assert_eq!(content, small_content);
+ // Should not contain outline header
+ assert!(!content.contains("File outline for"));
+ }
+ _ => panic!("Expected Text mention for small file"),
+ }
+ }
}
@@ -1,10 +1,11 @@
use action_log::ActionLog;
use anyhow::{Context as _, Result};
use gpui::{AsyncApp, Entity};
-use language::{OutlineItem, ParseStatus};
+use language::{Buffer, OutlineItem, ParseStatus};
use project::Project;
use regex::Regex;
use std::fmt::Write;
+use std::path::Path;
use text::Point;
/// For files over this size, instead of reading them (or including them in context),
@@ -128,3 +129,67 @@ fn render_entries(
entries_rendered
}
+
+/// Result of getting buffer content, which can be either full content or an outline.
+pub struct BufferContent {
+ /// The actual content (either full text or outline)
+ pub text: String,
+ /// Whether this is an outline (true) or full content (false)
+ pub is_outline: bool,
+}
+
+/// Returns either the full content of a buffer or its outline, depending on size.
+/// For files larger than AUTO_OUTLINE_SIZE, returns an outline with a header.
+/// For smaller files, returns the full content.
+pub async fn get_buffer_content_or_outline(
+ buffer: Entity<Buffer>,
+ path: Option<&Path>,
+ cx: &AsyncApp,
+) -> Result<BufferContent> {
+ let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?;
+
+ if file_size > AUTO_OUTLINE_SIZE {
+ // For large files, use outline instead of full content
+ // Wait until the buffer has been fully parsed, so we can read its outline
+ let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
+ while *parse_status.borrow() != ParseStatus::Idle {
+ parse_status.changed().await?;
+ }
+
+ let outline_items = buffer.read_with(cx, |buffer, _| {
+ let snapshot = buffer.snapshot();
+ snapshot
+ .outline(None)
+ .items
+ .into_iter()
+ .map(|item| item.to_point(&snapshot))
+ .collect::<Vec<_>>()
+ })?;
+
+ let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
+
+ let text = if let Some(path) = path {
+ format!(
+ "# File outline for {} (file too large to show full content)\n\n{}",
+ path.display(),
+ outline_text
+ )
+ } else {
+ format!(
+ "# File outline (file too large to show full content)\n\n{}",
+ outline_text
+ )
+ };
+ Ok(BufferContent {
+ text,
+ is_outline: true,
+ })
+ } else {
+ // File is small enough, return full content
+ let text = buffer.read_with(cx, |buffer, _| buffer.text())?;
+ Ok(BufferContent {
+ text,
+ is_outline: false,
+ })
+ }
+}
@@ -261,37 +261,31 @@ impl Tool for ReadFileTool {
Ok(result)
} else {
// No line ranges specified, so check file size to see if it's too big.
- let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
+ let path_buf = std::path::PathBuf::from(&file_path);
+ let buffer_content =
+ outline::get_buffer_content_or_outline(buffer.clone(), Some(&path_buf), cx)
+ .await?;
- if file_size <= outline::AUTO_OUTLINE_SIZE {
- // File is small enough, so return its contents.
- let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
-
- action_log.update(cx, |log, cx| {
- log.buffer_read(buffer, cx);
- })?;
+ action_log.update(cx, |log, cx| {
+ log.buffer_read(buffer, cx);
+ })?;
- Ok(result.into())
- } else {
- // File is too big, so return the outline
- // and a suggestion to read again with line numbers.
- let outline =
- outline::file_outline(project, file_path, action_log, None, cx).await?;
+ if buffer_content.is_outline {
Ok(formatdoc! {"
This file was too big to read all at once.
- Here is an outline of its symbols:
-
- {outline}
+ {}
Using the line numbers in this outline, you can call this tool again
while specifying the start_line and end_line fields to see the
implementations of symbols in the outline.
Alternatively, you can fall back to the `grep` tool (if available)
- to search the file for specific content."
+ to search the file for specific content.", buffer_content.text
}
.into())
+ } else {
+ Ok(buffer_content.text.into())
}
}
})