llman_core/
managed_block.rs1use crate::fs_utils::atomic_write_with_mode;
2use anyhow::{Result, anyhow};
3use std::fs;
4use std::path::Path;
5
6pub const LLMAN_PROMPTS_MARKER_START: &str = "<!-- LLMAN-PROMPTS:START -->";
7pub const LLMAN_PROMPTS_MARKER_END: &str = "<!-- LLMAN-PROMPTS:END -->";
8
9pub fn has_markers(content: &str, start_marker: &str, end_marker: &str) -> bool {
10 content.lines().any(|line| line.trim() == start_marker)
11 && content.lines().any(|line| line.trim() == end_marker)
12}
13
14pub fn has_llman_prompt_markers(content: &str) -> bool {
15 has_markers(
16 content,
17 LLMAN_PROMPTS_MARKER_START,
18 LLMAN_PROMPTS_MARKER_END,
19 )
20}
21
22pub fn is_marker_on_own_line(content: &str, marker_index: usize, marker_len: usize) -> bool {
23 let bytes = content.as_bytes();
24 let mut left = marker_index as isize - 1;
25 while left >= 0 {
26 let ch = bytes[left as usize] as char;
27 if ch == '\n' {
28 break;
29 }
30 if ch != ' ' && ch != '\t' && ch != '\r' {
31 return false;
32 }
33 left -= 1;
34 }
35
36 let mut right = marker_index + marker_len;
37 while right < bytes.len() {
38 let ch = bytes[right] as char;
39 if ch == '\n' {
40 break;
41 }
42 if ch != ' ' && ch != '\t' && ch != '\r' {
43 return false;
44 }
45 right += 1;
46 }
47
48 true
49}
50
51pub fn find_marker_index(content: &str, marker: &str, from_index: usize) -> Option<usize> {
52 let mut search_index = from_index;
53 while let Some(pos) = content[search_index..].find(marker) {
54 let idx = search_index + pos;
55 if is_marker_on_own_line(content, idx, marker.len()) {
56 return Some(idx);
57 }
58 search_index = idx + marker.len();
59 if search_index >= content.len() {
60 break;
61 }
62 }
63 None
64}
65
66pub fn update_file_with_markers(
67 path: &Path,
68 body: &str,
69 start_marker: &str,
70 end_marker: &str,
71) -> Result<()> {
72 let mut content = if path.exists() {
73 fs::read_to_string(path)?
74 } else {
75 String::new()
76 };
77
78 if !content.is_empty() {
79 let start_index = find_marker_index(&content, start_marker, 0);
80 let end_index = start_index
81 .and_then(|start| find_marker_index(&content, end_marker, start + start_marker.len()))
82 .or_else(|| find_marker_index(&content, end_marker, 0));
83
84 match (start_index, end_index) {
85 (Some(start), Some(end)) => {
86 if end < start {
87 return Err(anyhow!(
88 "Invalid marker state in {}. End marker appears before start marker.",
89 path.display()
90 ));
91 }
92 let before = &content[..start];
93 let after = &content[end + end_marker.len()..];
94 content = format!("{before}{start_marker}\n{body}\n{end_marker}{after}");
95 }
96 (None, None) => {
97 content = format!("{start_marker}\n{body}\n{end_marker}\n\n{content}");
98 }
99 _ => {
100 return Err(anyhow!(
101 "Invalid marker state in {}. Found start: {}, Found end: {}",
102 path.display(),
103 start_index.is_some(),
104 end_index.is_some()
105 ));
106 }
107 }
108 } else {
109 content = format!("{start_marker}\n{body}\n{end_marker}");
110 }
111
112 if let Some(parent) = path.parent()
113 && !parent.as_os_str().is_empty()
114 {
115 fs::create_dir_all(parent)?;
116 }
117 atomic_write_with_mode(path, content.as_bytes(), None)?;
118 Ok(())
119}
120
121pub fn update_text_with_markers(
122 existing: &str,
123 body: &str,
124 append_when_missing: bool,
125 start_marker: &str,
126 end_marker: &str,
127) -> String {
128 let body = body.trim_end();
129
130 let mut start_idx: Option<usize> = None;
131 let mut end_idx: Option<usize> = None;
132
133 let mut cursor = 0usize;
135 for line in existing.split_inclusive('\n') {
136 let line_start = cursor;
137 let line_end = cursor + line.len();
138 let trimmed = line.trim_matches(['\r', '\n']);
139 if trimmed.trim() == start_marker {
140 start_idx = Some(line_start);
141 } else if trimmed.trim() == end_marker {
142 end_idx = Some(line_end);
143 break;
144 }
145 cursor = line_end;
146 }
147
148 match (start_idx, end_idx) {
149 (Some(start), Some(end)) => {
150 let before = &existing[..start];
151 let after = &existing[end..];
152 let mut out = String::new();
153 out.push_str(before);
154 out.push_str(start_marker);
155 out.push('\n');
156 out.push_str(body);
157 out.push('\n');
158 out.push_str(end_marker);
159 out.push('\n');
160 out.push_str(after);
161 out
162 }
163 _ if append_when_missing => {
164 let mut out = existing.to_string();
165 if !out.ends_with('\n') && !out.is_empty() {
166 out.push('\n');
167 }
168 if !out.ends_with('\n') {
169 out.push('\n');
170 }
171 out.push_str(start_marker);
172 out.push('\n');
173 out.push_str(body);
174 out.push('\n');
175 out.push_str(end_marker);
176 out.push('\n');
177 out
178 }
179 _ => existing.to_string(),
180 }
181}