use std::fmt;
use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct FilePath(String);
impl FilePath {
#[must_use]
pub fn new(path: impl AsRef<Path>) -> Self {
let raw = path.as_ref().to_string_lossy().replace('\\', "/");
let trimmed = raw.strip_prefix("./").unwrap_or(&raw);
Self(trimmed.to_owned())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for FilePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Position {
pub line: u32,
pub column: u32,
}
impl Position {
pub const START: Self = Self { line: 1, column: 1 };
#[must_use]
pub const fn new(line: u32, column: u32) -> Self {
Self { line, column }
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Location {
pub file: FilePath,
pub position: Position,
}
impl Location {
#[must_use]
pub fn new(file: FilePath, position: Position) -> Self {
Self { file, position }
}
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.file, self.position)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keeps_a_posix_path_unchanged() {
assert_eq!(
FilePath::new("src/components/Button.tsx").as_str(),
"src/components/Button.tsx"
);
}
#[test]
fn converts_windows_separators() {
assert_eq!(
FilePath::new(r"src\components\Button.tsx").as_str(),
"src/components/Button.tsx"
);
assert_eq!(FilePath::new(r"src\a/b\c.ts").as_str(), "src/a/b/c.ts");
}
#[test]
fn drops_a_leading_dot_slash() {
assert_eq!(FilePath::new("./src/a.ts").as_str(), "src/a.ts");
assert_eq!(FilePath::new(r".\src\a.ts").as_str(), "src/a.ts");
}
#[test]
fn does_not_mangle_a_dotfile() {
assert_eq!(FilePath::new(".eslintrc.ts").as_str(), ".eslintrc.ts");
assert_eq!(
FilePath::new("src/.hidden/a.ts").as_str(),
"src/.hidden/a.ts"
);
}
#[test]
fn paths_sort_deterministically() {
let mut paths: Vec<FilePath> = ["src/b.ts", "src/a.ts", "lib/z.ts", "src/a/b.ts"]
.iter()
.map(FilePath::new)
.collect();
paths.sort();
let rendered: Vec<&str> = paths.iter().map(FilePath::as_str).collect();
assert_eq!(rendered, ["lib/z.ts", "src/a.ts", "src/a/b.ts", "src/b.ts"]);
}
#[test]
fn positions_order_by_line_then_column() {
let mut positions = vec![
Position::new(2, 1),
Position::new(1, 10),
Position::new(1, 2),
Position::new(10, 1),
];
positions.sort();
assert_eq!(
positions,
[
Position::new(1, 2),
Position::new(1, 10),
Position::new(2, 1),
Position::new(10, 1)
]
);
}
#[test]
fn renders_the_way_editors_expect() {
let location = Location::new(FilePath::new("src/a.ts"), Position::new(12, 5));
assert_eq!(location.to_string(), "src/a.ts:12:5");
}
#[test]
fn start_is_one_based() {
assert_eq!(Position::START, Position::new(1, 1));
assert_eq!(Position::START.to_string(), "1:1");
}
#[test]
fn file_path_serializes_as_a_bare_string() {
let path = FilePath::new("src/a.ts");
assert_eq!(serde_json::to_string(&path).expect("ok"), "\"src/a.ts\"");
}
}