use std::path::{Path, PathBuf};
use anyhow::Result;
use clap::Args;
use tldr_core::types::RelevantContext;
use tldr_core::{get_relevant_context, Language};
use crate::commands::daemon_router::{params_with_entry_depth, try_daemon_route};
use crate::output::{OutputFormat, OutputWriter};
#[derive(Debug, Args)]
pub struct ContextArgs {
pub entry: String,
#[arg(default_value = ".")]
pub path: PathBuf,
#[arg(long, short = 'p')]
pub project: Option<PathBuf>,
#[arg(long, short = 'l')]
pub lang: Option<Language>,
#[arg(long, short = 'd', default_value = "3")]
pub depth: usize,
#[arg(long)]
pub include_docstrings: bool,
#[arg(long)]
pub file: Option<PathBuf>,
}
impl ContextArgs {
fn effective_project(&self) -> PathBuf {
match &self.project {
Some(p) if self.path == PathBuf::from(".") => p.clone(),
_ => self.path.clone(),
}
}
pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
let writer = OutputWriter::new(format, quiet);
let mut project_path = self.effective_project();
let (entry, derived_file): (String, Option<PathBuf>) =
match split_file_func_shorthand(&self.entry) {
Some((file, func)) => (func, Some(file)),
None => (self.entry.clone(), None),
};
let effective_file: Option<PathBuf> =
self.file.clone().or_else(|| derived_file.clone());
if derived_file.is_some()
&& self.path == PathBuf::from(".")
&& self.project.is_none()
{
if let Some(file) = effective_file.as_ref() {
if let Some(root) = infer_project_root_from_file(file) {
project_path = root;
}
}
}
let language = self
.lang
.unwrap_or_else(|| Language::from_directory(&project_path).unwrap_or(Language::Python));
if effective_file.is_none() {
if let Some(context) = try_daemon_route::<RelevantContext>(
&project_path,
"context",
params_with_entry_depth(&entry, Some(self.depth)),
) {
if writer.is_text() {
let text = context.to_llm_string();
writer.write_text(&text)?;
return Ok(());
} else {
writer.write(&context)?;
return Ok(());
}
}
}
writer.progress(&format!(
"Building context for {} (depth={})...",
entry, self.depth
));
let context = get_relevant_context(
&project_path,
&entry,
self.depth,
language,
self.include_docstrings,
effective_file.as_deref(),
)?;
if writer.is_text() {
let text = context.to_llm_string();
writer.write_text(&text)?;
} else {
writer.write(&context)?;
}
Ok(())
}
}
fn split_file_func_shorthand(entry: &str) -> Option<(PathBuf, String)> {
let mut idx = entry.rfind(':')?;
loop {
if idx == 0 || idx + 1 >= entry.len() {
match entry[..idx].rfind(':') {
Some(prev) => {
idx = prev;
continue;
}
None => return None,
}
}
let file_part = &entry[..idx];
let func_part = &entry[idx + 1..];
let candidate = PathBuf::from(file_part);
if candidate.is_file() && !func_part.is_empty() && !func_part.starts_with(':') {
return Some((candidate, func_part.to_string()));
}
match entry[..idx].rfind(':') {
Some(prev) => idx = prev,
None => return None,
}
}
}
fn infer_project_root_from_file(file: &Path) -> Option<PathBuf> {
let abs = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
let parent = abs.parent()?;
const MARKERS: &[&str] = &[
".git",
"package.json",
"Cargo.toml",
"go.mod",
"pyproject.toml",
"pom.xml",
"build.gradle",
"build.gradle.kts",
"mix.exs",
"dune-project",
"Package.swift",
];
let mut cursor: Option<&Path> = Some(parent);
while let Some(dir) = cursor {
for m in MARKERS {
if dir.join(m).exists() {
return Some(dir.to_path_buf());
}
}
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
if entry
.path()
.extension()
.and_then(|e| e.to_str())
.map(|e| e == "csproj" || e == "sln")
.unwrap_or(false)
{
return Some(dir.to_path_buf());
}
}
}
cursor = dir.parent();
}
Some(parent.to_path_buf())
}