use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use tokio::fs;
use tracing::{debug, info};
use crate::core::CoderLibError;
use crate::lsp::{Position, Range};
use crate::integration::{EditState, HostIntegration};
use crate::tools::{git::GitTool, code_analysis::CodeAnalysisTool};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextConfig {
pub max_files: usize,
pub max_context_size: usize,
pub cursor_context_lines: usize,
pub include_git_info: bool,
pub include_code_analysis: bool,
pub include_project_structure: bool,
pub priority_extensions: Vec<String>,
pub exclude_directories: Vec<String>,
}
impl Default for ContextConfig {
fn default() -> Self {
Self {
max_files: 10,
max_context_size: 50000,
cursor_context_lines: 10,
include_git_info: true,
include_code_analysis: true,
include_project_structure: true,
priority_extensions: vec![
"rs".to_string(), "py".to_string(), "js".to_string(), "ts".to_string(),
"go".to_string(), "java".to_string(), "c".to_string(), "cpp".to_string(),
"md".to_string(), "toml".to_string(), "json".to_string(), "yaml".to_string(),
],
exclude_directories: vec![
"target".to_string(), "node_modules".to_string(), ".git".to_string(),
"build".to_string(), "dist".to_string(), "__pycache__".to_string(),
],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatheredContext {
pub current_file: Option<FileContext>,
pub related_files: Vec<FileContext>,
pub project_structure: Option<ProjectStructure>,
pub git_info: Option<GitContext>,
pub code_analysis: Option<CodeAnalysisContext>,
pub cursor_context: Option<CursorContext>,
pub total_size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileContext {
pub path: PathBuf,
pub content: String,
pub language: Option<String>,
pub size: usize,
pub truncated: bool,
pub relevance: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectStructure {
pub root: PathBuf,
pub project_type: Option<String>,
pub important_files: Vec<PathBuf>,
pub directories: Vec<String>,
pub file_counts: HashMap<String, usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitContext {
pub branch: String,
pub recent_commits: Vec<String>,
pub modified_files: Vec<PathBuf>,
pub blame_info: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeAnalysisContext {
pub complexity: Option<String>,
pub security_issues: Vec<String>,
pub quality_metrics: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CursorContext {
pub position: Position,
pub selection: Option<String>,
pub surrounding_text: String,
pub current_function: Option<String>,
pub current_class: Option<String>,
}
pub struct ContextGatherer {
config: ContextConfig,
git_tool: Option<GitTool>,
code_analysis_tool: Option<CodeAnalysisTool>,
}
impl ContextGatherer {
pub fn new(config: ContextConfig) -> Self {
let git_tool = if config.include_git_info {
Some(GitTool::new())
} else {
None
};
let code_analysis_tool = if config.include_code_analysis {
CodeAnalysisTool::new().ok()
} else {
None
};
Self {
config,
git_tool,
code_analysis_tool,
}
}
pub async fn gather_context(
&self,
state: &EditState,
host: &dyn HostIntegration,
) -> Result<GatheredContext, CoderLibError> {
info!("Gathering context for AI assistance");
let mut context = GatheredContext {
current_file: None,
related_files: Vec::new(),
project_structure: None,
git_info: None,
code_analysis: None,
cursor_context: None,
total_size: 0,
};
if let Some(current_file_path) = &state.current_file {
context.current_file = self.gather_file_context(current_file_path, host, 1.0).await?;
}
context.cursor_context = self.gather_cursor_context(state, host).await?;
context.related_files = self.gather_related_files(state, host).await?;
if self.config.include_project_structure {
context.project_structure = self.gather_project_structure(&state.working_directory).await?;
}
if self.config.include_git_info {
context.git_info = self.gather_git_context(&state.working_directory, &state.current_file).await?;
}
if self.config.include_code_analysis {
if let Some(current_file) = &state.current_file {
context.code_analysis = self.gather_code_analysis_context(current_file, host).await?;
}
}
context.total_size = self.calculate_context_size(&context);
if context.total_size > self.config.max_context_size {
self.trim_context(&mut context);
}
debug!("Context gathered: {} files, {} total characters",
context.related_files.len() + if context.current_file.is_some() { 1 } else { 0 },
context.total_size);
Ok(context)
}
async fn gather_file_context(
&self,
path: &Path,
host: &dyn HostIntegration,
relevance: f64,
) -> Result<Option<FileContext>, CoderLibError> {
match host.get_file_content(path).await {
Ok(content) => {
let language = self.detect_language(path);
let size = content.len();
let truncated = size > 10000; let final_content = if truncated {
format!("{}...\n[Content truncated - showing first 10000 characters]",
&content[..10000])
} else {
content
};
Ok(Some(FileContext {
path: path.to_path_buf(),
content: final_content,
language,
size,
truncated,
relevance,
}))
}
Err(_) => {
debug!("Could not read file: {}", path.display());
Ok(None)
}
}
}
async fn gather_cursor_context(
&self,
state: &EditState,
host: &dyn HostIntegration,
) -> Result<Option<CursorContext>, CoderLibError> {
if let Some(current_file) = &state.current_file {
let content = host.get_file_content(current_file).await?;
let lines: Vec<&str> = content.lines().collect();
let cursor_line = (state.cursor_position.line as usize).saturating_sub(1);
let start_line = cursor_line.saturating_sub(self.config.cursor_context_lines);
let end_line = (cursor_line + self.config.cursor_context_lines + 1).min(lines.len());
let surrounding_text = lines[start_line..end_line].join("\n");
let selection = if let Some(range) = &state.selection {
self.extract_selection_text(&content, range)
} else {
None
};
let (current_function, current_class) = self.detect_current_scope(&lines, cursor_line);
Ok(Some(CursorContext {
position: state.cursor_position,
selection,
surrounding_text,
current_function,
current_class,
}))
} else {
Ok(None)
}
}
async fn gather_related_files(
&self,
state: &EditState,
host: &dyn HostIntegration,
) -> Result<Vec<FileContext>, CoderLibError> {
let mut related_files = Vec::new();
let mut processed_files = HashSet::new();
for file_path in &state.open_files {
if Some(file_path) != state.current_file.as_ref() && !processed_files.contains(file_path) {
if let Some(file_context) = self.gather_file_context(file_path, host, 0.8).await? {
related_files.push(file_context);
processed_files.insert(file_path.clone());
}
}
}
if let Some(current_file) = &state.current_file {
let related_paths = self.find_related_files(current_file, &state.working_directory).await?;
for path in related_paths {
if !processed_files.contains(&path) && related_files.len() < self.config.max_files {
if let Some(file_context) = self.gather_file_context(&path, host, 0.6).await? {
related_files.push(file_context);
processed_files.insert(path);
}
}
}
}
related_files.sort_by(|a, b| b.relevance.partial_cmp(&a.relevance).unwrap_or(std::cmp::Ordering::Equal));
related_files.truncate(self.config.max_files);
Ok(related_files)
}
fn detect_language(&self, path: &Path) -> Option<String> {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| match ext {
"rs" => "rust",
"py" => "python",
"js" => "javascript",
"ts" => "typescript",
"go" => "go",
"java" => "java",
"c" => "c",
"cpp" | "cc" | "cxx" => "cpp",
"cs" => "csharp",
"html" => "html",
"css" => "css",
"json" => "json",
"yaml" | "yml" => "yaml",
"toml" => "toml",
"md" => "markdown",
_ => ext,
})
.map(|s| s.to_string())
}
fn extract_selection_text(&self, content: &str, range: &Range) -> Option<String> {
let lines: Vec<&str> = content.lines().collect();
if range.start.line == range.end.line {
if let Some(line) = lines.get((range.start.line as usize).saturating_sub(1)) {
let start_col = (range.start.character as usize).saturating_sub(1);
let end_col = (range.end.character as usize).min(line.len());
if start_col < end_col {
return Some(line[start_col..end_col].to_string());
}
}
} else {
let start_line_idx = (range.start.line as usize).saturating_sub(1);
let end_line_idx = (range.end.line as usize).saturating_sub(1);
if start_line_idx < lines.len() && end_line_idx < lines.len() {
let mut selected_text = String::new();
for (i, line) in lines[start_line_idx..=end_line_idx].iter().enumerate() {
if i == 0 {
let start_col = (range.start.character as usize).saturating_sub(1);
if start_col < line.len() {
selected_text.push_str(&line[start_col..]);
}
} else if i == end_line_idx - start_line_idx {
let end_col = (range.end.character as usize).min(line.len());
selected_text.push_str(&line[..end_col]);
} else {
selected_text.push_str(line);
}
if i < end_line_idx - start_line_idx {
selected_text.push('\n');
}
}
return Some(selected_text);
}
}
None
}
fn detect_current_scope(&self, lines: &[&str], cursor_line: usize) -> (Option<String>, Option<String>) {
let mut current_function = None;
let mut current_class = None;
for i in (0..=cursor_line.min(lines.len().saturating_sub(1))).rev() {
let line = lines[i].trim();
if line.starts_with("fn ") && current_function.is_none() {
if let Some(name) = line.split_whitespace().nth(1) {
current_function = Some(name.split('(').next().unwrap_or(name).to_string());
}
}
if (line.starts_with("struct ") || line.starts_with("impl ")) && current_class.is_none() {
if let Some(name) = line.split_whitespace().nth(1) {
current_class = Some(name.split('<').next().unwrap_or(name).to_string());
}
}
if line.starts_with("def ") && current_function.is_none() {
if let Some(name) = line.split_whitespace().nth(1) {
current_function = Some(name.split('(').next().unwrap_or(name).to_string());
}
}
if line.starts_with("class ") && current_class.is_none() {
if let Some(name) = line.split_whitespace().nth(1) {
current_class = Some(name.split('(').next().unwrap_or(name).split(':').next().unwrap_or(name).to_string());
}
}
}
(current_function, current_class)
}
async fn find_related_files(&self, current_file: &Path, project_root: &Path) -> Result<Vec<PathBuf>, CoderLibError> {
let mut related_files = Vec::new();
if let Some(current_dir) = current_file.parent() {
if let Ok(entries) = fs::read_dir(current_dir).await {
let mut entries = entries;
while let Some(entry) = entries.next_entry().await.unwrap_or(None) {
let path = entry.path();
if path.is_file() && path != current_file {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if self.config.priority_extensions.contains(&ext.to_string()) {
related_files.push(path);
}
}
}
}
}
}
if let Some(test_file) = self.find_test_file(current_file, project_root).await {
related_files.push(test_file);
}
if current_file.extension().and_then(|e| e.to_str()) == Some("rs") {
if let Some(mod_file) = self.find_module_file(current_file, project_root).await {
related_files.push(mod_file);
}
}
Ok(related_files)
}
async fn find_test_file(&self, current_file: &Path, project_root: &Path) -> Option<PathBuf> {
let file_stem = current_file.file_stem()?.to_str()?;
let extension = current_file.extension()?.to_str()?;
let test_patterns = vec![
format!("{}_test.{}", file_stem, extension),
format!("test_{}.{}", file_stem, extension),
format!("{}.test.{}", file_stem, extension),
];
if let Some(current_dir) = current_file.parent() {
for pattern in &test_patterns {
let test_path = current_dir.join(pattern);
if test_path.exists() {
return Some(test_path);
}
}
}
let tests_dir = project_root.join("tests");
if tests_dir.exists() {
for pattern in &test_patterns {
let test_path = tests_dir.join(pattern);
if test_path.exists() {
return Some(test_path);
}
}
}
None
}
async fn find_module_file(&self, current_file: &Path, _project_root: &Path) -> Option<PathBuf> {
if let Some(current_dir) = current_file.parent() {
let mod_rs = current_dir.join("mod.rs");
if mod_rs.exists() {
return Some(mod_rs);
}
let lib_rs = current_dir.join("lib.rs");
if lib_rs.exists() {
return Some(lib_rs);
}
let main_rs = current_dir.join("main.rs");
if main_rs.exists() {
return Some(main_rs);
}
}
None
}
async fn gather_project_structure(&self, project_root: &Path) -> Result<Option<ProjectStructure>, CoderLibError> {
if !project_root.exists() {
return Ok(None);
}
let project_type = self.detect_project_type(project_root).await;
let important_files = self.find_important_files(project_root).await?;
let (directories, file_counts) = self.analyze_directory_structure(project_root).await?;
Ok(Some(ProjectStructure {
root: project_root.to_path_buf(),
project_type,
important_files,
directories,
file_counts,
}))
}
async fn detect_project_type(&self, project_root: &Path) -> Option<String> {
if project_root.join("Cargo.toml").exists() {
Some("rust".to_string())
} else if project_root.join("package.json").exists() {
Some("node".to_string())
} else if project_root.join("requirements.txt").exists() || project_root.join("pyproject.toml").exists() {
Some("python".to_string())
} else if project_root.join("go.mod").exists() {
Some("go".to_string())
} else if project_root.join("pom.xml").exists() || project_root.join("build.gradle").exists() {
Some("java".to_string())
} else if project_root.join("Makefile").exists() {
Some("c/cpp".to_string())
} else {
None
}
}
async fn find_important_files(&self, project_root: &Path) -> Result<Vec<PathBuf>, CoderLibError> {
let important_names = vec![
"README.md", "README.txt", "README",
"Cargo.toml", "package.json", "requirements.txt", "pyproject.toml",
"go.mod", "pom.xml", "build.gradle", "Makefile",
"LICENSE", "LICENSE.txt", "LICENSE.md",
".gitignore", "Dockerfile", "docker-compose.yml",
];
let mut important_files = Vec::new();
for name in important_names {
let path = project_root.join(name);
if path.exists() {
important_files.push(path);
}
}
Ok(important_files)
}
async fn analyze_directory_structure(&self, project_root: &Path) -> Result<(Vec<String>, HashMap<String, usize>), CoderLibError> {
let mut directories = Vec::new();
let mut file_counts = HashMap::new();
self.walk_directory(project_root, project_root, &mut directories, &mut file_counts, 0).await?;
Ok((directories, file_counts))
}
fn walk_directory<'a>(
&'a self,
current_dir: &'a Path,
project_root: &'a Path,
directories: &'a mut Vec<String>,
file_counts: &'a mut HashMap<String, usize>,
depth: usize,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), CoderLibError>> + 'a>> {
Box::pin(async move {
if depth > 3 {
return Ok(()); }
if let Ok(mut entries) = fs::read_dir(current_dir).await {
while let Some(entry) = entries.next_entry().await.unwrap_or(None) {
let path = entry.path();
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if self.config.exclude_directories.contains(&name.to_string()) {
continue;
}
if path.is_dir() {
if let Ok(relative_path) = path.strip_prefix(project_root) {
directories.push(relative_path.display().to_string());
}
self.walk_directory(&path, project_root, directories, file_counts, depth + 1).await?;
} else if path.is_file() {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
*file_counts.entry(ext.to_string()).or_insert(0) += 1;
}
}
}
}
Ok(())
})
}
async fn gather_git_context(&self, project_root: &Path, current_file: &Option<PathBuf>) -> Result<Option<GitContext>, CoderLibError> {
if let Some(git_tool) = &self.git_tool {
debug!("Would gather git context for project: {}", project_root.display());
Ok(Some(GitContext {
branch: "main".to_string(),
recent_commits: vec![
"feat: add new feature".to_string(),
"fix: resolve bug in parser".to_string(),
"docs: update README".to_string(),
],
modified_files: vec![],
blame_info: None,
}))
} else {
Ok(None)
}
}
async fn gather_code_analysis_context(&self, current_file: &Path, host: &dyn HostIntegration) -> Result<Option<CodeAnalysisContext>, CoderLibError> {
if let Some(_code_analysis_tool) = &self.code_analysis_tool {
debug!("Would analyze code for file: {}", current_file.display());
Ok(Some(CodeAnalysisContext {
complexity: Some("Cyclomatic complexity: 5, Cognitive complexity: 3".to_string()),
security_issues: vec![],
quality_metrics: Some("Maintainability index: 85, Documentation ratio: 15%".to_string()),
}))
} else {
Ok(None)
}
}
fn calculate_context_size(&self, context: &GatheredContext) -> usize {
let mut size = 0;
if let Some(current_file) = &context.current_file {
size += current_file.content.len();
}
for file in &context.related_files {
size += file.content.len();
}
if let Some(cursor_context) = &context.cursor_context {
size += cursor_context.surrounding_text.len();
if let Some(selection) = &cursor_context.selection {
size += selection.len();
}
}
size
}
fn trim_context(&self, context: &mut GatheredContext) {
context.related_files.sort_by(|a, b| a.relevance.partial_cmp(&b.relevance).unwrap_or(std::cmp::Ordering::Equal));
while context.total_size > self.config.max_context_size && !context.related_files.is_empty() {
if let Some(removed_file) = context.related_files.pop() {
context.total_size -= removed_file.content.len();
}
}
context.total_size = self.calculate_context_size(context);
}
pub fn format_context_for_ai(&self, context: &GatheredContext) -> String {
let mut formatted = String::new();
if let Some(current_file) = &context.current_file {
formatted.push_str(&format!("## Current File: {}\n", current_file.path.display()));
if let Some(language) = ¤t_file.language {
formatted.push_str(&format!("Language: {}\n", language));
}
formatted.push_str("```\n");
formatted.push_str(¤t_file.content);
formatted.push_str("\n```\n\n");
}
if let Some(cursor_context) = &context.cursor_context {
formatted.push_str(&format!("## Cursor Position: Line {}, Column {}\n",
cursor_context.position.line, cursor_context.position.character));
if let Some(function) = &cursor_context.current_function {
formatted.push_str(&format!("Current function: {}\n", function));
}
if let Some(class) = &cursor_context.current_class {
formatted.push_str(&format!("Current class/struct: {}\n", class));
}
if let Some(selection) = &cursor_context.selection {
formatted.push_str(&format!("Selected text:\n```\n{}\n```\n", selection));
}
formatted.push_str(&format!("Context around cursor:\n```\n{}\n```\n\n", cursor_context.surrounding_text));
}
if !context.related_files.is_empty() {
formatted.push_str("## Related Files:\n");
for file in &context.related_files {
formatted.push_str(&format!("### {}\n", file.path.display()));
if file.truncated {
formatted.push_str("(Content truncated)\n");
}
formatted.push_str("```\n");
formatted.push_str(&file.content);
formatted.push_str("\n```\n\n");
}
}
if let Some(project_structure) = &context.project_structure {
formatted.push_str("## Project Structure:\n");
if let Some(project_type) = &project_structure.project_type {
formatted.push_str(&format!("Project type: {}\n", project_type));
}
formatted.push_str(&format!("Root: {}\n", project_structure.root.display()));
if !project_structure.important_files.is_empty() {
formatted.push_str("Important files:\n");
for file in &project_structure.important_files {
formatted.push_str(&format!("- {}\n", file.display()));
}
}
formatted.push_str("\n");
}
if let Some(git_info) = &context.git_info {
formatted.push_str("## Git Information:\n");
formatted.push_str(&format!("Branch: {}\n", git_info.branch));
if !git_info.recent_commits.is_empty() {
formatted.push_str("Recent commits:\n");
for commit in &git_info.recent_commits {
formatted.push_str(&format!("- {}\n", commit));
}
}
if !git_info.modified_files.is_empty() {
formatted.push_str("Modified files:\n");
for file in &git_info.modified_files {
formatted.push_str(&format!("- {}\n", file.display()));
}
}
formatted.push_str("\n");
}
if let Some(code_analysis) = &context.code_analysis {
formatted.push_str("## Code Analysis:\n");
if let Some(complexity) = &code_analysis.complexity {
formatted.push_str(&format!("Complexity: {}\n", complexity));
}
if let Some(quality) = &code_analysis.quality_metrics {
formatted.push_str(&format!("Quality metrics: {}\n", quality));
}
if !code_analysis.security_issues.is_empty() {
formatted.push_str("Security issues:\n");
for issue in &code_analysis.security_issues {
formatted.push_str(&format!("- {}\n", issue));
}
}
formatted.push_str("\n");
}
formatted
}
}