sifs 0.1.1

SIFS Is Fast Search: instant local code search for agents
Documentation
use crate::types::{Chunk, SearchResult};

const GIT_URL_SCHEMES: &[&str] = &[
    "https://",
    "http://",
    "ssh://",
    "git://",
    "git+ssh://",
    "file://",
];

pub fn is_git_url(path: &str) -> bool {
    if GIT_URL_SCHEMES
        .iter()
        .any(|scheme| path.starts_with(scheme))
    {
        return true;
    }
    let Some((left, right)) = path.split_once(':') else {
        return false;
    };
    !right.starts_with('/')
        && left.contains('@')
        && left
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || ".-@_".contains(c))
}

pub fn resolve_chunk(chunks: &[Chunk], file_path: &str, line: usize) -> Option<Chunk> {
    let mut fallback = None;
    for chunk in chunks {
        if chunk.file_path == file_path && chunk.start_line <= line && line <= chunk.end_line {
            if line < chunk.end_line {
                return Some(chunk.clone());
            }
            if fallback.is_none() {
                fallback = Some(chunk.clone());
            }
        }
    }
    fallback
}

pub fn format_results(header: &str, results: &[SearchResult]) -> String {
    let mut lines = vec![header.to_owned(), String::new()];
    for (i, result) in results.iter().enumerate() {
        lines.push(format!(
            "## {}. {}  [score={:.3}, source={}]",
            i + 1,
            result.chunk.location(),
            result.score,
            result.source
        ));
        lines.push("```".to_owned());
        lines.push(result.chunk.content.trim().to_owned());
        lines.push("```".to_owned());
        lines.push(String::new());
    }
    lines.join("\n")
}

#[cfg(test)]
mod tests {
    use super::{format_results, is_git_url, resolve_chunk};
    use crate::types::{Chunk, SearchMode, SearchResult};

    fn chunk(file_path: &str, start_line: usize, end_line: usize) -> Chunk {
        Chunk {
            content: "fn example() {}".to_owned(),
            file_path: file_path.to_owned(),
            start_line,
            end_line,
            language: Some("rust".to_owned()),
        }
    }

    #[test]
    fn resolves_line_at_chunk_boundaries() {
        let chunks = vec![chunk("src/lib.rs", 1, 10), chunk("src/lib.rs", 10, 20)];

        assert_eq!(
            resolve_chunk(&chunks, "src/lib.rs", 10).unwrap().start_line,
            10
        );
        assert!(resolve_chunk(&chunks, "src/lib.rs", 21).is_none());
    }

    #[test]
    fn git_url_detection_covers_scheme_and_scp_forms() {
        assert!(is_git_url("https://github.com/org/repo"));
        assert!(is_git_url("git@github.com:org/repo.git"));
        assert!(!is_git_url("/tmp/local:repo"));
    }

    #[test]
    fn formatted_results_show_source_mode() {
        let result = SearchResult {
            chunk: chunk("src/lib.rs", 1, 1),
            score: 0.5,
            source: SearchMode::Bm25,
        };

        let output = format_results("Header", &[result]);

        assert!(output.contains("[score=0.500, source=bm25]"));
    }
}