Since we removed the filtering step during context gathering, we want
the model to perform more targeted searches. This PR tweaks search tool
schema allowing the model to search within syntax nodes such as `impl`
blocks or methods.
This is what the query schema looks like now:
```rust
/// Search for relevant code by path, syntax hierarchy, and content.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchToolQuery {
/// 1. A glob pattern to match file paths in the codebase to search in.
pub glob: String,
/// 2. Regular expressions to match syntax nodes **by their first line** and hierarchy.
///
/// Subsequent regexes match nodes within the full content of the nodes matched by the previous regexes.
///
/// Example: Searching for a `User` class
/// ["class\s+User"]
///
/// Example: Searching for a `get_full_name` method under a `User` class
/// ["class\s+User", "def\sget_full_name"]
///
/// Skip this field to match on content alone.
#[schemars(length(max = 3))]
#[serde(default)]
pub syntax_node: Vec<String>,
/// 3. An optional regular expression to match the final content that should appear in the results.
///
/// - Content will be matched within all lines of the matched syntax nodes.
/// - If syntax node regexes are provided, this field can be skipped to include as much of the node itself as possible.
/// - If no syntax node regexes are provided, the content will be matched within the entire file.
pub content: Option<String>,
}
```
We'll need to keep refining this, but the core implementation is ready.
Release Notes:
- N/A
---------
Co-authored-by: Ben <ben@zed.dev>
Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
143 lines
4.4 KiB
Rust
143 lines
4.4 KiB
Rust
use anyhow::Result;
|
|
use gpui::{AsyncApp, Entity};
|
|
use language::{Buffer, OutlineItem};
|
|
use regex::Regex;
|
|
use std::fmt::Write;
|
|
use text::Point;
|
|
|
|
/// For files over this size, instead of reading them (or including them in context),
|
|
/// we automatically provide the file's symbol outline instead, with line numbers.
|
|
pub const AUTO_OUTLINE_SIZE: usize = 16384;
|
|
|
|
/// 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<&str>,
|
|
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
|
|
buffer
|
|
.read_with(cx, |buffer, _| buffer.parsing_idle())?
|
|
.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 {path} (file too large to show full content)\n\n{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,
|
|
})
|
|
}
|
|
}
|
|
|
|
async fn render_outline(
|
|
items: impl IntoIterator<Item = OutlineItem<Point>>,
|
|
regex: Option<Regex>,
|
|
offset: usize,
|
|
results_per_page: usize,
|
|
) -> Result<String> {
|
|
let mut items = items.into_iter().skip(offset);
|
|
|
|
let entries = items
|
|
.by_ref()
|
|
.filter(|item| {
|
|
regex
|
|
.as_ref()
|
|
.is_none_or(|regex| regex.is_match(&item.text))
|
|
})
|
|
.take(results_per_page)
|
|
.collect::<Vec<_>>();
|
|
let has_more = items.next().is_some();
|
|
|
|
let mut output = String::new();
|
|
let entries_rendered = render_entries(&mut output, entries);
|
|
|
|
// Calculate pagination information
|
|
let page_start = offset + 1;
|
|
let page_end = offset + entries_rendered;
|
|
let total_symbols = if has_more {
|
|
format!("more than {}", page_end)
|
|
} else {
|
|
page_end.to_string()
|
|
};
|
|
|
|
// Add pagination information
|
|
if has_more {
|
|
writeln!(&mut output, "\nShowing symbols {page_start}-{page_end} (there were more symbols found; use offset: {page_end} to see next page)",
|
|
)
|
|
} else {
|
|
writeln!(
|
|
&mut output,
|
|
"\nShowing symbols {page_start}-{page_end} (total symbols: {total_symbols})",
|
|
)
|
|
}
|
|
.ok();
|
|
|
|
Ok(output)
|
|
}
|
|
|
|
fn render_entries(
|
|
output: &mut String,
|
|
items: impl IntoIterator<Item = OutlineItem<Point>>,
|
|
) -> usize {
|
|
let mut entries_rendered = 0;
|
|
|
|
for item in items {
|
|
// Indent based on depth ("" for level 0, " " for level 1, etc.)
|
|
for _ in 0..item.depth {
|
|
output.push(' ');
|
|
}
|
|
output.push_str(&item.text);
|
|
|
|
// Add position information - convert to 1-based line numbers for display
|
|
let start_line = item.range.start.row + 1;
|
|
let end_line = item.range.end.row + 1;
|
|
|
|
if start_line == end_line {
|
|
writeln!(output, " [L{}]", start_line).ok();
|
|
} else {
|
|
writeln!(output, " [L{}-{}]", start_line, end_line).ok();
|
|
}
|
|
entries_rendered += 1;
|
|
}
|
|
|
|
entries_rendered
|
|
}
|