返回 DeepSeek-TUI-2026
project_doc.rs
根目录 / crates / tui / src / project_doc.rs
1 //! Project document discovery and loading
2 //!
3 //! Supports auto-discovery of project instructions like Claude Code.
4 //! Priority: AGENTS.md > .claude/instructions.md > CLAUDE.md > .deepseek/instructions.md
5
6 use std::path::{Path, PathBuf};
7
8 /// Document filenames to search for (in priority order)
9 pub const DOC_FILENAMES: &[&str] = &[
10 "AGENTS.md",
11 ".claude/instructions.md",
12 "CLAUDE.md",
13 ".deepseek/instructions.md",
14 ];
15
16 /// Maximum bytes to read from project docs (default: 32KB)
17 #[allow(dead_code)] // Used by read_project_docs
18 pub const DEFAULT_MAX_BYTES: usize = 32768;
19
20 /// A discovered project document
21 #[derive(Debug, Clone)]
22 #[allow(dead_code)]
23 pub struct ProjectDoc {
24 pub path: PathBuf,
25 pub content: String,
26 }
27
28 /// Walk from cwd up to git root, collecting all project docs
29 pub fn discover_paths(cwd: &Path) -> Vec<PathBuf> {
30 let mut paths = Vec::new();
31 let git_root = find_git_root(cwd);
32
33 let mut current = cwd.to_path_buf();
34 loop {
35 for filename in DOC_FILENAMES {
36 let doc_path = current.join(filename);
37 if doc_path.exists() && doc_path.is_file() {
38 paths.push(doc_path);
39 }
40 }
41
42 // Stop at git root or filesystem root
43 if let Some(ref root) = git_root
44 && current == *root
45 {
46 break;
47 }
48
49 match current.parent() {
50 Some(parent) if parent != current => {
51 current = parent.to_path_buf();
52 }
53 _ => break,
54 }
55 }
56
57 // Reverse so parent docs come first (will be overridden by child docs)
58 paths.reverse();
59 paths
60 }
61
62 /// Find the git root directory from cwd
63 fn find_git_root(cwd: &Path) -> Option<PathBuf> {
64 let mut current = cwd.to_path_buf();
65 loop {
66 if current.join(".git").exists() {
67 return Some(current);
68 }
69 match current.parent() {
70 Some(parent) if parent != current => {
71 current = parent.to_path_buf();
72 }
73 _ => return None,
74 }
75 }
76 }
77
78 /// Read and concatenate project docs with byte limit
79 #[allow(dead_code)] // Public API; project_context.rs provides the active code path
80 pub fn read_project_docs(paths: &[PathBuf], max_bytes: usize) -> Option<String> {
81 if paths.is_empty() {
82 return None;
83 }
84
85 let mut combined = String::new();
86 let mut total_bytes = 0;
87
88 for path in paths {
89 if total_bytes >= max_bytes {
90 break;
91 }
92
93 if let Ok(content) = std::fs::read_to_string(path) {
94 let remaining = max_bytes.saturating_sub(total_bytes);
95 let content = if content.len() > remaining {
96 // Truncate to remaining bytes at a word boundary if possible
97 let truncated: String = content.chars().take(remaining).collect();
98 format!("{truncated}\n\n[...truncated...]")
99 } else {
100 content
101 };
102
103 if !combined.is_empty() {
104 combined.push_str("\n\n---\n\n");
105 }
106 combined.push_str(&format_instructions(path, &content));
107 total_bytes += content.len();
108 }
109 }
110
111 if combined.is_empty() {
112 None
113 } else {
114 Some(combined)
115 }
116 }
117
118 /// Format project instructions for injection into system prompt
119 #[allow(dead_code)] // Used by read_project_docs
120 pub fn format_instructions(path: &Path, content: &str) -> String {
121 format!(
122 "# Project instructions from {}\n\n<INSTRUCTIONS>\n{}\n</INSTRUCTIONS>",
123 path.display(),
124 content.trim()
125 )
126 }
127
128 /// Load project docs from workspace with default settings
129 #[allow(dead_code)] // Convenience function; project_context.rs provides the active code path
130 pub fn load_from_workspace(workspace: &Path) -> Option<String> {
131 let paths = discover_paths(workspace);
132 read_project_docs(&paths, DEFAULT_MAX_BYTES)
133 }
134
134 lines RUST