aboutsummaryrefslogblamecommitdiffstats
path: root/src/util/location.rs
blob: 0c9c426033a6baa71cb198d8deb86c5cf45431a0 (plain) (tree)














































































































                                                                                                    
//! Deal with positions in a file.
//!
//! * Convert between byte indices and unist points.
//! * Convert between byte indices into a string which is built up of several
//!   slices in a whole document, and byte indices into that whole document.

use crate::unist::Point;
use alloc::{vec, vec::Vec};

/// Each stop represents a new slice, which contains the byte index into the
/// corresponding string where the slice starts (`0`), and the byte index into
/// the whole document where that slice starts (`1`).
pub type Stop = (usize, usize);

#[derive(Debug)]
pub struct Location {
    /// List, where each index is a line number (0-based), and each value is
    /// the byte index *after* where the line ends.
    indices: Vec<usize>,
}

impl Location {
    /// Get an index for the given `bytes`.
    ///
    /// Port of <https://github.com/vfile/vfile-location/blob/main/index.js>
    #[must_use]
    pub fn new(bytes: &[u8]) -> Self {
        let mut index = 0;
        let mut location_index = Self { indices: vec![] };

        while index < bytes.len() {
            if bytes[index] == b'\r' {
                if index + 1 < bytes.len() && bytes[index + 1] == b'\n' {
                    location_index.indices.push(index + 2);
                } else {
                    location_index.indices.push(index + 1);
                }
            } else if bytes[index] == b'\n' {
                location_index.indices.push(index + 1);
            }

            index += 1;
        }

        location_index.indices.push(index + 1);
        location_index
    }

    /// Get the line and column-based `point` for `offset` in the bound indices.
    ///
    /// Returns `None` when given out of bounds input.
    ///
    /// Port of <https://github.com/vfile/vfile-location/blob/main/index.js>
    #[must_use]
    pub fn to_point(&self, offset: usize) -> Option<Point> {
        let mut index = 0;

        if let Some(end) = self.indices.last() {
            if offset < *end {
                while index < self.indices.len() {
                    if self.indices[index] > offset {
                        break;
                    }

                    index += 1;
                }

                let previous = if index > 0 {
                    self.indices[index - 1]
                } else {
                    0
                };
                return Some(Point {
                    line: index + 1,
                    column: offset + 1 - previous,
                    offset,
                });
            }
        }

        None
    }

    /// Like `to_point`, but takes a relative offset from a certain string
    /// instead of an absolute offset into the whole document.
    ///
    /// The relative offset is made absolute based on `stops`, which represent
    /// where that certain string is in the whole document.
    #[must_use]
    pub fn relative_to_point(&self, stops: &[Stop], relative: usize) -> Option<Point> {
        Location::relative_to_absolute(stops, relative).and_then(|absolute| self.to_point(absolute))
    }

    /// Turn a relative offset into an absolute offset.
    #[must_use]
    pub fn relative_to_absolute(stops: &[Stop], relative: usize) -> Option<usize> {
        let mut index = 0;

        while index < stops.len() && stops[index].0 <= relative {
            index += 1;
        }

        // There are no points: that only occurs if there was an empty string.
        if index == 0 {
            None
        } else {
            let (stop_relative, stop_absolute) = &stops[index - 1];
            Some(stop_absolute + (relative - stop_relative))
        }
    }
}