use super::{
ToolResult, ToolRuntime,
args::{
REPO_MAP_MAX_FILE_BYTES, REPO_MAP_MAX_FILES, REPO_MAP_MAX_REFS_PER_FILE,
REPO_MAP_MAX_SYMBOLS_PER_FILE, RepoMapArgs,
},
contract::{metadata_key as meta, tool_name},
fs::ExistingPathPolicy,
};
use crate::agent::cancellation::AgentCancellation;
use serde_json::json;
use std::{
collections::{HashMap, HashSet},
fs,
path::{Path, PathBuf},
};
use tree_sitter::{Language, Parser, Query, QueryCursor, StreamingIterator};
const OUTPUT_BYTES_PER_TOKEN: usize = 4;
const WALK_CANCEL_INTERVAL: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum LanguageKind {
Rust,
Python,
TypeScript,
Tsx,
Go,
}
impl LanguageKind {
fn from_path(path: &Path) -> Option<Self> {
match path.extension().and_then(|extension| extension.to_str()) {
Some("rs") => Some(Self::Rust),
Some("py") => Some(Self::Python),
Some("ts") => Some(Self::TypeScript),
Some("tsx") => Some(Self::Tsx),
Some("go") => Some(Self::Go),
_ => None,
}
}
fn name(self) -> &'static str {
match self {
Self::Rust => "rust",
Self::Python => "python",
Self::TypeScript => "typescript",
Self::Tsx => "tsx",
Self::Go => "go",
}
}
fn language(self) -> Language {
match self {
Self::Rust => tree_sitter_rust::LANGUAGE.into(),
Self::Python => tree_sitter_python::LANGUAGE.into(),
Self::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
Self::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
Self::Go => tree_sitter_go::LANGUAGE.into(),
}
}
fn query(self) -> &'static str {
match self {
Self::Rust => {
r#"
(function_item name: (identifier) @name.definition.function) @definition.function
(struct_item name: (type_identifier) @name.definition.class) @definition.class
(enum_item name: (type_identifier) @name.definition.class) @definition.class
(trait_item name: (type_identifier) @name.definition.interface) @definition.interface
(mod_item name: (identifier) @name.definition.module) @definition.module
(const_item name: (identifier) @name.definition.constant) @definition.constant
(call_expression function: (identifier) @name.reference.call) @reference.call
(call_expression function: (field_expression field: (field_identifier) @name.reference.call)) @reference.call
(macro_invocation macro: (identifier) @name.reference.call) @reference.call
"#
}
Self::Python => {
r#"
(class_definition name: (identifier) @name.definition.class) @definition.class
(function_definition name: (identifier) @name.definition.function) @definition.function
(call function: [(identifier) @name.reference.call (attribute attribute: (identifier) @name.reference.call)]) @reference.call
"#
}
Self::TypeScript | Self::Tsx => {
r#"
(function_declaration name: (identifier) @name.definition.function) @definition.function
(method_definition name: (property_identifier) @name.definition.method) @definition.method
(class_declaration name: (type_identifier) @name.definition.class) @definition.class
(interface_declaration name: (type_identifier) @name.definition.interface) @definition.interface
(type_alias_declaration name: (type_identifier) @name.definition.type) @definition.type
(enum_declaration name: (identifier) @name.definition.enum) @definition.enum
(new_expression constructor: (identifier) @name.reference.call) @reference.call
"#
}
Self::Go => {
r#"
(function_declaration name: (identifier) @name.definition.function) @definition.function
(method_declaration name: (field_identifier) @name.definition.method) @definition.method
(type_spec name: (type_identifier) @name.definition.type) @definition.type
(call_expression function: [(identifier) @name.reference.call (selector_expression field: (field_identifier) @name.reference.call)]) @reference.call
"#
}
}
}
fn display_kind(self, kind: &str) -> &'static str {
match (self, kind) {
(_, "function") => "fn",
(Self::Rust, "class") => "struct",
(_, "class") => "class",
(Self::Rust, "interface") => "trait",
(_, "interface") => "interface",
(_, "method") => "method",
(_, "type") => "type",
(_, "constant") => "const",
(_, "module") => "mod",
(_, "enum") => "enum",
_ => "symbol",
}
}
}
#[derive(Debug, Clone)]
struct SourceFile {
absolute: PathBuf,
relative: String,
language: LanguageKind,
}
#[derive(Debug, Clone)]
struct SymbolDef {
name: String,
display_kind: String,
file: usize,
line: usize,
score: f64,
}
#[derive(Debug, Clone)]
struct FileSymbols {
path_idx: usize,
language: LanguageKind,
defs: Vec<SymbolDef>,
refs: HashSet<String>,
}
#[derive(Debug, Default)]
struct RepoMapStats {
files_scanned: usize,
files_parsed: usize,
symbols: usize,
references: usize,
truncated: bool,
limit_reason: Option<&'static str>,
}
impl ToolRuntime {
pub(super) fn repo_map(
&self,
args: RepoMapArgs,
cancellation: &AgentCancellation,
) -> anyhow::Result<ToolResult> {
cancellation.check()?;
let requested_path = args.path.as_deref().unwrap_or(".");
let root = self.resolve_existing_path(
requested_path,
ExistingPathPolicy::repo_map(self.repo_map_absolute_paths),
)?;
let max_tokens = args.max_tokens();
let mut stats = RepoMapStats::default();
let files = collect_files(
&self.workspace_walker,
&root,
&self.cwd_canonical,
cancellation,
&mut stats,
)?;
let mut parsed = Vec::new();
let mut languages = HashSet::new();
for (file_index, file) in files.iter().enumerate() {
cancellation.check()?;
if let Some(symbols) = parse_file(file, file_index)? {
stats.files_parsed += 1;
stats.symbols += symbols.defs.len();
stats.references += symbols.refs.len();
languages.insert(symbols.language.name());
parsed.push(symbols);
}
}
cancellation.check()?;
let ranked = rank_symbols(&parsed, &files, args.query.as_deref());
cancellation.check()?;
let root_label = relative_path(&root, &self.cwd_canonical);
let (content, render_truncated) = render_map(
&ranked,
&files,
&root_label,
max_tokens,
stats.files_scanned,
stats.files_parsed,
);
stats.truncated |= render_truncated;
if render_truncated && stats.limit_reason.is_none() {
stats.limit_reason = Some("output_bytes");
}
cancellation.check()?;
let mut languages = languages.into_iter().collect::<Vec<_>>();
languages.sort_unstable();
Ok(ToolResult {
tool_name: tool_name::REPO_MAP.to_string(),
success: true,
content,
metadata: json!({
(meta::PATH): root,
(meta::QUERY): args.query,
(meta::MAX_TOKENS): max_tokens,
(meta::FILES_SCANNED): stats.files_scanned,
(meta::FILES_PARSED): stats.files_parsed,
(meta::SYMBOLS): stats.symbols,
(meta::REFERENCES): stats.references,
(meta::LANGUAGES): languages,
(meta::TRUNCATED): stats.truncated,
(meta::LIMIT_REASON): stats.limit_reason,
}),
display: Default::default(),
})
}
}
fn collect_files(
walker: &super::workspace::WorkspaceWalker,
root: &Path,
cwd: &Path,
cancellation: &AgentCancellation,
stats: &mut RepoMapStats,
) -> anyhow::Result<Vec<SourceFile>> {
if root.is_file() {
let language = LanguageKind::from_path(root)
.ok_or_else(|| anyhow::anyhow!("repo_map file extension is not supported"))?;
let len = fs::metadata(root)?.len();
if len > REPO_MAP_MAX_FILE_BYTES as u64 {
stats.files_scanned = 0;
return Ok(Vec::new());
}
stats.files_scanned = 1;
return Ok(vec![SourceFile {
absolute: root.to_path_buf(),
relative: relative_path(root, cwd),
language,
}]);
}
if !root.is_dir() {
anyhow::bail!("repo_map path must be a file or directory");
}
let mut files = Vec::new();
let walk = walker.walk(
super::workspace::WorkspaceWalkOptions {
root,
include_files: true,
include_dirs: false,
skip_dirs: &[".git", "target", "node_modules", ".venv", "dist", "build"],
cancel_interval: WALK_CANCEL_INTERVAL,
},
Some(cancellation),
)?;
for entry in walk.entries {
if !entry.file_type.is_file() {
continue;
}
let path = entry.path.as_path();
let Some(language) = LanguageKind::from_path(path) else {
continue;
};
let Ok(metadata) = fs::metadata(path) else {
continue;
};
if metadata.len() > REPO_MAP_MAX_FILE_BYTES as u64 {
continue;
}
if files.len() >= REPO_MAP_MAX_FILES {
stats.truncated = true;
stats.limit_reason = Some("file_count");
break;
}
stats.files_scanned += 1;
files.push(SourceFile {
absolute: path.to_path_buf(),
relative: relative_path(path, cwd),
language,
});
}
Ok(files)
}
fn parse_file(file: &SourceFile, path_idx: usize) -> anyhow::Result<Option<FileSymbols>> {
let source = match fs::read_to_string(&file.absolute) {
Ok(source) => source,
Err(error) if error.kind() == std::io::ErrorKind::InvalidData => return Ok(None),
Err(error) => return Err(error.into()),
};
let language = file.language.language();
let mut parser = Parser::new();
if parser.set_language(&language).is_err() {
return Ok(None);
}
let Some(tree) = parser.parse(&source, None) else {
return Ok(None);
};
let query = match Query::new(&language, file.language.query()) {
Ok(query) => query,
Err(_) => return Ok(None),
};
let capture_names = query.capture_names();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
let mut defs = Vec::new();
let mut refs = HashSet::new();
while let Some(query_match) = matches.next() {
for capture in query_match.captures {
let capture_name = capture_names[capture.index as usize];
let Ok(name) = capture.node.utf8_text(source.as_bytes()) else {
continue;
};
if name.is_empty() {
continue;
}
if let Some(kind) = capture_name.strip_prefix("name.definition.") {
if defs.len() >= REPO_MAP_MAX_SYMBOLS_PER_FILE {
continue;
}
defs.push(SymbolDef {
name: name.to_string(),
display_kind: file.language.display_kind(kind).to_string(),
file: path_idx,
line: capture.node.start_position().row + 1,
score: 0.0,
});
} else if capture_name == "name.reference.call"
&& refs.len() < REPO_MAP_MAX_REFS_PER_FILE
{
refs.insert(name.to_string());
}
}
}
Ok(Some(FileSymbols {
path_idx,
language: file.language,
defs,
refs,
}))
}
fn rank_symbols(
files_symbols: &[FileSymbols],
files: &[SourceFile],
query: Option<&str>,
) -> Vec<SymbolDef> {
let mut definitions = Vec::new();
let mut defines: HashMap<String, Vec<usize>> = HashMap::new();
let mut refs: HashMap<String, HashSet<usize>> = HashMap::new();
for file_symbols in files_symbols {
for def in &file_symbols.defs {
let index = definitions.len();
definitions.push(def.clone());
defines.entry(def.name.clone()).or_default().push(index);
}
for reference in &file_symbols.refs {
refs.entry(reference.clone())
.or_default()
.insert(file_symbols.path_idx);
}
}
let query_lower = query.map(str::to_lowercase);
for def in &mut definitions {
let mut score = refs.get(&def.name).map_or(0, HashSet::len) as f64;
let name_lower = def.name.to_lowercase();
let path_lower = files[def.file].relative.to_lowercase();
if let Some(query) = query_lower.as_deref()
&& (name_lower.contains(query) || path_lower.contains(query))
{
score += 10.0;
}
if is_long_snake_case(&def.name) {
score *= 10.0;
}
if defines.get(&def.name).is_some_and(|defs| defs.len() > 5) {
score *= 0.1;
}
if def.name.starts_with('_') {
score *= 0.1;
}
def.score = score;
}
definitions.sort_by(|left, right| {
right
.score
.total_cmp(&left.score)
.then_with(|| files[left.file].relative.cmp(&files[right.file].relative))
.then_with(|| left.line.cmp(&right.line))
.then_with(|| left.name.cmp(&right.name))
});
definitions
}
fn is_long_snake_case(name: &str) -> bool {
name.len() >= 8
&& name.contains('_')
&& name
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
}
fn render_map(
symbols: &[SymbolDef],
files: &[SourceFile],
path: &str,
max_tokens: usize,
files_scanned: usize,
files_parsed: usize,
) -> (String, bool) {
if symbols.is_empty() {
return (
format!(
"No supported symbols found in {path}. Scanned {files_scanned} files, parsed {files_parsed}."
),
false,
);
}
let mut grouped: HashMap<usize, Vec<&SymbolDef>> = HashMap::new();
for symbol in symbols {
grouped.entry(symbol.file).or_default().push(symbol);
}
let mut file_ids = grouped.keys().copied().collect::<Vec<_>>();
file_ids.sort_by(|left, right| {
let left_score = grouped[left]
.iter()
.map(|symbol| symbol.score)
.fold(f64::NEG_INFINITY, f64::max);
let right_score = grouped[right]
.iter()
.map(|symbol| symbol.score)
.fold(f64::NEG_INFINITY, f64::max);
right_score
.total_cmp(&left_score)
.then_with(|| files[*left].relative.cmp(&files[*right].relative))
});
let budget = max_tokens.saturating_mul(OUTPUT_BYTES_PER_TOKEN);
let mut output = String::new();
let mut truncated = false;
'files: for file_id in file_ids {
let mut file_symbols = grouped.remove(&file_id).unwrap_or_default();
file_symbols.sort_by(|left, right| {
right
.score
.total_cmp(&left.score)
.then_with(|| left.line.cmp(&right.line))
.then_with(|| left.name.cmp(&right.name))
});
let header = format!("{}:\n", files[file_id].relative);
if !push_budgeted(&mut output, &header, budget) {
truncated = true;
break;
}
let last_index = file_symbols.len().saturating_sub(1);
for (index, symbol) in file_symbols.iter().enumerate() {
let branch = if index == last_index { "└" } else { "├" };
let line = format!(
" {branch}─ {} {} (line {})\n",
symbol.display_kind, symbol.name, symbol.line
);
if !push_budgeted(&mut output, &line, budget) {
truncated = true;
break 'files;
}
}
}
if truncated {
append_truncation_marker(&mut output, budget);
}
(output.trim_end().to_string(), truncated)
}
fn push_budgeted(output: &mut String, text: &str, budget: usize) -> bool {
if output.len().saturating_add(text.len()) <= budget {
output.push_str(text);
return true;
}
false
}
fn append_truncation_marker(output: &mut String, budget: usize) {
let marker = format!("\n[repo_map output truncated at {budget} bytes]");
if marker.len() >= budget {
output.clear();
output.push_str(&marker[..floor_char_boundary(&marker, budget)]);
return;
}
while output.len().saturating_add(marker.len()) > budget {
let Some((index, _)) = output.char_indices().next_back() else {
break;
};
output.truncate(index);
}
output.push_str(&marker);
}
fn floor_char_boundary(text: &str, mut index: usize) -> usize {
index = index.min(text.len());
while !text.is_char_boundary(index) {
index -= 1;
}
index
}
fn relative_path(path: &Path, cwd: &Path) -> String {
path.strip_prefix(cwd)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/")
.trim_matches('/')
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn source_file(path: &str, language: LanguageKind) -> SourceFile {
SourceFile {
absolute: PathBuf::from(path),
relative: path.to_string(),
language,
}
}
#[test]
fn repo_map_collect_files_excludes_gitignored_sources() {
let temp = tempfile::TempDir::new().unwrap();
fs::create_dir(temp.path().join(".git")).unwrap();
fs::write(temp.path().join(".gitignore"), "ignored.rs\n").unwrap();
fs::write(temp.path().join("ignored.rs"), "fn ignored() {}").unwrap();
fs::write(temp.path().join("visible.rs"), "fn visible() {}").unwrap();
let walker = super::super::workspace::WorkspaceWalker;
let mut stats = RepoMapStats::default();
let files = collect_files(
&walker,
temp.path(),
temp.path(),
&AgentCancellation::default(),
&mut stats,
)
.unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].relative, "visible.rs");
}
#[test]
fn repo_map_collect_files_excludes_p4ignored_sources() {
let temp = tempfile::TempDir::new().unwrap();
fs::write(temp.path().join(".p4ignore"), "ignored.rs\n").unwrap();
fs::write(temp.path().join("ignored.rs"), "fn ignored() {}").unwrap();
fs::write(temp.path().join("visible.rs"), "fn visible() {}").unwrap();
let walker = super::super::workspace::WorkspaceWalker;
let mut stats = RepoMapStats::default();
let files = collect_files(
&walker,
temp.path(),
temp.path(),
&AgentCancellation::default(),
&mut stats,
)
.unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].relative, "visible.rs");
}
#[test]
fn repo_map_queries_compile_for_all_languages() {
for language in [
LanguageKind::Rust,
LanguageKind::Python,
LanguageKind::TypeScript,
LanguageKind::Tsx,
LanguageKind::Go,
] {
let lang = language.language();
let query = Query::new(&lang, language.query());
assert!(
query.is_ok(),
"query for {:?} failed: {:?}",
language,
query.err()
);
}
}
#[test]
fn repo_map_parses_simple_rust_defs() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("lib.rs");
fs::write(
&path,
"struct AgentSession;\nfn run_print_with_tools_streaming_output_inner() { run_print_with_tools_streaming_output_inner(); }\n",
)
.unwrap();
let file = SourceFile {
absolute: path,
relative: "lib.rs".to_string(),
language: LanguageKind::Rust,
};
let symbols = parse_file(&file, 0).unwrap().unwrap();
assert!(symbols.defs.iter().any(|def| def.name == "AgentSession"));
assert!(
symbols
.defs
.iter()
.any(|def| def.name == "run_print_with_tools_streaming_output_inner")
);
assert!(
symbols
.refs
.contains("run_print_with_tools_streaming_output_inner")
);
}
#[test]
fn repo_map_parses_python_defs() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("app.py");
fs::write(
&path,
"class Agent:\n pass\n\ndef run_agent():\n Agent()\n",
)
.unwrap();
let file = SourceFile {
absolute: path,
relative: "app.py".to_string(),
language: LanguageKind::Python,
};
let symbols = parse_file(&file, 0).unwrap().unwrap();
assert!(symbols.defs.iter().any(|def| def.name == "Agent"));
assert!(symbols.defs.iter().any(|def| def.name == "run_agent"));
assert!(symbols.refs.contains("Agent"));
}
#[test]
fn repo_map_parses_typescript_defs() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("app.ts");
fs::write(
&path,
"interface AgentOptions {}\ntype AgentId = string;\nclass Agent {}\nfunction runAgent() { return new Agent(); }\n",
)
.unwrap();
let file = SourceFile {
absolute: path,
relative: "app.ts".to_string(),
language: LanguageKind::TypeScript,
};
let symbols = parse_file(&file, 0).unwrap().unwrap();
assert!(symbols.defs.iter().any(|def| def.name == "AgentOptions"));
assert!(symbols.defs.iter().any(|def| def.name == "AgentId"));
assert!(symbols.defs.iter().any(|def| def.name == "Agent"));
assert!(symbols.defs.iter().any(|def| def.name == "runAgent"));
assert!(symbols.refs.contains("Agent"));
}
#[test]
fn repo_map_parses_go_defs() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("app.go");
fs::write(
&path,
"package main\n\ntype Agent struct{}\n\nfunc RunAgent() { NewAgent() }\n\nfunc NewAgent() Agent { return Agent{} }\n",
)
.unwrap();
let file = SourceFile {
absolute: path,
relative: "app.go".to_string(),
language: LanguageKind::Go,
};
let symbols = parse_file(&file, 0).unwrap().unwrap();
assert!(symbols.defs.iter().any(|def| def.name == "Agent"));
assert!(symbols.defs.iter().any(|def| def.name == "RunAgent"));
assert!(symbols.defs.iter().any(|def| def.name == "NewAgent"));
assert!(symbols.refs.contains("NewAgent"));
}
#[test]
fn repo_map_ranking_applies_query_boost_and_penalty() {
let files = vec![
source_file("src/a.rs", LanguageKind::Rust),
source_file("src/private.rs", LanguageKind::Rust),
source_file("src/private.rs", LanguageKind::Rust),
];
let mut duplicate_defs = Vec::new();
for file in 0..6 {
duplicate_defs.push(SymbolDef {
name: "duplicate_name".to_string(),
display_kind: "fn".to_string(),
file: file.min(2),
line: file + 1,
score: 0.0,
});
}
let parsed = vec![
FileSymbols {
path_idx: 0,
language: LanguageKind::Rust,
defs: vec![SymbolDef {
name: "query_target".to_string(),
display_kind: "fn".to_string(),
file: 0,
line: 1,
score: 0.0,
}],
refs: HashSet::new(),
},
FileSymbols {
path_idx: 1,
language: LanguageKind::Rust,
defs: vec![SymbolDef {
name: "_private_name".to_string(),
display_kind: "fn".to_string(),
file: 1,
line: 1,
score: 0.0,
}],
refs: HashSet::from([
"query_target".to_string(),
"_private_name".to_string(),
"duplicate_name".to_string(),
]),
},
FileSymbols {
path_idx: 2,
language: LanguageKind::Rust,
defs: duplicate_defs,
refs: HashSet::from(["query_target".to_string()]),
},
];
let ranked = rank_symbols(&parsed, &files, Some("query"));
assert_eq!(ranked[0].name, "query_target");
let private = ranked
.iter()
.find(|symbol| symbol.name == "_private_name")
.unwrap();
let duplicate = ranked
.iter()
.find(|symbol| symbol.name == "duplicate_name")
.unwrap();
assert!(private.score <= 1.0, "{}", private.score);
assert!(duplicate.score <= 1.0, "{}", duplicate.score);
}
#[test]
fn repo_map_ranking_boosts_unreferenced_query_match_over_referenced_symbol() {
let files = vec![
source_file("src/query.rs", LanguageKind::Rust),
source_file("src/referenced.rs", LanguageKind::Rust),
];
let parsed = vec![
FileSymbols {
path_idx: 0,
language: LanguageKind::Rust,
defs: vec![SymbolDef {
name: "queryTarget".to_string(),
display_kind: "fn".to_string(),
file: 0,
line: 1,
score: 0.0,
}],
refs: HashSet::new(),
},
FileSymbols {
path_idx: 1,
language: LanguageKind::Rust,
defs: vec![SymbolDef {
name: "ReferencedSymbol".to_string(),
display_kind: "fn".to_string(),
file: 1,
line: 1,
score: 0.0,
}],
refs: HashSet::from(["ReferencedSymbol".to_string()]),
},
];
let ranked = rank_symbols(&parsed, &files, Some("query"));
assert_eq!(ranked[0].name, "queryTarget");
assert_eq!(ranked[0].score, 10.0);
assert_eq!(ranked[1].name, "ReferencedSymbol");
assert_eq!(ranked[1].score, 1.0);
}
#[test]
fn repo_map_render_output_format() {
let files = vec![source_file("src/agent/mod.rs", LanguageKind::Rust)];
let symbols = vec![
SymbolDef {
name: "run".to_string(),
display_kind: "fn".to_string(),
file: 0,
line: 142,
score: 2.0,
},
SymbolDef {
name: "AgentSession".to_string(),
display_kind: "struct".to_string(),
file: 0,
line: 89,
score: 1.0,
},
];
let (output, truncated) = render_map(&symbols, &files, ".", 512, 1, 1);
assert!(!truncated);
assert!(output.contains("src/agent/mod.rs:"), "{output}");
assert!(output.contains(" ├─ fn run (line 142)"), "{output}");
assert!(
output.contains(" └─ struct AgentSession (line 89)"),
"{output}"
);
}
#[test]
fn repo_map_render_truncates() {
let files = vec![source_file("src/agent/mod.rs", LanguageKind::Rust)];
let symbols = (0..100)
.map(|index| SymbolDef {
name: format!("symbol_{index}"),
display_kind: "fn".to_string(),
file: 0,
line: index + 1,
score: 1.0,
})
.collect::<Vec<_>>();
let (output, truncated) = render_map(&symbols, &files, ".", 30, 1, 1);
assert!(truncated);
assert!(output.contains("[repo_map output truncated at 120 bytes]"));
}
}