mirror of
https://github.com/eza-community/eza.git
synced 2026-10-12 02:13:29 +00:00
feat: add lines-of-code counting and summary
Introduces a built-in lines-of-code (LOC) counter to `eza` that supports over 50 programming languages. - Adds the `--code` flag to provide a project-wide summary of code, comments, and blanks by language. - Adds the `--loc` flag to include lines-of-code and language columns in the long view. - Provides support for multiple display modes (`lines`, `percent`, `both`) for both flags. - Includes comment-aware counting, respecting language-specific comment syntax while ignoring string literals. - Updates completions for fish, nushell, powershell, and zsh to include the new flags.
This commit is contained in:
committed by
Christina Sørensen
parent
247fb8ed07
commit
4a7bfa58fc
@@ -59,6 +59,7 @@ complete -c eza -s a -l all -d "Show hidden and 'dot' files. Use this twice to a
|
||||
complete -c eza -s A -l almost-all -d "Equivalent to --all; included for compatibility with `ls -A`"
|
||||
complete -c eza -s d -l treat-dirs-as-files -d "List directories like regular files"
|
||||
complete -c eza -s L -l level -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9"
|
||||
complete -c eza -l code -d "Summarise lines of code by language" -x -a "lines percent both"
|
||||
complete -c eza -s w -l width -d "Limits column output of grid, 0 implies auto-width"
|
||||
complete -c eza -s r -l reverse -d "Reverse the sort order"
|
||||
complete -c eza -s s -l sort -d "Which field to sort by" -x -a "
|
||||
@@ -98,6 +99,7 @@ complete -c eza -s g -l group -d "List each file's group"
|
||||
complete -c eza -s h -l header -d "Add a header row to each column"
|
||||
complete -c eza -s H -l links -d "List each file's number of hard links"
|
||||
complete -c eza -s i -l inode -d "List each file's inode number"
|
||||
complete -c eza -l loc -d "Add lines-of-code and language columns" -x -a "lines percent both"
|
||||
complete -c eza -s S -l blocksize -d "List each file's size of allocated file system blocks"
|
||||
complete -c eza -s t -l time -d "Which timestamp field to list" -x -a "
|
||||
modified\t'Display modified time'
|
||||
|
||||
@@ -40,6 +40,8 @@ export extern "eza" [
|
||||
--header(-h) # Add a header row to each column
|
||||
--links(-H) # List each file's number of hard links
|
||||
--inode(-i) # List each file's inode number
|
||||
--loc: string # Add lines-of-code and language columns (lines, percent, both)
|
||||
--code: string # Summarise lines of code by language (lines, percent, both)
|
||||
--blocksize(-S) # List each file's size of allocated file system blocks
|
||||
--time(-t) -d # Which timestamp field to list
|
||||
--modified(-m) # Use the modified timestamp field
|
||||
|
||||
@@ -11,6 +11,7 @@ Register-ArgumentCompleter -Native -CommandName 'eza' -ScriptBlock {
|
||||
$ArrayAbsolute = @('on', 'follow', 'off')
|
||||
$ArrayTime = @('modified', 'accessed', 'created')
|
||||
$ArrayTimeStyle = @('default', 'iso', 'long-iso', 'full-iso', 'relative', '+%Y-%m-%d %H:%M', '+%Y.%m.%d %H:$M:$s')
|
||||
$ArrayCodeMode = @('lines', 'percent', 'both')
|
||||
|
||||
$commandElements = $commandAst.CommandElements
|
||||
$command = @(
|
||||
@@ -63,7 +64,17 @@ Register-ArgumentCompleter -Native -CommandName 'eza' -ScriptBlock {
|
||||
break
|
||||
}
|
||||
'*;--classify' {
|
||||
$ArrayWhen |
|
||||
$ArrayWhen |
|
||||
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)}
|
||||
break
|
||||
}
|
||||
'*;--loc' {
|
||||
$ArrayCodeMode |
|
||||
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)}
|
||||
break
|
||||
}
|
||||
'*;--code' {
|
||||
$ArrayCodeMode |
|
||||
ForEach-Object {[System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)}
|
||||
break
|
||||
}
|
||||
@@ -100,7 +111,9 @@ Register-ArgumentCompleter -Native -CommandName 'eza' -ScriptBlock {
|
||||
# [CompletionResult]::new('-H' ,'links' , [CompletionResultType]::ParameterName, 'list each file''s number of hard links')
|
||||
[CompletionResult]::new('--links' ,'links' , [CompletionResultType]::ParameterName, 'list each file''s number of hard links')
|
||||
# [CompletionResult]::new('-i' ,'inode' , [CompletionResultType]::ParameterName, 'list each file''s inode number')
|
||||
[CompletionResult]::new('--inode' ,'inode' , [CompletionResultType]::ParameterName, 'list each file''s inode number')
|
||||
[CompletionResult]::new('--inode' ,'inode' , [CompletionResultType]::ParameterName, 'list each file''s inode number')
|
||||
[CompletionResult]::new('--loc' ,'loc' , [CompletionResultType]::ParameterName, 'add lines-of-code and language columns')
|
||||
[CompletionResult]::new('--code' ,'code' , [CompletionResultType]::ParameterName, 'summarise lines of code by language')
|
||||
# [CompletionResult]::new('-M' ,'mounts' , [CompletionResultType]::ParameterName, 'show mount details (Linux and Mac only)')
|
||||
# [CompletionResult]::new('--mounts' ,'mounts' , [CompletionResultType]::ParameterName, 'show mount details (Linux and Mac only)')
|
||||
# [CompletionResult]::new('-n' ,'numeric' , [CompletionResultType]::ParameterName, 'list numeric user and group IDs')
|
||||
|
||||
@@ -50,6 +50,8 @@ __eza() {
|
||||
{-h,--header}"[Add a header row to each column]" \
|
||||
{-H,--links}"[List each file's number of hard links]" \
|
||||
{-i,--inode}"[List each file's inode number]" \
|
||||
--loc="[Add lines-of-code and language columns]:(mode):(lines percent both)" \
|
||||
--code="[Summarise lines of code by language]:(mode):(lines percent both)" \
|
||||
{-m,--modified}"[Use the modified timestamp field]" \
|
||||
{-n,--numeric}"[List numeric user and group IDs.]" \
|
||||
{-S,--blocksize}"[List each file's size of allocated file system blocks.]" \
|
||||
|
||||
@@ -83,6 +83,11 @@ When used without a value, defaults to ‘`automatic`’.
|
||||
`-T`, `--tree`
|
||||
: Recurse into directories as a tree.
|
||||
|
||||
`--code[=MODE]`
|
||||
: Print a lines-of-code summary by language instead of listing files, in the spirit of tools like `tokei` and `cloc`.
|
||||
|
||||
: The given paths (or the current directory) are walked recursively, honouring a git repository’s `.gitignore` when one is present, and each recognised language is reported with its file, line, code, comment, and blank counts, plus a bar visualising its share of the code. Valid modes are ‘`lines`’, ‘`percent`’, and ‘`both`’ (the default).
|
||||
|
||||
`--follow-symlinks`
|
||||
: Drill down into symbolic links that point to directories.
|
||||
|
||||
@@ -221,6 +226,13 @@ These options are available when running with `--long` (`-l`):
|
||||
`-i`, `--inode`
|
||||
: List each file’s inode number.
|
||||
|
||||
`--loc[=MODE]`
|
||||
: Add a language column and a lines-of-code column to the long view.
|
||||
|
||||
: Only regular files in a recognised programming language are counted; counting is comment-aware, so the code column excludes comment and blank lines.
|
||||
|
||||
: Valid modes are ‘`lines`’ (the count of code lines), ‘`percent`’ (each file’s share of the code in the whole tree), and ‘`both`’ (the default). In `percent` and `both` modes the denominator is the total code across the recursed tree, or the git repository if one is present.
|
||||
|
||||
`-m`, `--modified`
|
||||
: Use the modified timestamp field.
|
||||
|
||||
|
||||
@@ -183,6 +183,13 @@ commands:
|
||||
? - -i
|
||||
- --inode
|
||||
:
|
||||
? # so only the bare forms are generated here.
|
||||
- null # --loc and --code take their mode via `=` only (lines/percent/both),
|
||||
- --loc
|
||||
: prefix: -l
|
||||
? - null
|
||||
- --code
|
||||
:
|
||||
? - -m
|
||||
- --modified
|
||||
:
|
||||
|
||||
@@ -338,6 +338,25 @@ impl<'dir> File<'dir> {
|
||||
self.filetype().is_some_and(std::fs::FileType::is_file)
|
||||
}
|
||||
|
||||
/// The programming language this file is written in, worked out from its
|
||||
/// name or extension, if eza recognises it.
|
||||
pub fn language(&self) -> Option<&'static crate::loc::Language> {
|
||||
crate::loc::language_for(&self.name, self.ext.as_deref())
|
||||
}
|
||||
|
||||
/// Count this file’s lines of code. Returns `None` for anything that
|
||||
/// isn’t a readable, regular file in a recognised language — that is,
|
||||
/// directories, links, unknown extensions, and binaries.
|
||||
pub fn loc(&self) -> Option<crate::loc::LocCounts> {
|
||||
if !self.is_file() {
|
||||
return None;
|
||||
}
|
||||
let lang = self.language()?;
|
||||
crate::loc::LocCounts::from_path(&self.path, lang)
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Whether this file is both a regular file *and* executable for the
|
||||
/// current user. An executable file has a different purpose from an
|
||||
/// executable directory, so they should be highlighted differently.
|
||||
|
||||
@@ -9,6 +9,8 @@ pub mod fs;
|
||||
#[allow(unused)]
|
||||
pub mod info;
|
||||
#[allow(unused)]
|
||||
pub mod loc;
|
||||
#[allow(unused)]
|
||||
pub mod logger;
|
||||
#[allow(unused)]
|
||||
pub mod options;
|
||||
|
||||
+622
@@ -0,0 +1,622 @@
|
||||
// SPDX-FileCopyrightText: 2024 Christina Sørensen
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors
|
||||
// SPDX-License-Identifier: MIT
|
||||
//! Lines-of-code counting.
|
||||
//!
|
||||
//! This is eza’s own, dependency-free source-code counter. Given a file’s
|
||||
//! name we work out which programming language it’s written in (from its
|
||||
//! extension, or its whole name for files like `Makefile`), and then we walk
|
||||
//! its contents a line at a time, classifying each physical line as one of:
|
||||
//!
|
||||
//! - **code** — a line that contains at least one character of actual source,
|
||||
//! - **comment** — a line that is entirely comment (line- or block-style),
|
||||
//! - **blank** — a line that is empty or only whitespace.
|
||||
//!
|
||||
//! The counter is deliberately *comment-aware* rather than a naïve line
|
||||
//! count: it understands each language’s line- and block-comment syntax and
|
||||
//! skips over string literals so that a `//` inside `"https://…"` is not
|
||||
//! mistaken for a comment. Its known limitations are that block comments are
|
||||
//! treated as non-nesting and that only single-line string literals are
|
||||
//! tracked; both are rare enough in practice not to skew a project’s totals.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::io;
|
||||
use std::ops::AddAssign;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use phf::{Map, phf_map};
|
||||
use rayon::prelude::*;
|
||||
|
||||
/// A programming language eza knows how to count, along with the comment
|
||||
/// syntax needed to tell code from commentary.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Language {
|
||||
/// The human-readable name shown in the summary and language column.
|
||||
pub name: &'static str,
|
||||
|
||||
/// Tokens that begin a comment lasting to the end of the line.
|
||||
pub line_comments: &'static [&'static str],
|
||||
|
||||
/// `(open, close)` delimiter pairs for block comments.
|
||||
pub block_comments: &'static [(&'static str, &'static str)],
|
||||
}
|
||||
|
||||
/// The tally of lines within a single file, or an aggregate across many.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct LocCounts {
|
||||
/// Total physical lines. Always equals `code + comments + blanks`.
|
||||
pub lines: usize,
|
||||
|
||||
/// Lines containing at least one character of source code.
|
||||
pub code: usize,
|
||||
|
||||
/// Lines that are entirely comment.
|
||||
pub comments: usize,
|
||||
|
||||
/// Empty or whitespace-only lines.
|
||||
pub blanks: usize,
|
||||
}
|
||||
|
||||
impl AddAssign for LocCounts {
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.lines += rhs.lines;
|
||||
self.code += rhs.code;
|
||||
self.comments += rhs.comments;
|
||||
self.blanks += rhs.blanks;
|
||||
}
|
||||
}
|
||||
|
||||
impl LocCounts {
|
||||
/// Count the lines of the given `source`, using `lang`’s comment rules.
|
||||
#[must_use]
|
||||
pub fn from_source(source: &str, lang: &Language) -> Self {
|
||||
let mut counts = Self::default();
|
||||
// The block-comment terminator we’re currently hunting for, if any.
|
||||
// This is threaded across lines so multi-line block comments work.
|
||||
let mut block: Option<&'static str> = None;
|
||||
|
||||
for line in source.lines() {
|
||||
counts.lines += 1;
|
||||
let (has_code, has_comment) = classify_line(line, lang, &mut block);
|
||||
if has_code {
|
||||
counts.code += 1;
|
||||
} else if has_comment {
|
||||
counts.comments += 1;
|
||||
} else {
|
||||
counts.blanks += 1;
|
||||
}
|
||||
}
|
||||
|
||||
counts
|
||||
}
|
||||
|
||||
/// Read `path` and count its lines. Returns `Ok(None)` for files that
|
||||
/// aren’t valid UTF-8 (i.e. binaries), which we simply don’t count.
|
||||
pub fn from_path(path: &Path, lang: &Language) -> io::Result<Option<Self>> {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(source) => Ok(Some(Self::from_source(&source, lang))),
|
||||
Err(e) if e.kind() == io::ErrorKind::InvalidData => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify a single physical line, updating `block` with any block-comment
|
||||
/// state that carries over to the next line. Returns `(has_code, has_comment)`.
|
||||
fn classify_line(line: &str, lang: &Language, block: &mut Option<&'static str>) -> (bool, bool) {
|
||||
let mut has_code = false;
|
||||
let mut has_comment = false;
|
||||
let mut rest = line;
|
||||
|
||||
'scan: loop {
|
||||
// Inside a block comment: everything up to the closing delimiter is
|
||||
// comment. If it never closes on this line, the block continues.
|
||||
if let Some(close) = *block {
|
||||
has_comment = true;
|
||||
match rest.find(close) {
|
||||
Some(pos) => {
|
||||
rest = &rest[pos + close.len()..];
|
||||
*block = None;
|
||||
continue;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
rest = rest.trim_start();
|
||||
if rest.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
// A line comment swallows the remainder of the line.
|
||||
if lang.line_comments.iter().any(|lc| rest.starts_with(lc)) {
|
||||
has_comment = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// A block comment opener starts a (possibly multi-line) comment.
|
||||
if let Some((open, close)) = lang
|
||||
.block_comments
|
||||
.iter()
|
||||
.find(|(open, _)| rest.starts_with(open))
|
||||
{
|
||||
has_comment = true;
|
||||
*block = Some(close);
|
||||
rest = &rest[open.len()..];
|
||||
continue 'scan;
|
||||
}
|
||||
|
||||
// Anything else is code. Consume one unit, skipping over string
|
||||
// literals so their contents can’t be mistaken for comments.
|
||||
has_code = true;
|
||||
let c = rest.chars().next().unwrap();
|
||||
if c == '"' {
|
||||
rest = consume_string(&rest[c.len_utf8()..], '"');
|
||||
} else {
|
||||
rest = &rest[c.len_utf8()..];
|
||||
}
|
||||
}
|
||||
|
||||
(has_code, has_comment)
|
||||
}
|
||||
|
||||
/// Consume a string literal body, returning the slice after the closing
|
||||
/// `quote`. Backslash escapes are honoured; an unterminated string consumes
|
||||
/// the rest of the line.
|
||||
fn consume_string(s: &str, quote: char) -> &str {
|
||||
let mut chars = s.char_indices();
|
||||
while let Some((i, c)) = chars.next() {
|
||||
if c == '\\' {
|
||||
chars.next();
|
||||
} else if c == quote {
|
||||
return &s[i + c.len_utf8()..];
|
||||
}
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
/// Work out the language of a file from its whole name (for files like
|
||||
/// `Makefile`) or, failing that, its already-lowercased extension.
|
||||
#[must_use]
|
||||
pub fn language_for(name: &str, ext: Option<&str>) -> Option<&'static Language> {
|
||||
if let Some(lang) = BY_FILENAME.get(name) {
|
||||
return Some(lang);
|
||||
}
|
||||
ext.and_then(|e| BY_EXTENSION.get(e)).copied()
|
||||
}
|
||||
|
||||
/// The aggregate line counts for a single language across many files.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LangStat {
|
||||
pub language: &'static Language,
|
||||
pub files: usize,
|
||||
pub counts: LocCounts,
|
||||
|
||||
/// The name and lowercase extension of one counted file, giving the
|
||||
/// summary view a representative file to pick the language’s icon from.
|
||||
pub rep_file: (String, Option<String>),
|
||||
}
|
||||
|
||||
/// The result of counting a whole tree: a per-language breakdown, ordered by
|
||||
/// language name for stable output.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Report {
|
||||
languages: BTreeMap<&'static str, LangStat>,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
fn add(&mut self, language: &'static Language, counts: LocCounts, path: &Path) {
|
||||
let stat = self.languages.entry(language.name).or_insert_with(|| {
|
||||
let name = path
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(str::to_ascii_lowercase);
|
||||
LangStat {
|
||||
language,
|
||||
files: 0,
|
||||
counts: LocCounts::default(),
|
||||
rep_file: (name, ext),
|
||||
}
|
||||
});
|
||||
stat.files += 1;
|
||||
stat.counts += counts;
|
||||
}
|
||||
|
||||
/// The per-language rows, ordered by language name.
|
||||
#[must_use]
|
||||
pub fn languages(&self) -> impl Iterator<Item = &LangStat> {
|
||||
self.languages.values()
|
||||
}
|
||||
|
||||
/// The grand total across every language.
|
||||
#[must_use]
|
||||
pub fn total(&self) -> LocCounts {
|
||||
let mut total = LocCounts::default();
|
||||
for stat in self.languages.values() {
|
||||
total += stat.counts;
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
/// The total number of counted files.
|
||||
#[must_use]
|
||||
pub fn total_files(&self) -> usize {
|
||||
self.languages.values().map(|s| s.files).sum()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.languages.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively count every recognised source file under `roots`, using
|
||||
/// `is_ignored` to skip files (e.g. those matched by `.gitignore`). Hidden
|
||||
/// entries and symbolic links are always skipped, so `.git` and friends never
|
||||
/// get walked. Counting itself is parallelised across a thread pool.
|
||||
pub fn count_tree<F>(roots: &[PathBuf], is_ignored: &F) -> Report
|
||||
where
|
||||
F: Fn(&Path) -> bool,
|
||||
{
|
||||
let mut jobs: Vec<(PathBuf, &'static Language)> = Vec::new();
|
||||
for root in roots {
|
||||
collect_jobs(root, is_ignored, &mut jobs);
|
||||
}
|
||||
|
||||
let counted: Vec<(&'static Language, LocCounts, &PathBuf)> = jobs
|
||||
.par_iter()
|
||||
.filter_map(|(path, lang)| {
|
||||
LocCounts::from_path(path, lang)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|counts| (*lang, counts, path))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut report = Report::default();
|
||||
for (lang, counts, path) in counted {
|
||||
report.add(lang, counts, path);
|
||||
}
|
||||
report
|
||||
}
|
||||
|
||||
/// Walk one path, gathering `(file, language)` jobs for every recognised
|
||||
/// source file beneath it.
|
||||
fn collect_jobs<F>(path: &Path, is_ignored: &F, jobs: &mut Vec<(PathBuf, &'static Language)>)
|
||||
where
|
||||
F: Fn(&Path) -> bool,
|
||||
{
|
||||
let Ok(meta) = std::fs::symlink_metadata(path) else {
|
||||
return;
|
||||
};
|
||||
let file_type = meta.file_type();
|
||||
|
||||
// Never follow symlinks: it risks cycles and double-counting.
|
||||
if file_type.is_symlink() {
|
||||
return;
|
||||
}
|
||||
|
||||
if file_type.is_file() {
|
||||
let name = path
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default();
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(str::to_ascii_lowercase);
|
||||
if let Some(lang) = language_for(name, ext.as_deref()) {
|
||||
jobs.push((path.to_path_buf(), lang));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if file_type.is_dir() {
|
||||
let Ok(entries) = std::fs::read_dir(path) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
// Skip hidden entries by default — this also keeps us out of `.git`.
|
||||
if entry.file_name().to_string_lossy().starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
let child = entry.path();
|
||||
if is_ignored(&child) {
|
||||
continue;
|
||||
}
|
||||
collect_jobs(&child, is_ignored, jobs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Count the given `roots`, respecting a repository’s `.gitignore` if the
|
||||
/// `git` feature is enabled and the roots live inside one. This is the entry
|
||||
/// point used by both the `--loc` percentage columns and the `--code` summary.
|
||||
#[must_use]
|
||||
pub fn count_roots(roots: &[PathBuf]) -> Report {
|
||||
#[cfg(feature = "git")]
|
||||
{
|
||||
if let Some(first) = roots.first()
|
||||
&& let Ok(repo) = git2::Repository::discover(first)
|
||||
{
|
||||
// `is_path_ignored` wants a path it can resolve against the work
|
||||
// tree; a `./`-prefixed relative path (as produced by walking `.`)
|
||||
// confuses it, so canonicalise first, falling back to the raw path.
|
||||
let is_ignored = |p: &Path| {
|
||||
let resolved = std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
|
||||
repo.is_path_ignored(&resolved).unwrap_or(false)
|
||||
};
|
||||
return count_tree(roots, &is_ignored);
|
||||
}
|
||||
}
|
||||
count_tree(roots, &|_: &Path| false)
|
||||
}
|
||||
|
||||
// Comment-syntax building blocks, shared between the many languages that use
|
||||
// the same conventions.
|
||||
const C_LINE: &[&str] = &["//"];
|
||||
const C_BLOCK: &[(&str, &str)] = &[("/*", "*/")];
|
||||
const HASH_LINE: &[&str] = &["#"];
|
||||
const NO_BLOCK: &[(&str, &str)] = &[];
|
||||
|
||||
macro_rules! languages {
|
||||
($( $konst:ident = ($name:literal, $line:expr, $block:expr); )*) => {
|
||||
$(
|
||||
static $konst: Language = Language {
|
||||
name: $name,
|
||||
line_comments: $line,
|
||||
block_comments: $block,
|
||||
};
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
languages! {
|
||||
RUST = ("Rust", C_LINE, &[("/*", "*/")]);
|
||||
C = ("C", C_LINE, C_BLOCK);
|
||||
CPP = ("C++", C_LINE, C_BLOCK);
|
||||
CSHARP = ("C#", C_LINE, C_BLOCK);
|
||||
JAVA = ("Java", C_LINE, C_BLOCK);
|
||||
KOTLIN = ("Kotlin", C_LINE, C_BLOCK);
|
||||
SCALA = ("Scala", C_LINE, C_BLOCK);
|
||||
SWIFT = ("Swift", C_LINE, C_BLOCK);
|
||||
GO = ("Go", C_LINE, C_BLOCK);
|
||||
JAVASCRIPT = ("JavaScript", C_LINE, C_BLOCK);
|
||||
TYPESCRIPT = ("TypeScript", C_LINE, C_BLOCK);
|
||||
JSX = ("JSX", C_LINE, C_BLOCK);
|
||||
TSX = ("TSX", C_LINE, C_BLOCK);
|
||||
DART = ("Dart", C_LINE, C_BLOCK);
|
||||
ZIG = ("Zig", C_LINE, NO_BLOCK);
|
||||
OBJC = ("Objective-C", C_LINE, C_BLOCK);
|
||||
PHP = ("PHP", &["//", "#"], C_BLOCK);
|
||||
CSS = ("CSS", NO_LINE, C_BLOCK);
|
||||
SCSS = ("SCSS", C_LINE, C_BLOCK);
|
||||
GLSL = ("GLSL", C_LINE, C_BLOCK);
|
||||
PYTHON = ("Python", HASH_LINE, &[("\"\"\"", "\"\"\""), ("'''", "'''")]);
|
||||
RUBY = ("Ruby", HASH_LINE, &[("=begin", "=end")]);
|
||||
PERL = ("Perl", HASH_LINE, &[("=pod", "=cut")]);
|
||||
SHELL = ("Shell", HASH_LINE, NO_BLOCK);
|
||||
FISH = ("Fish", HASH_LINE, NO_BLOCK);
|
||||
POWERSHELL = ("PowerShell", HASH_LINE, &[("<#", "#>")]);
|
||||
LUA = ("Lua", &["--"], &[("--[[", "]]")]);
|
||||
HASKELL = ("Haskell", &["--"], &[("{-", "-}")]);
|
||||
ELM = ("Elm", &["--"], &[("{-", "-}")]);
|
||||
SQL = ("SQL", &["--"], C_BLOCK);
|
||||
NIX = ("Nix", HASH_LINE, C_BLOCK);
|
||||
TOML = ("TOML", HASH_LINE, NO_BLOCK);
|
||||
YAML = ("YAML", HASH_LINE, NO_BLOCK);
|
||||
JSON = ("JSON", NO_LINE, NO_BLOCK);
|
||||
MARKDOWN = ("Markdown", NO_LINE, NO_BLOCK);
|
||||
HTML = ("HTML", NO_LINE, &[("<!--", "-->")]);
|
||||
XML = ("XML", NO_LINE, &[("<!--", "-->")]);
|
||||
ELIXIR = ("Elixir", HASH_LINE, NO_BLOCK);
|
||||
ERLANG = ("Erlang", &["%"], NO_BLOCK);
|
||||
CLOJURE = ("Clojure", &[";"], NO_BLOCK);
|
||||
LISP = ("Lisp", &[";"], &[("#|", "|#")]);
|
||||
SCHEME = ("Scheme", &[";"], &[("#|", "|#")]);
|
||||
OCAML = ("OCaml", NO_LINE, &[("(*", "*)")]);
|
||||
FSHARP = ("F#", C_LINE, &[("(*", "*)")]);
|
||||
VIM = ("Vim script", &["\""], NO_BLOCK);
|
||||
MAKE = ("Makefile", HASH_LINE, NO_BLOCK);
|
||||
DOCKER = ("Dockerfile", HASH_LINE, NO_BLOCK);
|
||||
TEX = ("TeX", &["%"], NO_BLOCK);
|
||||
R = ("R", HASH_LINE, NO_BLOCK);
|
||||
JULIA = ("Julia", HASH_LINE, &[("#=", "=#")]);
|
||||
ASSEMBLY = ("Assembly", &[";"], NO_BLOCK);
|
||||
PROTOBUF = ("Protocol Buffers", C_LINE, C_BLOCK);
|
||||
}
|
||||
|
||||
const NO_LINE: &[&str] = &[];
|
||||
|
||||
/// Look-up from an exact file name to its language.
|
||||
static BY_FILENAME: Map<&'static str, &'static Language> = phf_map! {
|
||||
"Makefile" => &MAKE,
|
||||
"makefile" => &MAKE,
|
||||
"GNUmakefile" => &MAKE,
|
||||
"Dockerfile" => &DOCKER,
|
||||
"Containerfile" => &DOCKER,
|
||||
"Rakefile" => &RUBY,
|
||||
"Gemfile" => &RUBY,
|
||||
"CMakeLists.txt" => &MAKE,
|
||||
};
|
||||
|
||||
/// Look-up from a (lowercase) extension to its language.
|
||||
static BY_EXTENSION: Map<&'static str, &'static Language> = phf_map! {
|
||||
"rs" => &RUST,
|
||||
"c" => &C,
|
||||
"h" => &C,
|
||||
"cc" => &CPP,
|
||||
"cpp" => &CPP,
|
||||
"cxx" => &CPP,
|
||||
"hpp" => &CPP,
|
||||
"hh" => &CPP,
|
||||
"cs" => &CSHARP,
|
||||
"java" => &JAVA,
|
||||
"kt" => &KOTLIN,
|
||||
"kts" => &KOTLIN,
|
||||
"scala" => &SCALA,
|
||||
"sc" => &SCALA,
|
||||
"swift" => &SWIFT,
|
||||
"go" => &GO,
|
||||
"js" => &JAVASCRIPT,
|
||||
"mjs" => &JAVASCRIPT,
|
||||
"cjs" => &JAVASCRIPT,
|
||||
"ts" => &TYPESCRIPT,
|
||||
"jsx" => &JSX,
|
||||
"tsx" => &TSX,
|
||||
"dart" => &DART,
|
||||
"zig" => &ZIG,
|
||||
"m" => &OBJC,
|
||||
"mm" => &OBJC,
|
||||
"php" => &PHP,
|
||||
"css" => &CSS,
|
||||
"scss" => &SCSS,
|
||||
"sass" => &SCSS,
|
||||
"glsl" => &GLSL,
|
||||
"vert" => &GLSL,
|
||||
"frag" => &GLSL,
|
||||
"py" => &PYTHON,
|
||||
"pyw" => &PYTHON,
|
||||
"rb" => &RUBY,
|
||||
"pl" => &PERL,
|
||||
"pm" => &PERL,
|
||||
"sh" => &SHELL,
|
||||
"bash" => &SHELL,
|
||||
"zsh" => &SHELL,
|
||||
"ksh" => &SHELL,
|
||||
"fish" => &FISH,
|
||||
"ps1" => &POWERSHELL,
|
||||
"psm1" => &POWERSHELL,
|
||||
"lua" => &LUA,
|
||||
"hs" => &HASKELL,
|
||||
"elm" => &ELM,
|
||||
"sql" => &SQL,
|
||||
"nix" => &NIX,
|
||||
"toml" => &TOML,
|
||||
"yaml" => &YAML,
|
||||
"yml" => &YAML,
|
||||
"json" => &JSON,
|
||||
"md" => &MARKDOWN,
|
||||
"markdown" => &MARKDOWN,
|
||||
"html" => &HTML,
|
||||
"htm" => &HTML,
|
||||
"xml" => &XML,
|
||||
"ex" => &ELIXIR,
|
||||
"exs" => &ELIXIR,
|
||||
"erl" => &ERLANG,
|
||||
"hrl" => &ERLANG,
|
||||
"clj" => &CLOJURE,
|
||||
"cljs" => &CLOJURE,
|
||||
"lisp" => &LISP,
|
||||
"el" => &LISP,
|
||||
"scm" => &SCHEME,
|
||||
"ml" => &OCAML,
|
||||
"mli" => &OCAML,
|
||||
"fs" => &FSHARP,
|
||||
"fsx" => &FSHARP,
|
||||
"vim" => &VIM,
|
||||
"tex" => &TEX,
|
||||
"r" => &R,
|
||||
"jl" => &JULIA,
|
||||
"s" => &ASSEMBLY,
|
||||
"asm" => &ASSEMBLY,
|
||||
"proto" => &PROTOBUF,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn count(source: &str, lang: &Language) -> LocCounts {
|
||||
LocCounts::from_source(source, lang)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_file_is_all_zero() {
|
||||
assert_eq!(count("", &RUST), LocCounts::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn totals_always_add_up() {
|
||||
let c = count("fn main() {}\n\n// hi\n", &RUST);
|
||||
assert_eq!(c.lines, c.code + c.comments + c.blanks);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counts_code_comments_and_blanks() {
|
||||
let source = "fn main() {\n // a comment\n\n println!(\"hi\");\n}\n";
|
||||
let c = count(source, &RUST);
|
||||
assert_eq!(
|
||||
c,
|
||||
LocCounts {
|
||||
lines: 5,
|
||||
code: 3,
|
||||
comments: 1,
|
||||
blanks: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_comments_span_lines() {
|
||||
let source = "code();\n/* start\nstill comment\nend */\nmore();\n";
|
||||
let c = count(source, &C);
|
||||
assert_eq!(
|
||||
c,
|
||||
LocCounts {
|
||||
lines: 5,
|
||||
code: 2,
|
||||
comments: 3,
|
||||
blanks: 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_then_trailing_comment_is_code() {
|
||||
let c = count("let x = 1; // set x\n", &RUST);
|
||||
assert_eq!(c.code, 1);
|
||||
assert_eq!(c.comments, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comment_token_inside_string_is_code() {
|
||||
let c = count("let url = \"https://example.com\";\n", &RUST);
|
||||
assert_eq!(c.code, 1);
|
||||
assert_eq!(c.comments, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_open_and_close_on_same_line_after_code() {
|
||||
let c = count("do_thing(); /* inline */ do_more();\n", &C);
|
||||
assert_eq!(c.code, 1);
|
||||
assert_eq!(c.comments, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_languages() {
|
||||
let c = count("# comment\nname = 1\n", &TOML);
|
||||
assert_eq!(c.code, 1);
|
||||
assert_eq!(c.comments, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_language_by_extension() {
|
||||
assert_eq!(language_for("main.rs", Some("rs")), Some(&RUST));
|
||||
assert_eq!(language_for("app.py", Some("py")), Some(&PYTHON));
|
||||
assert_eq!(language_for("mystery.xyz", Some("xyz")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_language_by_filename() {
|
||||
assert_eq!(language_for("Makefile", None), Some(&MAKE));
|
||||
assert_eq!(language_for("Dockerfile", Some("")), Some(&DOCKER));
|
||||
}
|
||||
}
|
||||
+44
-1
@@ -23,12 +23,13 @@ use crate::fs::filter::{FileFilterFlags::OnlyFiles, GitIgnore};
|
||||
use crate::fs::{Dir, File};
|
||||
use crate::options::stdin::FilesInput;
|
||||
use crate::options::{Options, Vars, vars};
|
||||
use crate::output::{Mode, View, details, escape, file_name, grid, grid_details, lines};
|
||||
use crate::output::{Mode, View, code, details, escape, file_name, grid, grid_details, lines};
|
||||
use crate::theme::Theme;
|
||||
use log::*;
|
||||
|
||||
mod fs;
|
||||
mod info;
|
||||
mod loc;
|
||||
mod logger;
|
||||
mod options;
|
||||
mod output;
|
||||
@@ -226,6 +227,44 @@ impl Exa<'_> {
|
||||
pub fn run(mut self) -> io::Result<i32> {
|
||||
debug!("Running with options: {:#?}", self.options);
|
||||
|
||||
// The `--code` summary doesn’t list files: it walks the given paths and
|
||||
// prints a per-language lines-of-code breakdown, so handle it up front.
|
||||
if let Mode::Code(opts) = &self.options.view.mode {
|
||||
let opts = *opts;
|
||||
let mut exit_status = 0;
|
||||
let mut roots = Vec::new();
|
||||
|
||||
// Report paths that don’t exist, like the normal listing does,
|
||||
// and count the rest.
|
||||
for file_path in &self.input_paths {
|
||||
let path = PathBuf::from(file_path);
|
||||
if let Err(e) = std::fs::symlink_metadata(&path) {
|
||||
exit_status = 2;
|
||||
writeln!(io::stderr(), "{file_path:?}: {e}")?;
|
||||
} else {
|
||||
roots.push(path);
|
||||
}
|
||||
}
|
||||
let file_style = &self.options.view.file_style;
|
||||
let show_icons = match file_style.show_icons {
|
||||
file_name::ShowIcons::Always(_) => true,
|
||||
file_name::ShowIcons::Automatic(_) => file_style.is_a_tty,
|
||||
file_name::ShowIcons::Never => false,
|
||||
};
|
||||
if roots.is_empty() {
|
||||
return Ok(exit_status);
|
||||
}
|
||||
|
||||
let r = code::Render {
|
||||
theme: &self.theme,
|
||||
opts: &opts,
|
||||
roots,
|
||||
show_icons,
|
||||
};
|
||||
r.render(&mut self.writer)?;
|
||||
return Ok(exit_status);
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
let mut dirs = Vec::new();
|
||||
let mut exit_status = 0;
|
||||
@@ -512,6 +551,10 @@ impl Exa<'_> {
|
||||
};
|
||||
r.render(&mut self.writer)
|
||||
}
|
||||
|
||||
// The code summary never lists files; it’s handled up front in
|
||||
// `run` before we ever get here.
|
||||
(Mode::Code(_), _) => unreachable!("--code is handled in Exa::run"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,12 @@ pub fn get_command() -> clap::Command {
|
||||
.arg(arg!(-T --tree "recurse into directories as a tree"))
|
||||
.arg(arg!(-L --level <DEPTH> "limit the depth of recursion")
|
||||
.value_parser(value_parser!(usize)))
|
||||
.arg(arg!(--code <MODE> "summarise lines of code by language, recursing the tree or git repo")
|
||||
.num_args(0..=1)
|
||||
.require_equals(true)
|
||||
.value_parser(value_parser!(CodeContent))
|
||||
.default_missing_value("both")
|
||||
.hide_possible_values(true))
|
||||
.arg(arg!(--"follow-symlinks" "drill down into symbolic links that point to directories"))
|
||||
.arg(arg!(-w --width <COLS> "set screen width in columns")
|
||||
.value_parser(value_parser!(usize)))
|
||||
@@ -118,6 +124,12 @@ pub fn get_command() -> clap::Command {
|
||||
.next_help_heading("LONG VIEW OPTIONS")
|
||||
.arg(arg!(-h --header "add a header row to each column"))
|
||||
.arg(arg!(-i --inode "list each file's inode number"))
|
||||
.arg(arg!(--loc <MODE> "add lines-of-code and language columns [modes: lines, percent, both]")
|
||||
.num_args(0..=1)
|
||||
.require_equals(true)
|
||||
.value_parser(value_parser!(CodeContent))
|
||||
.default_missing_value("both")
|
||||
.hide_possible_values(true))
|
||||
.arg(arg!(-o --"octal-permissions" "list each file's permission in octal format"))
|
||||
.arg(arg!(-H --links "list each file's number of hard links"))
|
||||
.arg(arg!(-b --binary "show file sizes with binary prefixes"))
|
||||
@@ -200,6 +212,15 @@ pub enum ColorScaleModeArgs {
|
||||
Gradient,
|
||||
}
|
||||
|
||||
/// What the `--loc` columns and `--code` summary should display: raw line
|
||||
/// counts, each language’s share as a percentage, or both side by side.
|
||||
#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)]
|
||||
pub enum CodeContent {
|
||||
Lines,
|
||||
Percent,
|
||||
Both,
|
||||
}
|
||||
|
||||
impl ValueEnum for SortField {
|
||||
fn value_variants<'a>() -> &'a [Self] {
|
||||
&[
|
||||
|
||||
+72
-2
@@ -9,7 +9,7 @@ use clap::{ArgMatches, ValueEnum};
|
||||
use crate::output::TerminalWidth::Automatic;
|
||||
|
||||
use crate::fs::feature::xattr;
|
||||
use crate::options::parser::ColorScaleModeArgs;
|
||||
use crate::options::parser::{CodeContent, ColorScaleModeArgs};
|
||||
use crate::options::{NumberSource, OptionsError, Vars, vars};
|
||||
use crate::output::TerminalWidth::Set;
|
||||
use crate::output::color_scale::{ColorScaleMode, ColorScaleOptions};
|
||||
@@ -19,7 +19,7 @@ use crate::output::table::{
|
||||
Columns, FlagsFormat, GroupFormat, Options as TableOptions, SizeFormat, TimeTypes, UserFormat,
|
||||
};
|
||||
use crate::output::time::TimeFormat;
|
||||
use crate::output::{Mode, TerminalWidth, View, details, grid};
|
||||
use crate::output::{Mode, TerminalWidth, View, code, details, grid};
|
||||
|
||||
use super::parser::{ColorScaleArgs, TimeArgs};
|
||||
|
||||
@@ -62,6 +62,12 @@ impl Mode {
|
||||
is_tty: bool,
|
||||
strict: bool,
|
||||
) -> Result<Self, OptionsError> {
|
||||
// `--code` is its own standalone tool: it summarises languages rather
|
||||
// than listing files, so it takes precedence over the layout flags.
|
||||
if let Some(content) = matches.get_one::<CodeContent>("code").copied() {
|
||||
return Ok(Self::Code(code::Options { content }));
|
||||
}
|
||||
|
||||
let long = matches.get_flag("long");
|
||||
let oneline = matches.get_flag("oneline");
|
||||
let grid = matches.get_flag("grid");
|
||||
@@ -123,6 +129,7 @@ impl Mode {
|
||||
"group",
|
||||
"numeric",
|
||||
"mounts",
|
||||
"loc",
|
||||
] {
|
||||
if matches.contains_id(flag) {
|
||||
return Err(OptionsError::Useless(flag, false, "long"));
|
||||
@@ -279,6 +286,8 @@ impl Columns {
|
||||
let filesize = !matches.get_flag("no-filesize");
|
||||
let user = !matches.get_flag("no-user");
|
||||
|
||||
let loc = matches.get_one::<CodeContent>("loc").copied();
|
||||
|
||||
Ok(Self {
|
||||
time_types,
|
||||
inode,
|
||||
@@ -294,6 +303,7 @@ impl Columns {
|
||||
permissions,
|
||||
filesize,
|
||||
user,
|
||||
loc,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1076,4 +1086,64 @@ mod tests {
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deduce_mode_code_default_is_both() {
|
||||
assert_eq!(
|
||||
Mode::deduce(
|
||||
&mock_cli(vec!["--code"]),
|
||||
&MockVars::default(),
|
||||
false,
|
||||
false
|
||||
),
|
||||
Ok(Mode::Code(code::Options {
|
||||
content: CodeContent::Both
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deduce_mode_code_lines() {
|
||||
assert_eq!(
|
||||
Mode::deduce(
|
||||
&mock_cli(vec!["--code=lines"]),
|
||||
&MockVars::default(),
|
||||
false,
|
||||
false
|
||||
),
|
||||
Ok(Mode::Code(code::Options {
|
||||
content: CodeContent::Lines
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deduce_columns_loc_percent() {
|
||||
assert_eq!(
|
||||
Columns::deduce(&mock_cli(vec!["--loc=percent"]), &MockVars::default())
|
||||
.unwrap()
|
||||
.loc,
|
||||
Some(CodeContent::Percent)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deduce_columns_loc_bare_is_both() {
|
||||
assert_eq!(
|
||||
Columns::deduce(&mock_cli(vec!["--loc"]), &MockVars::default())
|
||||
.unwrap()
|
||||
.loc,
|
||||
Some(CodeContent::Both)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deduce_columns_loc_absent() {
|
||||
assert_eq!(
|
||||
Columns::deduce(&mock_cli(vec![""]), &MockVars::default())
|
||||
.unwrap()
|
||||
.loc,
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
// SPDX-FileCopyrightText: 2024 Christina Sørensen
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2023-2024 Christina Sørensen, eza contributors
|
||||
// SPDX-License-Identifier: MIT
|
||||
//! The **Code** view: a standalone lines-of-code summary, in the spirit of
|
||||
//! tools like `tokei` and `cloc`.
|
||||
//!
|
||||
//! Unlike the other views it doesn’t list files at all. Instead it walks the
|
||||
//! given paths (or the current directory) recursively — honouring a git
|
||||
//! repository’s `.gitignore` when there is one — counts every recognised
|
||||
//! source file, and prints one row per language with the project totals
|
||||
//! underneath.
|
||||
//!
|
||||
//! It borrows eza’s long-view look: an underlined header row rather than
|
||||
//! boxes, icons when they’re enabled, locale-aware number formatting, and a
|
||||
//! block-character bar visualising each language’s share of the code.
|
||||
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use nu_ansi_term::Style;
|
||||
|
||||
use crate::loc::LangStat;
|
||||
use crate::options::parser::CodeContent;
|
||||
use crate::output::icons::{icon_for_name_ext, iconify_style};
|
||||
use crate::theme::Theme;
|
||||
|
||||
/// The width, in cells, of the share bar next to the percentage column.
|
||||
const BAR_WIDTH: usize = 16;
|
||||
|
||||
/// Eighth-block characters, from one eighth to a full block, used to give
|
||||
/// the share bar sub-cell resolution.
|
||||
const EIGHTHS: [char; 8] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
|
||||
|
||||
/// Options for the code-summary view.
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub struct Options {
|
||||
/// Whether to show line counts, percentages, or both.
|
||||
pub content: CodeContent,
|
||||
}
|
||||
|
||||
/// Everything needed to render a code summary.
|
||||
pub struct Render<'a> {
|
||||
pub theme: &'a Theme,
|
||||
pub opts: &'a Options,
|
||||
|
||||
/// The paths to count, recursively. Empty means the current directory.
|
||||
pub roots: Vec<PathBuf>,
|
||||
|
||||
/// Whether to prefix each language with a representative file icon.
|
||||
pub show_icons: bool,
|
||||
}
|
||||
|
||||
/// How a summary column lines its contents up.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum Align {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// One fully-styled cell, ready to be measured and painted.
|
||||
struct Cell {
|
||||
text: String,
|
||||
style: Style,
|
||||
align: Align,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
fn new(text: String, style: Style, align: Align) -> Self {
|
||||
Self { text, style, align }
|
||||
}
|
||||
|
||||
/// The display width of this cell. Every character we emit — digits,
|
||||
/// letters, and block glyphs — occupies one terminal cell.
|
||||
fn width(&self) -> usize {
|
||||
self.text.chars().count()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render<'_> {
|
||||
pub fn render<W: Write>(self, w: &mut W) -> io::Result<()> {
|
||||
let report = crate::loc::count_roots(&self.roots);
|
||||
|
||||
if report.is_empty() {
|
||||
let style = self.theme.ui.punctuation.unwrap_or_default();
|
||||
return writeln!(w, "{}", style.paint("No recognised source code found."));
|
||||
}
|
||||
|
||||
let numerics =
|
||||
locale::Numeric::load_user_locale().unwrap_or_else(|_| locale::Numeric::english());
|
||||
|
||||
// The eza-flavoured palette: quantities take the size colour, the
|
||||
// language names the date colour, and structure stays dim.
|
||||
let header = self.theme.ui.header.unwrap_or_default();
|
||||
let lang_style = self.theme.ui.date.unwrap_or_default();
|
||||
let count_style = self
|
||||
.theme
|
||||
.ui
|
||||
.size
|
||||
.unwrap_or_default()
|
||||
.number_byte
|
||||
.unwrap_or_default();
|
||||
let dim = self.theme.ui.punctuation.unwrap_or_default();
|
||||
let bar_style = self
|
||||
.theme
|
||||
.ui
|
||||
.filekinds
|
||||
.unwrap_or_default()
|
||||
.directory
|
||||
.unwrap_or_default();
|
||||
// Bold the totals only when colours are on at all, so piped output
|
||||
// stays free of escape codes.
|
||||
let total_style = if self.theme.ui.colourful.unwrap_or_default() {
|
||||
Style::default().bold()
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
|
||||
let with_lines = matches!(self.opts.content, CodeContent::Lines | CodeContent::Both);
|
||||
let with_percent = matches!(self.opts.content, CodeContent::Percent | CodeContent::Both);
|
||||
|
||||
// Languages sorted by most code first, then by name for stability.
|
||||
let mut langs: Vec<&LangStat> = report.languages().collect();
|
||||
langs.sort_by(|a, b| {
|
||||
b.counts
|
||||
.code
|
||||
.cmp(&a.counts.code)
|
||||
.then_with(|| a.language.name.cmp(b.language.name))
|
||||
});
|
||||
|
||||
let total = report.total();
|
||||
let max_code = langs.first().map_or(0, |s| s.counts.code);
|
||||
|
||||
// The icon column prefix: icons get two cells (glyph + space), and
|
||||
// every icon-less row gets two spaces so the names stay aligned.
|
||||
let lang_cell = |stat: &LangStat| {
|
||||
let name = stat.language.name;
|
||||
if self.show_icons {
|
||||
let (rep_name, rep_ext) = &stat.rep_file;
|
||||
let icon = icon_for_name_ext(rep_name, rep_ext.as_deref());
|
||||
format!("{icon} {name}")
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
};
|
||||
let plain_lang = |name: &str| {
|
||||
if self.show_icons {
|
||||
format!(" {name}")
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let num = |n: usize, style: Style| Cell::new(numerics.format_int(n), style, Align::Right);
|
||||
let pct = |part: usize, style: Style| {
|
||||
let text = if total.code == 0 {
|
||||
"-".to_string()
|
||||
} else {
|
||||
format!("{:.1}%", (part as f64) * 100.0 / (total.code as f64))
|
||||
};
|
||||
Cell::new(text, style, Align::Right)
|
||||
};
|
||||
|
||||
// Build every row up front so each column can be sized to fit.
|
||||
let mut header_row = vec![Cell::new(plain_lang("Language"), header, Align::Left)];
|
||||
let mut body: Vec<Vec<Cell>> = langs
|
||||
.iter()
|
||||
.map(|s| vec![Cell::new(lang_cell(s), lang_style, Align::Left)])
|
||||
.collect();
|
||||
let mut total_row = vec![Cell::new(plain_lang("Total"), total_style, Align::Left)];
|
||||
|
||||
header_row.push(Cell::new("Files".into(), header, Align::Right));
|
||||
for (row, s) in body.iter_mut().zip(&langs) {
|
||||
row.push(num(s.files, count_style));
|
||||
}
|
||||
total_row.push(num(report.total_files(), total_style));
|
||||
|
||||
if with_lines {
|
||||
type Get = fn(&LangStat) -> usize;
|
||||
let metrics: [(&str, Get); 4] = [
|
||||
("Lines", |s| s.counts.lines),
|
||||
("Code", |s| s.counts.code),
|
||||
("Comments", |s| s.counts.comments),
|
||||
("Blanks", |s| s.counts.blanks),
|
||||
];
|
||||
for (title, get) in metrics {
|
||||
header_row.push(Cell::new(title.into(), header, Align::Right));
|
||||
let style = if title == "Code" { count_style } else { dim };
|
||||
for (row, s) in body.iter_mut().zip(&langs) {
|
||||
row.push(num(get(s), style));
|
||||
}
|
||||
}
|
||||
total_row.push(num(total.lines, total_style));
|
||||
total_row.push(num(total.code, total_style));
|
||||
total_row.push(num(total.comments, total_style));
|
||||
total_row.push(num(total.blanks, total_style));
|
||||
}
|
||||
|
||||
if with_percent {
|
||||
header_row.push(Cell::new("Code %".into(), header, Align::Right));
|
||||
for (row, s) in body.iter_mut().zip(&langs) {
|
||||
row.push(pct(s.counts.code, count_style));
|
||||
}
|
||||
total_row.push(pct(total.code, total_style));
|
||||
|
||||
// The share bar: scaled against the largest language, so the top
|
||||
// row always spans the full bar width.
|
||||
header_row.push(Cell::new(String::new(), header, Align::Left));
|
||||
for (row, s) in body.iter_mut().zip(&langs) {
|
||||
row.push(Cell::new(
|
||||
bar(s.counts.code, max_code),
|
||||
bar_style,
|
||||
Align::Left,
|
||||
));
|
||||
}
|
||||
total_row.push(Cell::new(String::new(), total_style, Align::Left));
|
||||
}
|
||||
|
||||
// Size each column to its widest cell.
|
||||
let columns = header_row.len();
|
||||
let mut widths = vec![0; columns];
|
||||
for row in std::iter::once(&header_row)
|
||||
.chain(body.iter())
|
||||
.chain(std::iter::once(&total_row))
|
||||
{
|
||||
for (width, cell) in widths.iter_mut().zip(row.iter()) {
|
||||
*width = (*width).max(cell.width());
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(w, "{}", paint_row(&header_row, &widths, self.show_icons))?;
|
||||
for row in &body {
|
||||
writeln!(w, "{}", paint_row(row, &widths, self.show_icons))?;
|
||||
}
|
||||
|
||||
let rule_width = 1 + widths.iter().sum::<usize>() + 2 * (columns - 1);
|
||||
writeln!(w, "{}", dim.paint("─".repeat(rule_width)))?;
|
||||
writeln!(w, "{}", paint_row(&total_row, &widths, self.show_icons))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a bar visualising `value` against the largest value in the table,
|
||||
/// with eighth-block resolution. Any non-zero value gets at least a sliver.
|
||||
fn bar(value: usize, max: usize) -> String {
|
||||
if max == 0 || value == 0 {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
|
||||
let units = (((value as f64) / (max as f64)) * ((BAR_WIDTH * 8) as f64)).round() as usize;
|
||||
let units = units.max(1);
|
||||
|
||||
let mut bar = "█".repeat(units / 8);
|
||||
if units % 8 > 0 {
|
||||
bar.push(EIGHTHS[units % 8 - 1]);
|
||||
}
|
||||
bar
|
||||
}
|
||||
|
||||
/// Paint one row: a leading space, then each cell padded to its column width
|
||||
/// and separated by two spaces. Trailing whitespace is trimmed so bars and
|
||||
/// short final cells don’t leave invisible padding behind.
|
||||
fn paint_row(cells: &[Cell], widths: &[usize], iconify_first: bool) -> String {
|
||||
let mut out = String::from(" ");
|
||||
for (i, cell) in cells.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(" ");
|
||||
}
|
||||
let padding = " ".repeat(widths[i].saturating_sub(cell.width()));
|
||||
// An empty cell is pure padding: don’t emit pointless colour codes.
|
||||
if cell.text.is_empty() {
|
||||
out.push_str(&padding);
|
||||
continue;
|
||||
}
|
||||
let painted = if i == 0 && iconify_first && cell.width() > 2 {
|
||||
// Paint the icon prefix separately from the name, so underlined
|
||||
// headers don’t drag the underline through the icon column, and
|
||||
// icons keep only the colour of the style they accompany.
|
||||
let split = cell
|
||||
.text
|
||||
.char_indices()
|
||||
.nth(2)
|
||||
.map_or(cell.text.len(), |(pos, _)| pos);
|
||||
let (prefix, name) = cell.text.split_at(split);
|
||||
format!(
|
||||
"{}{}",
|
||||
iconify_style(cell.style).paint(prefix),
|
||||
cell.style.paint(name)
|
||||
)
|
||||
} else {
|
||||
cell.style.paint(cell.text.as_str()).to_string()
|
||||
};
|
||||
match cell.align {
|
||||
Align::Left => {
|
||||
out.push_str(&painted);
|
||||
out.push_str(&padding);
|
||||
}
|
||||
Align::Right => {
|
||||
out.push_str(&padding);
|
||||
out.push_str(&painted);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.truncate(out.trim_end().len());
|
||||
out
|
||||
}
|
||||
@@ -80,6 +80,7 @@ use crate::fs::feature::xattr::Attribute;
|
||||
use crate::fs::fields::SecurityContextType;
|
||||
use crate::fs::filter::FileFilter;
|
||||
use crate::fs::{Dir, File};
|
||||
use crate::options::parser::CodeContent;
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::output::color_scale::{ColorScaleInformation, ColorScaleOptions};
|
||||
use crate::output::file_name::Options as FileStyle;
|
||||
@@ -192,8 +193,16 @@ impl<'a> Render<'a> {
|
||||
(None, _) => { /* Keep Git how it is */ }
|
||||
}
|
||||
|
||||
let loc_content = table.columns.loc;
|
||||
let mut table = Table::new(table, self.git, self.theme, self.git_repos);
|
||||
|
||||
// Percentage columns need the whole tree’s code total as their
|
||||
// denominator, so walk the tree (or git repo) once up front.
|
||||
if matches!(loc_content, Some(CodeContent::Percent | CodeContent::Both)) {
|
||||
let report = crate::loc::count_roots(&self.loc_roots());
|
||||
table.set_loc_total(Some(report.total().code));
|
||||
}
|
||||
|
||||
if self.opts.header {
|
||||
let header = table.header_row();
|
||||
table.add_widths(&header);
|
||||
@@ -231,6 +240,16 @@ impl<'a> Render<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The root path(s) to recurse when computing the `--loc` percentage
|
||||
/// denominator: the directory being listed, or the explicit files given.
|
||||
fn loc_roots(&self) -> Vec<PathBuf> {
|
||||
if let Some(dir) = self.dir {
|
||||
vec![dir.path.clone()]
|
||||
} else {
|
||||
self.files.iter().map(|f| f.path.clone()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to show the extended attribute hint
|
||||
pub fn show_xattr_hint(&self, file: &File<'_>) -> bool {
|
||||
// Do not show the hint '@' if the only extended attribute is the security
|
||||
|
||||
@@ -15,6 +15,7 @@ use term_grid as grid;
|
||||
use crate::fs::feature::git::GitCache;
|
||||
use crate::fs::filter::FileFilter;
|
||||
use crate::fs::{Dir, File};
|
||||
use crate::options::parser::CodeContent;
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::output::color_scale::ColorScaleInformation;
|
||||
use crate::output::details::{Options as DetailsOptions, Render as DetailsRender};
|
||||
@@ -258,6 +259,21 @@ impl<'a> Render<'a> {
|
||||
|
||||
let mut table = Table::new(options, self.git, self.theme, self.git_repos);
|
||||
|
||||
// The `--loc` percentage columns need the whole tree’s code total as
|
||||
// their denominator, just like in the details view.
|
||||
if matches!(
|
||||
options.columns.loc,
|
||||
Some(CodeContent::Percent | CodeContent::Both)
|
||||
) {
|
||||
let roots: Vec<std::path::PathBuf> = if let Some(dir) = self.dir {
|
||||
vec![dir.path.clone()]
|
||||
} else {
|
||||
self.files.iter().map(|f| f.path.clone()).collect()
|
||||
};
|
||||
let report = crate::loc::count_roots(&roots);
|
||||
table.set_loc_total(Some(report.total().code));
|
||||
}
|
||||
|
||||
// The header row will be printed separately, but it should be
|
||||
// considered for the width calculations.
|
||||
if self.details.header {
|
||||
|
||||
@@ -1123,6 +1123,19 @@ pub fn iconify_style(style: Style) -> Style {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Lookup the icon for a plain file name and lowercase extension, without
|
||||
/// needing a whole `File`. Used by the `--code` summary to give each
|
||||
/// language the icon of a representative source file.
|
||||
pub fn icon_for_name_ext(name: &str, ext: Option<&str>) -> char {
|
||||
if let Some(icon) = FILENAME_ICONS.get(name) {
|
||||
*icon
|
||||
} else if let Some(ext) = ext {
|
||||
*EXTENSION_ICONS.get(ext).unwrap_or(&Icons::FILE) //
|
||||
} else {
|
||||
Icons::FILE_UNKNOW //
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup the icon for a file based on the file's name, if the entry is a
|
||||
/// directory, or by the lowercase file extension.
|
||||
pub fn icon_for_file(file: &File<'_>) -> char {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
pub use self::cell::TextCell;
|
||||
pub use self::escape::escape;
|
||||
|
||||
pub mod code;
|
||||
pub mod color_scale;
|
||||
pub mod details;
|
||||
pub mod file_name;
|
||||
@@ -42,6 +43,9 @@ pub enum Mode {
|
||||
Details(details::Options),
|
||||
GridDetails(grid_details::Options),
|
||||
Lines,
|
||||
/// The `--code` lines-of-code summary, which lists languages rather than
|
||||
/// files.
|
||||
Code(code::Options),
|
||||
}
|
||||
|
||||
/// The width of the terminal requested by the user.
|
||||
|
||||
+88
-4
@@ -19,6 +19,7 @@ use uzers::UsersCache;
|
||||
use crate::fs::feature::git::GitCache;
|
||||
use crate::fs::{File, fields as f};
|
||||
use crate::options::Vars;
|
||||
use crate::options::parser::CodeContent;
|
||||
use crate::options::vars::EZA_WINDOWS_ATTRIBUTES;
|
||||
use crate::output::cell::TextCell;
|
||||
use crate::output::color_scale::ColorScaleInformation;
|
||||
@@ -64,6 +65,10 @@ pub struct Columns {
|
||||
pub permissions: bool,
|
||||
pub filesize: bool,
|
||||
pub user: bool,
|
||||
|
||||
/// If set, add a language column and a lines-of-code column (in the
|
||||
/// requested style) to the long view.
|
||||
pub loc: Option<CodeContent>,
|
||||
}
|
||||
|
||||
impl Columns {
|
||||
@@ -94,6 +99,18 @@ impl Columns {
|
||||
columns.push(Column::FileSize);
|
||||
}
|
||||
|
||||
if let Some(content) = self.loc {
|
||||
columns.push(Column::Language);
|
||||
match content {
|
||||
CodeContent::Lines => columns.push(Column::Loc(CodeContent::Lines)),
|
||||
CodeContent::Percent => columns.push(Column::Loc(CodeContent::Percent)),
|
||||
CodeContent::Both => {
|
||||
columns.push(Column::Loc(CodeContent::Lines));
|
||||
columns.push(Column::Loc(CodeContent::Percent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.blocksize {
|
||||
#[cfg(unix)]
|
||||
columns.push(Column::Blocksize);
|
||||
@@ -155,6 +172,8 @@ impl Columns {
|
||||
pub enum Column {
|
||||
Permissions,
|
||||
FileSize,
|
||||
Language,
|
||||
Loc(CodeContent),
|
||||
Timestamp(TimeType),
|
||||
#[cfg(unix)]
|
||||
Blocksize,
|
||||
@@ -190,9 +209,12 @@ impl Column {
|
||||
pub fn alignment(self) -> Alignment {
|
||||
#[allow(clippy::wildcard_in_or_patterns)]
|
||||
match self {
|
||||
Self::FileSize | Self::HardLinks | Self::Inode | Self::Blocksize | Self::GitStatus => {
|
||||
Alignment::Right
|
||||
}
|
||||
Self::FileSize
|
||||
| Self::HardLinks
|
||||
| Self::Inode
|
||||
| Self::Blocksize
|
||||
| Self::GitStatus
|
||||
| Self::Loc(_) => Alignment::Right,
|
||||
Self::Timestamp(_) | _ => Alignment::Left,
|
||||
}
|
||||
}
|
||||
@@ -200,7 +222,7 @@ impl Column {
|
||||
#[cfg(windows)]
|
||||
pub fn alignment(self) -> Alignment {
|
||||
match self {
|
||||
Self::FileSize | Self::GitStatus => Alignment::Right,
|
||||
Self::FileSize | Self::GitStatus | Self::Loc(_) => Alignment::Right,
|
||||
_ => Alignment::Left,
|
||||
}
|
||||
}
|
||||
@@ -215,6 +237,9 @@ impl Column {
|
||||
#[cfg(windows)]
|
||||
Self::Permissions => "Mode",
|
||||
Self::FileSize => "Size",
|
||||
Self::Language => "Language",
|
||||
Self::Loc(CodeContent::Percent) => "Code %",
|
||||
Self::Loc(_) => "Code",
|
||||
Self::Timestamp(t) => t.header(),
|
||||
#[cfg(unix)]
|
||||
Self::Blocksize => "Blocksize",
|
||||
@@ -417,6 +442,10 @@ pub struct Table<'a> {
|
||||
group_format: GroupFormat,
|
||||
flags_format: FlagsFormat,
|
||||
git: Option<&'a GitCache>,
|
||||
|
||||
/// The grand total of code lines used as the denominator for `--loc`
|
||||
/// percentage columns. `None` until computed by the details renderer.
|
||||
loc_total: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -451,9 +480,16 @@ impl<'a> Table<'a> {
|
||||
#[cfg(unix)]
|
||||
group_format: options.group_format,
|
||||
flags_format: options.flags_format,
|
||||
loc_total: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the grand total of code lines, the denominator for `--loc`
|
||||
/// percentage columns.
|
||||
pub fn set_loc_total(&mut self, total: Option<usize>) {
|
||||
self.loc_total = total;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn widths(&self) -> &TableWidths {
|
||||
&self.widths
|
||||
@@ -530,6 +566,8 @@ impl<'a> Table<'a> {
|
||||
&self.env.numeric,
|
||||
color_scale_info,
|
||||
),
|
||||
Column::Language => self.language(file),
|
||||
Column::Loc(content) => self.loc(file, content),
|
||||
#[cfg(unix)]
|
||||
Column::HardLinks => file.links().render(self.theme, &self.env.numeric),
|
||||
#[cfg(unix)]
|
||||
@@ -581,6 +619,52 @@ impl<'a> Table<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The language column: the recognised language’s name, or a dash.
|
||||
fn language(&self, file: &File<'_>) -> TextCell {
|
||||
match file.language() {
|
||||
Some(lang) => TextCell::paint(
|
||||
self.theme.ui.date.unwrap_or_default(),
|
||||
lang.name.to_string(),
|
||||
),
|
||||
None => self.loc_placeholder(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A lines-of-code column, rendered as a raw code-line count or as a
|
||||
/// percentage of the whole tree’s code, depending on `content`.
|
||||
fn loc(&self, file: &File<'_>, content: CodeContent) -> TextCell {
|
||||
let Some(counts) = file.loc() else {
|
||||
return self.loc_placeholder();
|
||||
};
|
||||
// Quantities take the same colour as file sizes, so the Code column
|
||||
// reads consistently next to Size.
|
||||
let style = self
|
||||
.theme
|
||||
.ui
|
||||
.size
|
||||
.unwrap_or_default()
|
||||
.number_byte
|
||||
.unwrap_or_default();
|
||||
match content {
|
||||
CodeContent::Percent => match self.loc_total {
|
||||
Some(total) if total > 0 => {
|
||||
let pct = (counts.code as f64) * 100.0 / (total as f64);
|
||||
TextCell::paint(style, format!("{pct:.1}%"))
|
||||
}
|
||||
_ => self.loc_placeholder(),
|
||||
},
|
||||
_ => TextCell::paint(style, self.env.numeric.format_int(counts.code)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The placeholder shown for files with no language or no count.
|
||||
fn loc_placeholder(&self) -> TextCell {
|
||||
TextCell::paint(
|
||||
self.theme.ui.punctuation.unwrap_or_default(),
|
||||
"-".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn git_status(&self, file: &File<'_>) -> f::Git {
|
||||
debug!("Getting Git status for file {:?}", file.path);
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Language Files Lines Code Comments Blanks Code %
|
||||
Rust 1 8 4 3 1 57.1% ████████████████
|
||||
Markdown 1 3 2 0 1 28.6% ████████
|
||||
Python 1 2 1 1 0 14.3% ████
|
||||
─────────────────────────────────────────────────────────────────────────
|
||||
Total 3 13 7 4 2 100.0%
|
||||
@@ -0,0 +1,4 @@
|
||||
bin.name = "eza"
|
||||
args = "--code tests/itest-loc"
|
||||
[env.add]
|
||||
LC_ALL = "C"
|
||||
@@ -0,0 +1 @@
|
||||
No recognised source code found.
|
||||
@@ -0,0 +1,4 @@
|
||||
bin.name = "eza"
|
||||
args = "--code tests/itest-loc/empty"
|
||||
[env.add]
|
||||
LC_ALL = "C"
|
||||
@@ -0,0 +1,6 @@
|
||||
Language Files Lines Code Comments Blanks
|
||||
Rust 1 8 4 3 1
|
||||
Markdown 1 3 2 0 1
|
||||
Python 1 2 1 1 0
|
||||
───────────────────────────────────────────────
|
||||
Total 3 13 7 4 2
|
||||
@@ -0,0 +1,4 @@
|
||||
bin.name = "eza"
|
||||
args = "--code=lines tests/itest-loc"
|
||||
[env.add]
|
||||
LC_ALL = "C"
|
||||
@@ -0,0 +1,6 @@
|
||||
Language Code Code % Name
|
||||
Rust - - binary.rs
|
||||
- - - empty
|
||||
Rust 4 57.1% hello.rs
|
||||
Markdown 2 28.6% notes.md
|
||||
Python 1 14.3% script.py
|
||||
@@ -0,0 +1,4 @@
|
||||
bin.name = "eza"
|
||||
args = "-lh --loc --no-permissions --no-filesize --no-user --no-time tests/itest-loc"
|
||||
[env.add]
|
||||
LC_ALL = "C"
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
// A greeter, for the lines-of-code tests.
|
||||
|
||||
/* Block comments
|
||||
span lines. */
|
||||
fn main() {
|
||||
let url = "https://eza.rocks"; // not a comment
|
||||
println!("{url}");
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Notes
|
||||
|
||||
Some prose.
|
||||
@@ -0,0 +1,2 @@
|
||||
# Say hi.
|
||||
print("hi")
|
||||
Reference in New Issue
Block a user