use crate::output::SpanContext;
use regex::Regex;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::path::Path;
pub(crate) const MAX_REGEX_SIZE: usize = 10_000;
pub fn infer_language(file_path: &str) -> Option<&'static str> {
match Path::new(file_path).extension().and_then(|s| s.to_str()) {
Some("rs") => Some("Rust"),
Some("py") => Some("Python"),
Some("js") => Some("JavaScript"),
Some("ts") => Some("TypeScript"),
Some("jsx") => Some("JavaScript"),
Some("tsx") => Some("TypeScript"),
Some("c") => Some("C"),
Some("cpp") | Some("cc") | Some("cxx") | Some("hpp") | Some("hxx") => Some("C++"),
Some("h") => Some("C"), Some("java") => Some("Java"),
Some("go") => Some("Go"),
Some("rb") => Some("Ruby"),
Some("php") => Some("PHP"),
Some("swift") => Some("Swift"),
Some("kt") | Some("kts") => Some("Kotlin"),
Some("scala") => Some("Scala"),
Some("sh") | Some("bash") => Some("Shell"),
Some("lua") => Some("Lua"),
Some("r") => Some("R"),
Some("m") => Some("Matlab"), Some("cs") => Some("C#"),
_ => None,
}
}
pub(crate) fn normalize_kind_label(kind: &str) -> String {
kind.to_lowercase()
}
pub(crate) fn like_pattern(query: &str) -> String {
let escaped = query
.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_");
format!("%{}%", escaped)
}
pub(crate) fn like_prefix(path: &std::path::Path) -> String {
let raw = path.to_string_lossy().to_string();
let escaped = raw
.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_");
format!("{}%", escaped)
}
pub(crate) fn referenced_symbol_from_name(name: &str) -> String {
name.strip_prefix("ref to ").unwrap_or(name).to_string()
}
pub(crate) struct FileCache {
pub(crate) bytes: Vec<u8>,
pub(crate) lines: Vec<String>,
}
pub(crate) fn load_file<'a>(
path: &str,
cache: &'a mut HashMap<String, FileCache>,
) -> Option<&'a FileCache> {
if !cache.contains_key(path) {
let bytes = match std::fs::read(path) {
Ok(bytes) => bytes,
Err(e) => {
eprintln!("Warning: Failed to read file '{}': {}", path, e);
return None;
}
};
let text = String::from_utf8_lossy(&bytes);
let lines = text.split('\n').map(|line| line.to_string()).collect();
cache.insert(path.to_string(), FileCache { bytes, lines });
}
cache.get(path)
}
pub(crate) fn snippet_from_file(
file_path: &str,
byte_start: u64,
byte_end: u64,
max_bytes: usize,
cache: &mut HashMap<String, FileCache>,
) -> (Option<String>, Option<bool>) {
if max_bytes == 0 {
return (None, None);
}
let file = match load_file(file_path, cache) {
Some(file) => file,
None => return (None, None),
};
let start = byte_start as usize;
let end = byte_end as usize;
if start >= file.bytes.len() || end > file.bytes.len() || start >= end {
return (None, None);
}
let capped_end = end.min(start + max_bytes);
let truncated = capped_end < end;
let snippet =
match crate::safe_extraction::extract_symbol_content_safe(&file.bytes, start, capped_end) {
Some(s) => s,
None => {
String::from_utf8_lossy(&file.bytes[start..capped_end]).to_string()
}
};
(Some(snippet), Some(truncated))
}
pub(crate) fn span_context_from_file(
file_path: &str,
start_line: u64,
end_line: u64,
context_lines: usize,
capped: bool,
cache: &mut HashMap<String, FileCache>,
) -> Option<SpanContext> {
let file = load_file(file_path, cache)?;
let line_count = file.lines.len() as u64;
if line_count == 0 {
return None;
}
let start_line = start_line.max(1).min(line_count);
let end_line = end_line.max(start_line).min(line_count);
let before_start = start_line.saturating_sub(context_lines as u64).max(1);
let after_end = (end_line + context_lines as u64).min(line_count);
let before = file.lines[(before_start - 1) as usize..(start_line - 1) as usize].to_vec();
let selected = file.lines[(start_line - 1) as usize..end_line as usize].to_vec();
let after = file.lines[end_line as usize..after_end as usize].to_vec();
let truncated = capped
|| (context_lines > 0 && (before.len() < context_lines || after.len() < context_lines));
Some(SpanContext {
before,
selected,
after,
truncated,
})
}
pub(crate) fn score_match(
query: &str,
name: &str,
display_fqn: &str,
fqn: &str,
regex: Option<&Regex>,
) -> u64 {
let mut score = 0;
if name == query {
score = score.max(100);
}
if display_fqn == query {
score = score.max(95);
}
if fqn == query {
score = score.max(90);
}
if name.starts_with(query) {
score = score.max(80);
}
if display_fqn.starts_with(query) {
score = score.max(70);
}
if name.contains(query) {
score = score.max(60);
}
if display_fqn.contains(query) {
score = score.max(50);
}
if fqn.contains(query) {
score = score.max(40);
}
if let Some(pattern) = regex {
if pattern.is_match(name) {
score = score.max(70);
} else if pattern.is_match(display_fqn) {
score = score.max(60);
} else if pattern.is_match(fqn) {
score = score.max(50);
}
}
score
}
pub(crate) fn span_id(file_path: &str, byte_start: u64, byte_end: u64) -> String {
let mut hasher = Sha256::new();
hasher.update(file_path.as_bytes());
hasher.update(b":");
hasher.update(byte_start.to_string().as_bytes());
hasher.update(b":");
hasher.update(byte_end.to_string().as_bytes());
let digest = hasher.finalize();
hex::encode(&digest[..8])
}
pub(crate) fn json_extract<T: serde::de::DeserializeOwned>(json: &str, key: &str) -> Option<T> {
serde_json::from_str::<serde_json::Value>(json)
.ok()?
.get(key)
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
pub(crate) fn match_id(file_path: &str, byte_start: u64, byte_end: u64, name: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(name.as_bytes());
hasher.update(b":");
hasher.update(file_path.as_bytes());
hasher.update(b":");
hasher.update(byte_start.to_string().as_bytes());
hasher.update(b":");
hasher.update(byte_end.to_string().as_bytes());
let digest = hasher.finalize();
hex::encode(&digest[..8])
}
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
pub(crate) struct SymbolNodeData {
#[serde(default)]
pub(crate) symbol_id: Option<String>,
#[serde(default)]
pub(crate) name: Option<String>,
#[serde(default)]
pub(crate) kind: String,
#[serde(default)]
pub(crate) kind_normalized: Option<String>,
#[serde(default)]
pub(crate) fqn: Option<String>,
#[serde(default)]
pub(crate) canonical_fqn: Option<String>,
#[serde(default)]
pub(crate) display_fqn: Option<String>,
pub(crate) byte_start: u64,
pub(crate) byte_end: u64,
pub(crate) start_line: u64,
pub(crate) start_col: u64,
pub(crate) end_line: u64,
pub(crate) end_col: u64,
}
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
pub(crate) struct ReferenceNodeData {
pub(crate) file: String,
pub(crate) byte_start: u64,
pub(crate) byte_end: u64,
pub(crate) start_line: u64,
pub(crate) start_col: u64,
pub(crate) end_line: u64,
pub(crate) end_col: u64,
}
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
pub(crate) struct CallNodeData {
pub(crate) file: String,
pub(crate) caller: String,
pub(crate) callee: String,
#[serde(default)]
pub(crate) caller_symbol_id: Option<String>,
#[serde(default)]
pub(crate) callee_symbol_id: Option<String>,
pub(crate) byte_start: u64,
pub(crate) byte_end: u64,
pub(crate) start_line: u64,
pub(crate) start_col: u64,
pub(crate) end_line: u64,
pub(crate) end_col: u64,
}