///|
/// One text file captured during a deterministic project scan.
pub(all) struct ProjectFile {
path : String
content : String
line_count : Int
char_count : Int
} derive(Debug, Eq)
///|
pub fn ProjectFile::new(path : String, content : String) -> ProjectFile {
let line_count = if content.length() == 0 {
0
} else {
content.split("\n").to_array().length()
}
{ path, content, line_count, char_count: content.length() }
}
///|
pub fn ProjectFile::line_of(self : ProjectFile, needle : String) -> Int? {
let mut line = 1
for text in self.content.split("\n") {
if text.to_owned().contains(needle) {
return Some(line)
}
line += 1
}
None
}
///|
pub fn ProjectFile::location_of(
self : ProjectFile,
needle : String,
) -> SourceLocation? {
match self.line_of(needle) {
Some(line) => Some(SourceLocation::new(self.path, line, 1))
None => None
}
}
///|
/// Complete, offline tree inventory for a project root.
pub(all) struct ProjectInventory {
root : String
files : Array[ProjectFile]
directories : Array[String]
ignored_paths : Array[String]
} derive(Debug, Eq)
///|
pub fn ProjectInventory::new(
root : String,
files : Array[ProjectFile],
directories : Array[String],
ignored_paths : Array[String],
) -> ProjectInventory {
{ root, files, directories, ignored_paths }
}
///|
pub fn ProjectInventory::has_file(
self : ProjectInventory,
path : String,
) -> Bool {
self.files.any(file => file.path == path)
}
///|
pub fn ProjectInventory::has_directory(
self : ProjectInventory,
path : String,
) -> Bool {
self.directories.contains(path)
}
///|
pub fn ProjectInventory::find_file(
self : ProjectInventory,
path : String,
) -> ProjectFile? {
for file in self.files {
if file.path == path {
return Some(file)
}
}
None
}
///|
pub fn ProjectInventory::files_with_suffix(
self : ProjectInventory,
suffix : String,
) -> Array[ProjectFile] {
let found : Array[ProjectFile] = []
for file in self.files {
if file.path.has_suffix(suffix) {
found.push(file)
}
}
found
}
///|
pub fn ProjectInventory::files_under(
self : ProjectInventory,
path : String,
) -> Array[ProjectFile] {
let prefix = if path.has_suffix("/") { path } else { path + "/" }
let found : Array[ProjectFile] = []
for file in self.files {
if file.path.has_prefix(prefix) {
found.push(file)
}
}
found
}
///|
pub fn ProjectInventory::total_lines(self : ProjectInventory) -> Int {
let mut total = 0
for file in self.files {
total += file.line_count
}
total
}
///|
pub fn ProjectInventory::to_snapshot(
self : ProjectInventory,
) -> ProjectSnapshot {
let files : Map[String, String] = {}
for file in self.files {
files[file.path] = file.content
}
ProjectSnapshot::new(self.root, files, self.directories)
}
///|
fn inventory_join(root : String, relative : String) -> String {
if root.has_suffix("/") || root.has_suffix("\\") {
root + relative
} else if relative.length() == 0 {
root
} else {
root + "/" + relative
}
}
///|
fn child_relative(parent : String, child : String) -> String {
if parent.length() == 0 {
child
} else {
parent + "/" + child
}
}
///|
fn should_ignore(config : DoctorConfig, path : String) -> Bool {
config.ignores_path(path) || path == ".git" || path.has_prefix(".git/")
}
///|
fn scan_directory(
root : String,
relative : String,
config : DoctorConfig,
files : Array[ProjectFile],
directories : Array[String],
ignored_paths : Array[String],
) -> Unit raise {
let current_path = inventory_join(root, relative)
for entry in @fs.read_dir(current_path) {
let child_relative_path = child_relative(relative, entry)
let child_path = inventory_join(root, child_relative_path)
if should_ignore(config, child_relative_path) {
ignored_paths.push(child_relative_path)
} else if @fs.is_dir(child_path) {
directories.push(child_relative_path)
scan_directory(
root, child_relative_path, config, files, directories, ignored_paths,
)
} else if @fs.is_file(child_path) {
files.push(
ProjectFile::new(
child_relative_path,
@fs.read_file_to_string(child_path),
),
)
}
}
}
///|
/// Recursively scan a project without touching ignored or generated directories.
pub fn scan_project(
root : String,
config : DoctorConfig,
) -> ProjectInventory raise {
if !@fs.path_exists(root) || !@fs.is_dir(root) {
fail("project path is not a directory: " + root)
}
let files : Array[ProjectFile] = []
let directories : Array[String] = []
let ignored_paths : Array[String] = []
scan_directory(root, "", config, files, directories, ignored_paths)
ProjectInventory::new(root, files, directories, ignored_paths)
}
///|
pub fn scan_project_default(root : String) -> ProjectInventory raise {
scan_project(root, DoctorConfig::default())
}