Skip to main content

llman_core/
managed_block.rs

1use 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    // Find markers on their own line (trimmed match), tracking byte indices.
134    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}