use super::embedded::{EmbeddedLanguage, MAX_FILES, MAX_SCAN_BYTES, MAX_TREE_NODES, read_source};
use super::*;
use ast_grep_core::{AstGrep, Language, tree_sitter::StrDoc};
use ast_grep_language::SupportLang;
use ast_grep_outline::{
DEFAULT_OUTLINE_RULES,
combined_extractor::CombinedExtractors,
extractor::parse_outline_rules,
model::OutlineEntry,
options::{
OutlineEntryDetail as Detail, OutlineExtractorOptions, OutlineFlagFilter as Flag,
OutlineMemberOptions,
},
};
use std::collections::HashMap;
fn options(args: &AstGrepArgs) -> anyhow::Result<OutlineExtractorOptions> {
let view = args.view.as_deref().unwrap_or("digest");
let items = args.items.as_deref().unwrap_or("structure");
Ok(OutlineExtractorOptions {
symbol_types: args
.symbol_type
.as_deref()
.map(|types| {
types
.split(',')
.map(|kind| serde_json::from_value(json!(kind.trim())))
.collect::<Result<Vec<_>, _>>()
})
.transpose()?,
item_regex: args.name.as_deref().map(regex::Regex::new).transpose()?,
imports: match items {
"structure" => Flag::No,
"imports" => Flag::Yes,
_ => Flag::Any,
},
exported: if items == "exports" {
Flag::Yes
} else {
Flag::Any
},
detail: if view == "names" && args.name.is_none() {
Detail::Name
} else {
Detail::Signature
},
members: matches!(view, "digest" | "expanded").then_some(OutlineMemberOptions {
public: if args.pub_members == Some(true) {
Flag::Yes
} else {
Flag::Any
},
detail: if view == "expanded" {
Detail::Signature
} else {
Detail::Name
},
}),
})
}
fn entry_json(entry: &OutlineEntry<'_>) -> serde_json::Value {
let range = &entry.range;
json!({
"name": entry.name, "symbolType": entry.symbol_type,
"signature": entry.signature, "astKind": entry.ast_kind,
"range": {
"start": {"line": range.start.line + 1, "column": range.start.column + 1},
"end": {"line": range.end.line + 1, "column": range.end.column + 1}
}
})
}
pub(super) fn run(
runtime: &ToolRuntime,
args: &AstGrepArgs,
resolved: &Path,
cancellation: &AgentCancellation,
timeout: Duration,
) -> anyhow::Result<ToolResult> {
let started = Instant::now();
let explicit = args
.language
.as_deref()
.map(str::parse::<SupportLang>)
.transpose()
.map_err(|_| anyhow::anyhow!("unsupported ast-grep language"))?;
let options = options(args)?;
let extraction_options = OutlineExtractorOptions {
symbol_types: None,
item_regex: None,
imports: Flag::Any,
exported: Flag::Any,
..options.clone()
};
let mut compiled = HashMap::new();
let mut state = AstGrepRunState::default();
let mut rules_by_language = HashMap::<_, Vec<_>>::new();
cancellation.check()?;
if started.elapsed() < timeout {
for rule in parse_outline_rules::<SupportLang>(DEFAULT_OUTLINE_RULES)? {
cancellation.check()?;
rules_by_language
.entry(rule.common().language)
.or_default()
.push(rule);
}
}
let mut files = 0;
let mut bytes = 0;
let mut skipped = 0;
let mut returned = 0;
let limit = args.limit.unwrap_or(AST_GREP_DEFAULT_LIMIT);
let allow_outside = runtime.ast_grep_absolute_paths
&& args
.path
.as_deref()
.is_some_and(|p| Path::new(p).is_absolute());
let mut visitor = |path: PathBuf| -> anyhow::Result<bool> {
cancellation.check()?;
if started.elapsed() >= timeout {
state.timed_out = true;
return Ok(false);
}
files += 1;
if files > MAX_FILES || bytes >= MAX_SCAN_BYTES {
state.workset_truncated = true;
return Ok(false);
}
let Some(language) = explicit.or_else(|| SupportLang::from_path(&path)) else {
return Ok(true);
};
if !rules_by_language.contains_key(&language) && !compiled.contains_key(&language) {
return Ok(true);
}
let Some(path_text) = path.to_str() else {
skipped += 1;
state.workset_truncated = true;
return Ok(true);
};
let path = runtime
.resolve_existing_path(path_text, ExistingPathPolicy::ast_grep(allow_outside))?;
let Some(src) = read_source(&path, &mut bytes) else {
skipped += 1;
state.workset_truncated = true;
return Ok(true);
};
if src.contains('\0') {
return Ok(true);
}
let lang = EmbeddedLanguage {
language,
cancellation: cancellation.clone(),
started,
timeout,
};
let doc = match lang.parse(src, MAX_TREE_NODES) {
Ok(doc) => doc,
Err(_) => {
cancellation.check()?;
skipped += 1;
state.workset_truncated = true;
state.timed_out |= lang.stopped();
return Ok(!state.timed_out);
}
};
if let std::collections::hash_map::Entry::Vacant(entry) = compiled.entry(language) {
let rules = rules_by_language.remove(&language).unwrap_or_default();
entry.insert(CombinedExtractors::try_from_rules(
rules,
extraction_options.clone(),
&Default::default(),
)?);
}
cancellation.check()?;
if lang.stopped() {
state.timed_out = true;
return Ok(false);
}
let root = AstGrep::doc(StrDoc {
src: doc.src,
tree: doc.tree,
lang: language,
});
let mut items = compiled[&language].extract(root.root());
loop {
cancellation.check()?;
if lang.stopped() {
state.timed_out = true;
return Ok(false);
}
let item = items.next();
cancellation.check()?;
if lang.stopped() {
state.timed_out = true;
return Ok(false);
}
let Some(item) = item else {
break;
};
if !options.keep_item(&item) {
continue;
}
if returned >= limit {
state.workset_truncated = true;
return Ok(false);
}
let mut value = entry_json(&item.entry);
value["isImport"] = json!(item.is_import);
value["isExported"] = json!(item.is_exported);
if !item.members.is_empty() {
let mut members = Vec::with_capacity(item.members.len());
for member in item.members {
cancellation.check()?;
if lang.stopped() {
state.timed_out = true;
return Ok(false);
}
let mut value = entry_json(&member.entry);
value["isPublic"] = json!(member.is_public);
members.push(value);
}
value["members"] = json!(members);
}
let entry = serde_json::to_string(
&json!({"path": path, "language": language.to_string(), "item": value}),
)?;
cancellation.check()?;
if lang.stopped() {
state.timed_out = true;
return Ok(false);
}
if entry.len() + 1 > AST_GREP_STDOUT_MAX_BYTES.saturating_sub(state.stdout.len()) {
state.stdout_truncated = true;
state.workset_truncated = true;
return Ok(false);
}
append_batch_output(&mut state.stdout, &entry);
returned += 1;
}
Ok(true)
};
if resolved.is_dir() {
let status = runtime.workspace_walker.visit_files_with_deadline(
WorkspaceWalkOptions {
root: resolved,
skip_dirs: &[".git"],
cancel_interval: 32,
},
Some(cancellation),
Some(started + timeout),
&mut visitor,
)?;
state.workset_truncated |= status.walk_errors > 0 || status.entries_omitted > 0;
} else {
visitor(resolved.to_path_buf())?;
}
cancellation.check()?;
state.timed_out |= started.elapsed() >= timeout;
let partial = state.workset_truncated || state.timed_out;
Ok(ToolResult {
tool_name: tool_name::AST_GREP.into(),
success: !state.timed_out,
content: state.stdout,
metadata: json!({
meta::ENGINE: "ast-grep", "execution": "embedded", "operation": "outline",
"items": args.items.as_deref().unwrap_or("structure"), "view": args.view.as_deref().unwrap_or("digest"),
"symbols_returned": returned, "files_skipped": skipped,
meta::EXIT_CODE: if state.timed_out { -1 } else { 0 }, meta::TIMED_OUT: state.timed_out,
meta::TRUNCATED: partial, "partial": partial,
meta::STDOUT_TRUNCATED: state.stdout_truncated, "workset_truncated": state.workset_truncated,
"line_base": 1, "column_base": 1
}),
display: ToolResultDisplay::default(),
})
}