source-map-cache 0.1.0

Source map cache
Documentation
/*
 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/swamp
 * Licensed under the MIT License. See LICENSE in the project root for license information.
 */
use seq_map::SeqMap;
use source_map_cache::SourceMap;
use source_map_node::Span;
use std::path::Path;
use std::path::PathBuf;

// Helper function to create a source map for testing without path verification
fn create_test_source_map() -> SourceMap {
    SourceMap {
        mounts: SeqMap::new(),
        cache: SeqMap::new(),
        file_cache: SeqMap::new(),
        next_file_id: 1,
    }
}

#[test]
fn test_basic_line_offsets() {
    let mut source_map = create_test_source_map();

    let file_content = "line 1\nline 2\nline 3\n";
    let file_id = 1;
    source_map.add_manual(file_id, "test", &PathBuf::from("test.txt"), file_content);

    assert_eq!(source_map.get_source_line(file_id, 1), Some("line 1"));
    assert_eq!(source_map.get_source_line(file_id, 2), Some("line 2"));
    assert_eq!(source_map.get_source_line(file_id, 3), Some("line 3"));
    assert_eq!(source_map.get_source_line(file_id, 4), None);

    assert_eq!(source_map.get_span_location_utf8(file_id, 0), Some((1, 1))); // Start of file
    assert_eq!(source_map.get_span_location_utf8(file_id, 7), Some((2, 1))); // Start of line 2
}

#[test]
fn test_file_without_trailing_newline() {
    let mut source_map = create_test_source_map();

    let file_content = "first line\nsecond line";
    let file_id = 1;
    source_map.add_manual(
        file_id,
        "test",
        &PathBuf::from("no_newline.txt"),
        file_content,
    );

    assert_eq!(source_map.get_source_line(file_id, 1), Some("first line"));
    assert_eq!(source_map.get_source_line(file_id, 2), Some("second line"));
    assert_eq!(source_map.get_source_line(file_id, 3), None);

    let span = Span {
        file_id,
        offset: 11, // Start of "second line"
        length: 11, // Length of "second line"
    };

    assert_eq!(source_map.get_text_span(&span), Some("second line"));
}

#[test]
fn test_empty_file_and_edge_cases() {
    let mut source_map = create_test_source_map();

    let empty_id = 1;
    source_map.add_manual(empty_id, "test", &PathBuf::from("empty.txt"), "");

    let single_id = 2;
    source_map.add_manual(
        single_id,
        "test",
        &PathBuf::from("single.txt"),
        "just one line",
    );

    let newlines_id = 3;
    source_map.add_manual(
        newlines_id,
        "test",
        &PathBuf::from("newlines.txt"),
        "\n\n\n",
    );

    // Test empty file
    assert_eq!(source_map.get_source_line(empty_id, 1), None);

    // Test single line file
    assert_eq!(
        source_map.get_source_line(single_id, 1),
        Some("just one line")
    );
    assert_eq!(source_map.get_source_line(single_id, 2), None);

    // Test file with only newlines
    assert_eq!(source_map.get_source_line(newlines_id, 1), Some(""));
    assert_eq!(source_map.get_source_line(newlines_id, 2), Some(""));
    assert_eq!(source_map.get_source_line(newlines_id, 3), Some(""));
    assert_eq!(source_map.get_source_line(newlines_id, 4), None);
}

#[test]
fn test_set_method_updates_both_caches() {
    let mut source_map = create_test_source_map();

    let mount_name = "test";
    let relative_path = Path::new("test_file.txt");
    let initial_contents = "initial content";
    let updated_contents = "updated content";

    // Test 1: Add new file with set method
    let file_id1 = source_map.set(mount_name, relative_path, initial_contents);

    // Verify file is in both caches
    assert!(source_map.cache.get(&file_id1).is_some());
    assert_eq!(
        source_map.cache.get(&file_id1).unwrap().contents,
        initial_contents
    );

    let cache_key = (
        mount_name.to_string(),
        relative_path.to_str().unwrap().to_string(),
    );
    assert!(source_map.file_cache.get(&cache_key).is_some());
    assert_eq!(*source_map.file_cache.get(&cache_key).unwrap(), file_id1);

    // Test 2: Update existing file with set method
    let file_id2 = source_map.set(mount_name, relative_path, updated_contents);

    // Should be the same file ID
    assert_eq!(file_id1, file_id2);

    // Verify contents are updated in cache
    assert_eq!(
        source_map.cache.get(&file_id2).unwrap().contents,
        updated_contents
    );

    // Verify file_cache still points to the same file ID
    assert_eq!(*source_map.file_cache.get(&cache_key).unwrap(), file_id2);

    // Test 3: Add different file to ensure file_cache works correctly
    let different_path = Path::new("different_file.txt");
    let different_contents = "different content";
    let file_id3 = source_map.set(mount_name, different_path, different_contents);

    // Should be a different file ID
    assert_ne!(file_id1, file_id3);

    // Verify both files exist in caches
    assert!(source_map.cache.get(&file_id3).is_some());
    let different_cache_key = (
        mount_name.to_string(),
        different_path.to_str().unwrap().to_string(),
    );
    assert_eq!(
        *source_map.file_cache.get(&different_cache_key).unwrap(),
        file_id3
    );

    // Original file should still be there
    assert_eq!(*source_map.file_cache.get(&cache_key).unwrap(), file_id1);
}

#[test]
fn test_multibyte_utf8_char_boundaries() {
    let mut source_map = create_test_source_map();

    let file_content = "Hello\u{a0}World\nSecond line with emoji 😀\n";
    let file_id = 1;
    source_map.add_manual(file_id, "test", &PathBuf::from("utf8.txt"), file_content);

    assert_eq!(source_map.get_span_location_utf8(file_id, 0), Some((1, 1))); // 'H'
    assert_eq!(source_map.get_span_location_utf8(file_id, 5), Some((1, 6))); // Start of \u{a0}
    assert_eq!(source_map.get_span_location_utf8(file_id, 6), None); // Middle of \u{a0} - invalid!
    assert_eq!(source_map.get_span_location_utf8(file_id, 7), Some((1, 7))); // 'W'

    let span1 = Span {
        file_id,
        offset: 0,
        length: 7,
    };

    let text = source_map.get_text_span(&span1).unwrap();
    assert_eq!(text, "Hello\u{a0}");

    let span2 = Span {
        file_id,
        offset: 6, // Middle of \u{a0} (bytes 5-6)
        length: 5,
    };

    assert_eq!(source_map.get_text_span(&span2), None); // Invalid offset!
}