use regex::Regex;
use serde_json::Value;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::LazyLock;
thread_local! {
static CACHED_RE: RefCell<Option<(String, Regex)>> = const { RefCell::new(None) };
}
fn get_or_compile_regex(pattern: &str) -> Result<Regex, regex::Error> {
CACHED_RE.with(|cell| {
let mut cache = cell.borrow_mut();
if let Some((ref cached_pat, ref re)) = *cache {
if cached_pat == pattern {
return Ok(re.clone());
}
}
let re = Regex::new(pattern)?;
*cache = Some((pattern.to_string(), re.clone()));
Ok(re)
})
}
static LINE_RANGE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(\d+)-(\d+)$").unwrap());
use crate::compact;
use crate::github;
pub struct ElementCache {
store: HashMap<(String, u64), HashMap<String, Value>>,
}
impl Default for ElementCache {
fn default() -> Self {
Self::new()
}
}
impl ElementCache {
pub fn new() -> Self {
Self {
store: HashMap::new(),
}
}
pub fn get(&self, repo: &str, number: u64, element_id: &str) -> Option<String> {
self.store
.get(&(repo.to_string(), number))
.and_then(|m| m.get(element_id))
.map(|v| serde_json::to_string(v).unwrap_or_default())
}
pub fn store_elements(&mut self, repo: &str, number: u64, elements_json: &str) {
if let Ok(val) = serde_json::from_str::<Value>(elements_json) {
if let Some(obj) = val.as_object() {
let mut map = HashMap::new();
for (k, v) in obj {
if !k.starts_with('_') {
map.insert(k.clone(), v.clone());
}
}
self.store.insert((repo.to_string(), number), map);
}
}
}
pub fn update_elements(&mut self, repo: &str, number: u64, elements_json: &str) {
if let Ok(val) = serde_json::from_str::<Value>(elements_json) {
if let Some(obj) = val.as_object() {
let entry = self.store.entry((repo.to_string(), number)).or_default();
for (k, v) in obj {
if !k.starts_with('_') {
entry.insert(k.clone(), v.clone());
}
}
}
}
}
pub fn available(&self, repo: &str, number: u64) -> Vec<String> {
match self.store.get(&(repo.to_string(), number)) {
Some(m) => {
let mut keys: Vec<String> = m.keys().cloned().collect();
keys.sort();
keys
}
None => Vec::new(),
}
}
pub fn retrieve(
&self,
repo: &str,
number: u64,
element_id: &str,
lines: Option<&str>,
grep: Option<&str>,
context: usize,
) -> String {
let elem_data = match self
.store
.get(&(repo.to_string(), number))
.and_then(|m| m.get(element_id))
{
Some(v) => v,
None => {
let available = self.available(repo, number);
let mut msg = format!(
"Element '{}' not found for {}#{}.",
element_id, repo, number
);
if !available.is_empty() {
msg.push_str(&format!("\nAvailable: {}", available.join(", ")));
} else {
msg.push_str("\nNo cached elements. Call fetch_issue first.");
}
return msg;
}
};
let content_val = elem_data.get("content");
let content_str: String;
let content_is_structured;
match content_val {
Some(Value::String(s)) => {
content_str = s.clone();
content_is_structured = false;
}
Some(val) => {
content_str = serde_json::to_string_pretty(val).unwrap_or_default();
content_is_structured = true;
}
None => {
content_str = String::new();
content_is_structured = false;
}
}
let content_lines: Vec<&str> = content_str.split('\n').collect();
if let Some(grep_pattern) = grep {
let regex = match get_or_compile_regex(grep_pattern) {
Ok(r) => r,
Err(e) => return format!("Invalid grep pattern: {}", e),
};
if content_is_structured {
if let Some(data) = content_val {
let matches = grep_json_value(data, ®ex, context, "");
let elem_type = elem_data
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let result = serde_json::json!({
"element_id": element_id,
"type": elem_type,
"grep": grep_pattern,
"matches": matches,
});
return serde_json::to_string_pretty(&result).unwrap_or_default();
}
}
let matches = grep_lines_internal(&content_lines, ®ex, context);
let mut result = serde_json::Map::new();
if let Some(obj) = elem_data.as_object() {
for (k, v) in obj {
if k != "content" {
result.insert(k.clone(), v.clone());
}
}
}
result.insert("grep".to_string(), Value::String(grep_pattern.to_string()));
result.insert("matches".to_string(), matches);
return serde_json::to_string_pretty(&Value::Object(result)).unwrap_or_default();
}
if let Some(lines_str) = lines {
let m = match LINE_RANGE_RE.captures(lines_str) {
Some(m) => m,
None => {
return format!(
"Invalid lines format: '{}'. Use 'start-end', e.g. '40-60'.",
lines_str
);
}
};
let start: usize = m[1].parse().unwrap_or(1);
let end: usize = m[2].parse().unwrap_or(usize::MAX);
if content_is_structured {
if let Some(Value::Array(arr)) = content_val {
let from = start.saturating_sub(1);
let to = end.min(arr.len());
let selected: Vec<Value> = arr[from..to].to_vec();
let mut result = serde_json::Map::new();
if let Some(obj) = elem_data.as_object() {
for (k, v) in obj {
if k != "content" {
result.insert(k.clone(), v.clone());
}
}
}
result.insert("content".to_string(), Value::Array(selected));
result.insert(
"items_shown".to_string(),
Value::String(format!("{}-{}", start, to)),
);
result.insert("total_items".to_string(), Value::from(arr.len()));
return serde_json::to_string_pretty(&Value::Object(result))
.unwrap_or_default();
}
}
let selected: Vec<&str> =
content_lines[start.saturating_sub(1)..end.min(content_lines.len())].to_vec();
let mut result = serde_json::Map::new();
if let Some(obj) = elem_data.as_object() {
for (k, v) in obj {
if k != "content" {
result.insert(k.clone(), v.clone());
}
}
}
result.insert("content".to_string(), Value::String(selected.join("\n")));
result.insert(
"lines_shown".to_string(),
Value::String(format!("{}-{}", start, end.min(content_lines.len()))),
);
return serde_json::to_string_pretty(&Value::Object(result)).unwrap_or_default();
}
let elem_type = elem_data.get("type").and_then(|v| v.as_str()).unwrap_or("");
if elem_type == "comment_segment" {
if let Some(Value::Array(arr)) = content_val {
let toc: Vec<Value> = arr
.iter()
.map(|c| {
let body = c.get("body").and_then(|v| v.as_str()).unwrap_or("");
let snippet: String = body
.chars()
.filter(|ch| !ch.is_control())
.take(80)
.collect();
serde_json::json!({
"_index": c.get("_index"),
"author": c.get("author"),
"created_at": c.get("created_at"),
"author_association": c.get("author_association"),
"snippet": snippet,
})
})
.collect();
let result = serde_json::json!({
"element_id": element_id,
"type": elem_type,
"total_comments": arr.len(),
"hint": "Use lines='1-20' to paginate, or grep='pattern' to search.",
"comments": toc,
});
return serde_json::to_string_pretty(&result).unwrap_or_default();
}
}
serde_json::to_string_pretty(elem_data).unwrap_or_default()
}
#[allow(clippy::too_many_arguments)]
pub fn fetch_issue(
&mut self,
repo: &str,
number: u64,
element_id: Option<&str>,
lines: Option<&str>,
grep: Option<&str>,
context: usize,
refresh: bool,
) -> String {
if let Some(eid) = element_id {
return self.retrieve(repo, number, eid, lines, grep, context);
}
if let Some(err) = crate::git_refs::validate_repo(repo) {
return err;
}
let key = (repo.to_string(), number);
if !refresh {
if let Some(elements) = self.store.get(&key) {
if !elements.is_empty() {
let mut ids: Vec<&String> = elements.keys().collect();
ids.sort();
return format!(
"Cached {}#{} — {} elements available: {}\n\
Use element_id='...' to drill down, or refresh=True to re-fetch.",
repo,
number,
ids.len(),
ids.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
);
}
}
}
let (text, cache_json) = match github::fetch_issue_internal(repo, number) {
Ok(r) => r,
Err(e) => return e,
};
if let Some(ref cj) = cache_json {
self.store_elements(repo, number, cj);
}
if text.len() > github::OVERFLOW_LIMIT {
let total_lines = text.matches('\n').count() + 1;
let overflow = serde_json::json!({
"overflow": {
"type": "overflow",
"total_chars": text.len(),
"total_lines": total_lines,
"content": text,
}
});
self.update_elements(
repo,
number,
&serde_json::to_string(&overflow).unwrap_or_default(),
);
let safe_end = compact::safe_byte_index(&text, github::OVERFLOW_PREVIEW);
let mut preview = text[..safe_end].to_string();
if let Some(last_nl) = preview.rfind('\n') {
if last_nl > 0 {
preview.truncate(last_nl);
}
}
preview.push_str(&format!(
"\n\n... [{} chars, {} lines — truncated]\n\
Use element_id='overflow' with lines='N-M' or grep='pattern' \
to explore the full result.",
text.len(),
total_lines
));
return preview;
}
text
}
}
fn grep_lines_internal(text_lines: &[&str], regex: &Regex, context: usize) -> Value {
let mut raw: Vec<(usize, usize, usize)> = Vec::new();
for (idx, line) in text_lines.iter().enumerate() {
if regex.is_match(line) {
let start = idx.saturating_sub(context);
let end = (idx + context + 1).min(text_lines.len());
raw.push((idx + 1, start, end));
}
}
struct Group {
lines: Vec<usize>,
start: usize,
end: usize,
}
let mut groups: Vec<Group> = Vec::new();
for (hit_line, start, end) in raw {
if let Some(last) = groups.last_mut() {
if start <= last.end {
last.lines.push(hit_line);
last.end = last.end.max(end);
continue;
}
}
groups.push(Group {
lines: vec![hit_line],
start,
end,
});
}
let result: Vec<Value> = groups
.into_iter()
.map(|g| {
let content = text_lines[g.start..g.end].join("\n");
serde_json::json!({
"lines": g.lines,
"context_start": g.start + 1,
"context_end": g.end,
"content": content,
})
})
.collect();
Value::Array(result)
}
fn grep_json_value(data: &Value, regex: &Regex, context: usize, path: &str) -> Vec<Value> {
match data {
Value::String(s) => {
let text = s.replace("\r\n", "\n");
let text_lines: Vec<&str> = text.split('\n').collect();
let matches = grep_lines_internal(&text_lines, regex, context);
if let Value::Array(arr) = matches {
arr.into_iter()
.map(|mut m| {
m["field"] = Value::String(path.to_string());
m
})
.collect()
} else {
Vec::new()
}
}
Value::Object(map) => {
let mut matches = Vec::new();
for (key, val) in map {
let child = if path.is_empty() {
key.clone()
} else {
format!("{}.{}", path, key)
};
matches.extend(grep_json_value(val, regex, context, &child));
}
matches
}
Value::Array(arr) => {
let mut matches = Vec::new();
for (i, item) in arr.iter().enumerate() {
let child = format!("{}[{}]", path, i);
let mut item_matches = grep_json_value(item, regex, context, &child);
if let Value::Object(obj) = item {
if obj.contains_key("author") && obj.contains_key("body") {
for m in &mut item_matches {
if let Some(author) = obj.get("author") {
m["author"] = author.clone();
}
if let Some(date) = obj.get("created_at") {
m["created_at"] = date.clone();
}
if let Some(assoc) = obj.get("author_association") {
m["author_association"] = assoc.clone();
}
if let Some(idx) = obj.get("_index") {
m["comment_index"] = idx.clone();
m["element_id"] =
Value::String(format!("comment_{}", idx.as_u64().unwrap_or(0)));
}
}
}
}
matches.extend(item_matches);
}
matches
}
_ => Vec::new(),
}
}