use std::borrow::Cow;
use std::hash::DefaultHasher;
use std::hash::Hash;
use std::hash::Hasher;
use std::path::Path;
use std::path::PathBuf;
use serde::Deserialize;
use serde::Serialize;
use crate::error::DatabaseError;
use crate::utils::read_file;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[repr(transparent)]
pub struct FileId(u64);
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[repr(u8)]
pub enum FileType {
Host,
Vendored,
Builtin,
}
#[derive(Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct File {
pub id: FileId,
pub name: Cow<'static, str>,
pub path: Option<PathBuf>,
pub file_type: FileType,
pub contents: Cow<'static, str>,
pub size: u32,
pub lines: Vec<u32>,
}
pub trait HasFileId {
fn file_id(&self) -> FileId;
}
impl File {
#[must_use]
pub fn new(
name: Cow<'static, str>,
file_type: FileType,
path: Option<PathBuf>,
contents: Cow<'static, str>,
) -> Self {
let id = FileId::new(&name);
let size = contents.len() as u32;
let lines = line_starts(contents.as_ref());
Self { id, name, path, file_type, contents, size, lines }
}
#[inline(always)]
pub fn read(workspace: &Path, path: &Path, file_type: FileType) -> Result<Self, DatabaseError> {
read_file(workspace, path, file_type)
}
#[must_use]
pub fn ephemeral(name: Cow<'static, str>, contents: Cow<'static, str>) -> Self {
Self::new(name, FileType::Host, None, contents)
}
#[inline]
#[must_use]
pub fn line_number(&self, offset: u32) -> u32 {
self.lines.binary_search(&offset).unwrap_or_else(|next_line| next_line - 1) as u32
}
#[must_use]
pub fn get_line_start_offset(&self, line: u32) -> Option<u32> {
self.lines.get(line as usize).copied()
}
#[must_use]
pub fn get_line_end_offset(&self, line: u32) -> Option<u32> {
match self.lines.get(line as usize + 1) {
Some(&end) => Some(end - 1),
None if line as usize == self.lines.len() - 1 => Some(self.size),
_ => None,
}
}
#[inline]
#[must_use]
pub fn column_number(&self, offset: u32) -> u32 {
let line_start =
self.lines.binary_search(&offset).unwrap_or_else(|next_line| self.lines[next_line - 1] as usize);
offset - line_start as u32
}
}
impl FileType {
#[must_use]
pub const fn is_host(self) -> bool {
matches!(self, FileType::Host)
}
#[must_use]
pub const fn is_vendored(self) -> bool {
matches!(self, FileType::Vendored)
}
#[must_use]
pub const fn is_builtin(self) -> bool {
matches!(self, FileType::Builtin)
}
}
impl FileId {
#[must_use]
pub fn new(logical_name: &str) -> Self {
let mut hasher = DefaultHasher::new();
logical_name.hash(&mut hasher);
Self(hasher.finish())
}
#[must_use]
pub const fn zero() -> Self {
Self(0)
}
#[must_use]
pub const fn is_zero(self) -> bool {
self.0 == 0
}
#[must_use]
pub fn as_u64(self) -> u64 {
self.0
}
}
impl HasFileId for File {
fn file_id(&self) -> FileId {
self.id
}
}
impl std::fmt::Display for FileId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[inline]
pub(crate) fn line_starts(source: &str) -> Vec<u32> {
const LINE_WIDTH_HEURISTIC: usize = 20;
let bytes = source.as_bytes();
let mut lines = Vec::with_capacity(bytes.len() / LINE_WIDTH_HEURISTIC);
lines.push(0);
match memchr::memchr2(b'\r', b'\n', bytes) {
None => {}
Some(cr) if bytes[cr] == b'\r' && bytes.get(cr + 1) != Some(&b'\n') => {
for pos in memchr::memchr_iter(b'\r', bytes) {
lines.push((pos + 1) as u32);
}
}
_ => {
for pos in memchr::memchr_iter(b'\n', bytes) {
lines.push((pos + 1) as u32);
}
}
}
lines
}