├── .github └── workflows │ ├── build_test_branch.yml │ └── build_test_master.yml ├── .gitignore ├── .todor ├── .todorignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── HomebrewFormula └── todor.rb ├── LICENSE ├── README.md ├── benches ├── bench.rs └── inputs │ └── jquery-3.3.1.js ├── build.rs ├── ci ├── before_deploy.sh ├── brew_sha.sh └── sha256.sh ├── config.md ├── ideas.md ├── src ├── bin │ └── todor │ │ ├── clap_app.rs │ │ ├── global_config.rs │ │ ├── logger.rs │ │ ├── main.rs │ │ ├── select.rs │ │ └── walk.rs ├── comments.rs ├── configs.rs ├── custom_tags.rs ├── default_config.json ├── display.rs ├── example_config.hjson ├── format.rs ├── lib.rs ├── maps.rs ├── parser.rs ├── remover.rs └── todo.rs └── tests ├── .todor ├── .todorignore ├── inputs ├── config1.toml ├── config2.json ├── config2.toml ├── config2.yaml ├── test1.rs └── test2.py ├── inputt ├── ignore_this.rs └── test1.rs └── integration_test.rs /.github/workflows/build_test_branch.yml: -------------------------------------------------------------------------------- 1 | name: Test Branch 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | 8 | strategy: 9 | matrix: 10 | rust: [stable] 11 | os: [ubuntu-latest, windows-latest, macos-latest] 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - name: Install latest ${{ matrix.rust }} on ${{ matrix.os }} 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: ${{ matrix.rust }} 23 | components: rustfmt, clippy 24 | 25 | - name: Format 26 | run: cargo fmt -- --check 27 | 28 | - name: Build 29 | run: cargo build --verbose 30 | 31 | - name: Run tests 32 | run: cargo test --verbose -------------------------------------------------------------------------------- /.github/workflows/build_test_master.yml: -------------------------------------------------------------------------------- 1 | name: Test Master 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | schedule: 8 | # Saturday 9 | - cron: '0 17 * * 6' 10 | 11 | jobs: 12 | test: 13 | 14 | strategy: 15 | matrix: 16 | rust: [stable] 17 | os: [ubuntu-latest, windows-latest, macos-latest] 18 | 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v1 23 | 24 | - name: Install latest ${{ matrix.rust }} on ${{ matrix.os }} 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: ${{ matrix.rust }} 29 | components: rustfmt, clippy 30 | 31 | - name: Format 32 | run: cargo fmt -- --check 33 | 34 | - name: Build 35 | run: cargo build --verbose 36 | 37 | - name: Run tests 38 | run: cargo test --verbose -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | 4 | # don't need lock file for lib 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /.todor: -------------------------------------------------------------------------------- 1 | { 2 | // tags to search for. These are case-insensitive 3 | "tags": [ 4 | "todo", 5 | "fix", 6 | "fixme" 7 | ], 8 | 9 | "styles": { 10 | "tags": { 11 | "fix": "red" 12 | "fixme": "red" 13 | "mayb": "magenta" 14 | } 15 | }, 16 | 17 | // Default extension fall-back 18 | "default_ext": "sh", 19 | 20 | // custom comment types 21 | "comments": [ 22 | ] 23 | } -------------------------------------------------------------------------------- /.todorignore: -------------------------------------------------------------------------------- 1 | benches/ 2 | tests/inputs 3 | tests/inputt -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | matrix: 4 | include: 5 | # Stable channel. 6 | - os: linux 7 | rust: stable 8 | env: TARGET=x86_64-unknown-linux-gnu 9 | - os: osx 10 | rust: stable 11 | env: TARGET=x86_64-apple-darwin 12 | - os: windows 13 | rust: stable 14 | env: TARGET=x86_64-pc-windows-msvc 15 | 16 | # Beta test 17 | - os: linux 18 | rust: beta 19 | env: TARGET=x86_64-unknown-linux-gnu 20 | - os: osx 21 | rust: beta 22 | env: TARGET=x86_64-apple-darwin 23 | - os: windows 24 | rust: beta 25 | env: TARGET=x86_64-pc-windows-msvc 26 | 27 | # Nightly test 28 | - os: linux 29 | rust: nightly 30 | env: TARGET=x86_64-unknown-linux-gnu 31 | - os: osx 32 | rust: nightly 33 | env: TARGET=x86_64-apple-darwin 34 | - os: windows 35 | rust: nightly 36 | env: TARGET=x86_64-pc-windows-msvc 37 | 38 | # Code formatting check 39 | - name: rustfmt 40 | os: linux 41 | rust: stable 42 | # skip the global install step 43 | install: 44 | - rustup component add rustfmt-preview 45 | script: cargo fmt -- --check 46 | 47 | allow_failures: 48 | - rust: nightly 49 | fast_finish: true 50 | 51 | env: 52 | global: 53 | - PROJECT_NAME=todor 54 | 55 | cache: 56 | directories: 57 | - ${TRAVIS_HOME}/.cargo 58 | 59 | script: 60 | - cargo build --verbose --all --target $TARGET 61 | - cargo test --verbose --all --target $TARGET 62 | - rm -rf ${TRAVIS_HOME}/.cargo/registry 63 | 64 | before_deploy: 65 | - sh ci/before_deploy.sh 66 | 67 | deploy: 68 | provider: releases 69 | # NOTE updating the `api_key.secure` 70 | # - go to: https://github.com/settings/tokens/new 71 | # - generate new token using `public_repo` scope 72 | # - encrypt it using: `travis encrypt API_KEY_HERE` 73 | # - paste the output below 74 | api_key: 75 | secure: "vvLKNhCaKTifhdKhJR27IDs8x48EVn80f4EX5wWmFdPh9ANt0RJjFWkQtJAVQEekAvIyFiCZ44Fd4l28Fh9MqGia1zvRKZI8RC9xsFVHFvqNih/teb23CY4EPy7p14oIkfV61faWo/M0GG7yhMdfofUBtLL/coE8VAMjqE3IWGV1B5bEMO4OcxpOSZfBkRg025bYV1UCFYjhJlyZPEEMoKvEZS1zncpsUs1XipmXL39ii1OrPQxfX1IcGmD+yO4jterZsVPB278O9j8d3ouNSIktflP5gwk2EgJ+H/EdRZ2m+2QPQdM/SyAJ+t9TarVbX19xL06d7j3yfX8aI/iq6hPCydcdSozFTfMu1QYy2JnmPK4+XvH8D1vNIELrVqSoekjsfzifCUakWcYmT64ysayuPMRZGOEDcMOLIjmOKgV0kPM30dUpGRQ4GZdc2oLstLIgiTMGPem4xsZMJJpEwnt6s3yBdgK/JUiQlW0+QQHDiJURa8EY77HEor31MEqBQ/vTy36wwsD1pJKZrKomHZiIi7yDzRLVf5X4xE78MVBSkZLH8TDWe6SJtKm1SOA5e5K3Mw4AxFZ7WTB6eg5o/nXZtsz5NyM3FAupkt/9YnO19P7jZJUScGIguc5cYsfeROZZOGZo/ywNOVXZu9PHL/eLJOg0cMHUvNI6fZE06Vw=" 76 | # for uploading multiple files 77 | file_glob: true 78 | # NOTE explanation on each env variable 79 | # - PROJECT_NAME: name of the project, set on the `env.global` above 80 | # - TRAVIS_TAG: tag name that the build is being deployed for, usually the version number 81 | # - TARGET: target triple of the build 82 | file: 83 | - $PROJECT_NAME-$TRAVIS_TAG-$TARGET.* 84 | skip_cleanup: true 85 | on: 86 | tags: true 87 | condition: $TRAVIS_RUST_VERSION = stable && $TARGET != "" 88 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Todo_r Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ## [Unreleased] 16 | 17 | ## v0.7.3 (2020-01-17) 18 | ### Added 19 | - Support for empty TODOs 20 | 21 | 22 | ## v0.7.2 (2019-02-10) 23 | ### Changed 24 | - Global config file on MacOS now follows XDG standard so its location can be changed by setting `XDG_CONFIG_HOME` 25 | 26 | 27 | ## v0.7.1 (2019-01-14) 28 | ### Added 29 | - improved help when using `-h` and `--help` tags 30 | 31 | 32 | ## v0.7.0 (2019-01-11) 33 | ### Added 34 | - More formatting options for `-f` flag 35 | - `default` prints in the normal ANSI style 36 | - `usermarkdown` prints a markdown tables organized by tagged users 37 | - `csv` prints a csv table 38 | - `-e` tag for reading piped content from stdin 39 | 40 | ### Changed 41 | - filtering now occurs at the parsing stage instead of while printing 42 | - rewrite of hashmap that handles extensions for a small performance gain. 43 | - internal rewrite of formatting printing that may slightly improve performance 44 | 45 | ### Library changes 46 | - all TodoR methods that with `_filtered_` in the name are removed. Instead filter while parsing using `open_filtered_todos()`. 47 | - full rewrite of `printer.rs` and iterators of `Todo` and `TodoFile` 48 | - renamed what is left of `printer` mod as `format` 49 | - added `maps.rs` to handle specialized HashMaps 50 | 51 | 52 | ## v0.6.0 (2019-01-03) 53 | ### Added 54 | - completions for bash, zsh, fish, and powershell 55 | - formula for `brew` package manager 56 | - global config support 57 | - formatted output formats using `-f` flag 58 | - JSON 59 | - Pretty JSON 60 | - Markdown 61 | 62 | ### Changed 63 | - ignore paths are now entirely handled by `todor` bin 64 | 65 | ### Deprecated 66 | - `ignore` config option 67 | - use a `.todorignore` file or `-i` flag to ignore paths 68 | 69 | ### Fixed 70 | - ANSI style support for numbered ANSI colors 71 | 72 | 73 | ## v0.5.1 (2018-12-19) 74 | ### Fixed 75 | - output when tags are styled to be underlined 76 | - `--check` output when you use `--user` to filter output 77 | 78 | 79 | ## v0.5.0 (2018-12-16) 80 | ### Added 81 | - user tagging 82 | - Types 83 | 1. `// TODO(user): item` 84 | 2. `// TODO: @user1 item @user2 @user3` 85 | - User tags are color highlighted in output 86 | - output only specific users using `-u` or `--user` flag 87 | - regex caching to not rebuild the same regexs over and over again 88 | - support for changing ANSI printing styles in config files 89 | 90 | ### Changed 91 | - stderr output using `--verbose` flag 92 | 93 | ### Library changes 94 | - debug statements using log crate 95 | - pulled `bin/todor.rs` into separate files for potentially better compilation optimization 96 | - moved `Todo` and `TodoFile` types into `todo` module 97 | - pulled config related types out of `comments.rs` and into `configs.rs` 98 | 99 | 100 | ## v0.4.2 (2018-12-10) 101 | ### Added 102 | - Windows release 103 | - `--check` tag to exit nicely only when no TODOs are found 104 | 105 | ### Fixed 106 | - [Windows]: path walker when files are not specified 107 | 108 | 109 | ## v0.4.1 (2018-12-08) 110 | ### Added 111 | - releases on Github using Travis CI 112 | 113 | 114 | ## v0.4 (2018-12-07) 115 | - Initial release 116 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todo_r" 3 | description = "Simple rust command line utility that keeps track of your todo comments in code" 4 | version = "0.7.3" 5 | authors = ["Lavi Blumberg "] 6 | homepage = "https://github.com/lavifb/todo_r" 7 | repository = "https://github.com/lavifb/todo_r" 8 | license = "MIT" 9 | edition = "2021" 10 | readme = "README.md" 11 | exclude = [ 12 | "benches/*", 13 | "tests/*", 14 | "ci/*", 15 | "HomebrewFormula/*", 16 | ".travis.yml", 17 | ] 18 | 19 | [dependencies] 20 | log = { version = "0.4", features = ["max_level_trace", "release_max_level_info"] } 21 | env_logger = "0.9" 22 | clap = "2.32" 23 | ignore = "0.4" 24 | dialoguer = "0.9" 25 | fnv = "1" 26 | regex = "1" 27 | ansi_term = "0.12" 28 | failure = "0.1" 29 | lazy_static = "1" 30 | config = "0.11" 31 | serde = { version = "1", features = ["derive"] } 32 | serde_json = "1" 33 | globset = "0.4" 34 | dirs = "4" 35 | atty = "0.2" 36 | 37 | [dev-dependencies] 38 | criterion = "0.3" 39 | assert_cmd = "2" 40 | 41 | [build-dependencies] 42 | clap = "2.32" 43 | 44 | [[bench]] 45 | name = "bench" 46 | harness = false 47 | 48 | [profile.release] 49 | lto = true 50 | -------------------------------------------------------------------------------- /HomebrewFormula/todor.rb: -------------------------------------------------------------------------------- 1 | class Todor < Formula 2 | version '0.7.3' 3 | desc "Find all your TODO notes with one command!" 4 | homepage "https://github.com/lavifb/todo_r" 5 | 6 | if OS.mac? 7 | url "https://github.com/lavifb/todo_r/releases/download/v0.7.3/todor-v0.7.3-x86_64-apple-darwin.tar.gz" 8 | sha256 "8f64d6c85af4650420148dc015fc34ec2d8930cacd5ccdf6014686d0d59771c8" 9 | elsif OS.linux? 10 | url "https://github.com/lavifb/todo_r/releases/download/v0.7.3/todor-v0.7.3-x86_64-unknown-linux-gnu.tar.gz" 11 | sha256 "6041d4c42f31e8c538c95f4d5e6094a0393e9d4d78bda940b0736ff247706ef0" 12 | end 13 | 14 | conflicts_with "todor" 15 | 16 | def install 17 | bin.install "todor" 18 | 19 | bash_completion.install "complete/todor.bash-completion" 20 | fish_completion.install "complete/todor.fish" 21 | zsh_completion.install "complete/_todor" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lavi Blumberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Todo_r 2 | ====== 3 | [![Actions Status](https://github.com/lavifb/todo_r/workflows/Test%20Master/badge.svg)](https://github.com/lavifb/todo_r/actions) 4 | [![Build Status](https://travis-ci.com/lavifb/todo_r.svg?branch=master)](https://travis-ci.org/lavifb/todo_r) 5 | 6 | ### Find all your notes with one command! 7 | 8 | Todo_r is a simple rust command line utility that keeps track of your todo items in code. 9 | It is pronounced "todoer" like someone that does todos. 10 | 11 | Find all your TODO notes with one command! 12 | 13 | A lot is adapted from [leasot](https://github.com/pgilad/leasot) but runs much faster. 14 | 15 | ## Installation 16 | 17 | The latest release can be downloaded from the releases page. 18 | 19 | If you use macOS Homebrew or Linuxbrew you can currently install the latest version using 20 | ```console 21 | $ brew tap lavifb/todo_r https://github.com/lavifb/todo_r.git 22 | $ brew install todor 23 | ``` 24 | 25 | ## Features 26 | 27 | - Reads TODO comments that are on their own line. 28 | ```rust 29 | // TODO: do this 30 | /* TODO: do that */ 31 | ``` 32 | Note: comments that are not on their own line are __not__ supported. 33 | 34 | - User references are tracked and can be found using `--user` flag. 35 | ```rust 36 | // TODO(user1): item 37 | // TODO: tagging @user2 and @user3 38 | // TODO(user1): @user3 both are also found! 39 | ``` 40 | Comments 1 and 3 are found with `todor -u user1`. 41 | 42 | - Custom tags can be searched using the `-t` flag. 43 | - Interactive mode for deleting comments is launched using the `-d` flag. 44 | - If files are not provided for input, todo_r searches the entire git repository. 45 | - `.gitignore` files are respected 46 | - More ignores can be added using `.todorignore` files that use the same syntax 47 | - If you are not using git, you can instead use a `.todor` file in the root directory 48 | 49 | ## Config files 50 | Create a `.todor` file in the root of your workspace with `todor init`. 51 | 52 | `.todor` files can also used as a config file to set custom tags, comments types, output styles, etc. 53 | 54 | Todo_r also supports a global config file at `$XDG_CONFIG_HOME/todor/todor.conf` (default `~/.config/todor/todor.conf`) for Mac/Linux and `~\AppData\Roaming\lavifb\todor\todor.conf` on Windows. 55 | 56 | A deeper explanation of config files can be found at [config.md](https://github.com/lavifb/todo_r/blob/master/config.md). 57 | 58 | ## Default Language Support 59 | These common languages are supported by default. 60 | More support can be added using config files above. 61 | 62 | | Filetype | Extensions | Comment Types | 63 | |-------------|---------------------|---------------| 64 | |C/C++ |`.c`,`.h`,`.cpp` |`//`,`/* */` | 65 | |C# |`.cs` |`//`,`/* */` | 66 | |CoffeeScript |`.coffee` |`#` | 67 | |Go |`.go` |`//`,`/* */` | 68 | |Haskell |`.hs` |`--` | 69 | |HTML |`.html`,`.htm` |`` | 70 | |Java |`.java` |`//`,`/* */` | 71 | |JavaScript |`.js`,`.es`,`.es6` |`//`,`/* */` | 72 | |Obj-C/C++ |`.m`,`.mm` |`//`,`/* */` | 73 | |Less |`.less` |`//`,`/* */` | 74 | |Markdown |`.md` |`` | 75 | |Perl |`.pl`,`.pm` |`#` | 76 | |PHP |`.php` |`//`,`/* */` | 77 | |Python |`.py` |`#`,`""" """` | 78 | |Ruby |`.rb` |`#` | 79 | |Rust |`.rs` |`//`,`/* */` | 80 | |Sass |`.sass`,`scss` |`//`,`/* */` | 81 | |Scala |`.scala` |`//`,`/* */` | 82 | |Shell |`.sh`,`.bash`,`.zsh` |`#` | 83 | |SQL |`.sql` |`--`,`/* */` | 84 | |Stylus |`.styl` |`//`,`/* */` | 85 | |Swift |`.swift` |`//`,`/* */` | 86 | |TeX |`.tex` |`%` | 87 | |TypeScript |`.ts`,`.tsx` |`//`,`/* */` | 88 | |YAML |`.yaml`,`.yml` |`#` | 89 | 90 | If there are any more languages/extensions that you feel should supported by default, feel free to submit an issue/pull request. 91 | 92 | --- 93 | written by Lavi Blumberg 94 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | // Benchmarking for todor 2 | 3 | use criterion::{criterion_group, criterion_main, Criterion}; 4 | use std::path::Path; 5 | use todo_r::TodoRBuilder; 6 | 7 | fn bench_jquery(c: &mut Criterion) { 8 | c.bench_function("jquery", |b| { 9 | b.iter(|| { 10 | let tags = vec!["TODO", "FIXME"]; 11 | let mut builder = TodoRBuilder::new(); 12 | builder.add_override_tags(tags); 13 | let mut todor = builder.build().unwrap(); 14 | todor 15 | .open_todos(Path::new("benches/inputs/jquery-3.3.1.js")) 16 | .unwrap(); 17 | }) 18 | }); 19 | } 20 | 21 | criterion_group!(benches, bench_jquery); 22 | criterion_main!(benches); 23 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use clap::Shell; 2 | use std::env; 3 | use std::fs; 4 | 5 | include!("src/bin/todor/clap_app.rs"); 6 | 7 | fn main() { 8 | let outdir = match env::var_os("OUT_DIR") { 9 | Some(outdir) => outdir, 10 | None => { 11 | println!("Environment variable OUT_DIR not found."); 12 | std::process::exit(1); 13 | } 14 | }; 15 | 16 | fs::create_dir_all(&outdir).unwrap(); 17 | 18 | let mut app = build_cli(); 19 | app.gen_completions("todor", Shell::Bash, &outdir); 20 | app.gen_completions("todor", Shell::Zsh, &outdir); 21 | app.gen_completions("todor", Shell::Fish, &outdir); 22 | app.gen_completions("todor", Shell::PowerShell, &outdir); 23 | } 24 | -------------------------------------------------------------------------------- /ci/before_deploy.sh: -------------------------------------------------------------------------------- 1 | # This script takes care of building your crate and packaging it for release 2 | 3 | set -ex 4 | 5 | main() { 6 | local src=$(pwd) \ 7 | stage= 8 | 9 | case $TRAVIS_OS_NAME in 10 | linux) 11 | stage=$(mktemp -d) 12 | ;; 13 | osx) 14 | stage=$(mktemp -d -t tmp) 15 | ;; 16 | windows) 17 | stage=$(mktemp -d) 18 | ;; 19 | esac 20 | 21 | test -f Cargo.lock || cargo generate-lockfile 22 | 23 | cargo build --target "$TARGET" --release --verbose --locked 24 | 25 | # copy binary to stage 26 | cp "target/$TARGET/release/$PROJECT_NAME" $stage/ 27 | 28 | # copy completions to stage 29 | mkdir $stage/complete 30 | cp target/"$TARGET"/release/build/todo_r-*/out/"$PROJECT_NAME".bash $stage/complete/${PROJECT_NAME}.bash-completion 31 | cp target/"$TARGET"/release/build/todo_r-*/out/"$PROJECT_NAME".fish $stage/complete/ 32 | cp target/"$TARGET"/release/build/todo_r-*/out/_"$PROJECT_NAME" $stage/complete/ 33 | cp target/"$TARGET"/release/build/todo_r-*/out/_"$PROJECT_NAME".ps1 $stage/complete/ 34 | 35 | cd $stage 36 | tar czf $src/$PROJECT_NAME-$TRAVIS_TAG-$TARGET.tar.gz * 37 | cd $src 38 | 39 | rm -rf $stage 40 | } 41 | 42 | main -------------------------------------------------------------------------------- /ci/brew_sha.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | TODOR_VERSION=$(git describe --abbrev=0 --tags) 6 | MAC_URL="https://github.com/lavifb/todo_r/releases/download/${TODOR_VERSION}/todor-${TODOR_VERSION}-x86_64-apple-darwin.tar.gz" 7 | MAC_SHA=$(curl -sfSL "$MAC_URL" | shasum -a 256) 8 | echo "Mac SHA ${MAC_SHA}" 9 | 10 | LIN_URL="https://github.com/lavifb/todo_r/releases/download/${TODOR_VERSION}/todor-${TODOR_VERSION}-x86_64-unknown-linux-gnu.tar.gz" 11 | LIN_SHA=$(curl -sfSL "$LIN_URL" | shasum -a 256) 12 | echo "Linux SHA ${LIN_SHA}" 13 | 14 | cat >HomebrewFormula/todor.rb <&2 10 | exit 1 11 | fi 12 | version="$1" 13 | 14 | # Linux and Darwin builds. 15 | for arch in x86_64; do 16 | for target in apple-darwin unknown-linux-gnu; do 17 | url="https://github.com/lavifb/todo_r/releases/download/$version/todor-$version-$arch-$target.tar.gz" 18 | sha=$(curl -sfSL "$url" | shasum -a 256) 19 | echo "$version-$arch-$target $sha" 20 | done 21 | done 22 | 23 | # Source. 24 | for ext in zip tar.gz; do 25 | url="https://github.com/lavifb/todo_r/archive/$version.$ext" 26 | sha=$(curl -sfSL "$url" | shasum -a 256) 27 | echo "source.$ext $sha" 28 | done -------------------------------------------------------------------------------- /config.md: -------------------------------------------------------------------------------- 1 | Todo_r Config Documentation 2 | ====== 3 | 4 | Note that while Todo_r is pre-1.0, these settings are subject to change. 5 | 6 | The default configuration that `todor` loads can be found in `src/default_config.json`. 7 | 8 | ### Tags 9 | ```json 10 | "tags": [ 11 | "todo", 12 | "fix", 13 | "fixme" 14 | ] 15 | ``` 16 | 17 | This config lists the keywords that are tracked by `todor`. The list items are case-insensitive. 18 | 19 | ### Styles 20 | ```json 21 | "styles": { 22 | "filepath": "u_", 23 | "tag": "green", 24 | "tags": {}, 25 | "content": "cyan", 26 | "line_number": 8, 27 | "user": 8 28 | } 29 | ``` 30 | 31 | ANSI printing styles for raw output of `todor`. Each item except `"tags"` takes either a string that is an ANSI color or a number 0-255 corresponding to the desired ANSI color. ANSI modifiers like bold, italic, and underline can also be added by prepending `b_`, `i_`, or `u_`. 32 | 33 | `"tags"` lets you define specific ANSI styles on a tag by tag basis. So if you want `FIXME` comments to be red and `MAYB` comments to be magenta, you can set the config to 34 | ```json 35 | "styles": { 36 | "tags": { 37 | "fixme": "red", 38 | "mayb": "magenta" 39 | } 40 | } 41 | ``` 42 | 43 | ### Default Extension 44 | ```json 45 | "default_ext": "sh" 46 | ``` 47 | 48 | The default extension fallback that `todor` uses if the file extension is not supported. This extension has to be defined by `"comments"` below or by `"default_comments"`. 49 | 50 | ### Comment Types 51 | ```json 52 | "comments": [ 53 | { 54 | "exts": [ 55 | "c", 56 | "h", 57 | "cpp", 58 | "rust", 59 | ], 60 | "types": [ 61 | { 62 | "single": "//" 63 | }, 64 | { 65 | "prefix": "/*", 66 | "suffix": "*/" 67 | } 68 | ] 69 | }, 70 | { 71 | "ext": "py", 72 | "types": [ 73 | { 74 | "single": "#" 75 | }, 76 | { 77 | "prefix": "\"\"\"", 78 | "suffix": "\"\"\"" 79 | } 80 | ] 81 | } 82 | ] 83 | ``` 84 | Each item in the list `"comments"` has two parts: 85 | 1. `"ext"` or `"exts"` defines the extensions to which to apply the defined comment type 86 | 2. `"types"` is a list of the types of comments that occur in these extensions 87 | 88 | Comments of two types are supported: single-line and block. 89 | 90 | #### Single-Line Comments 91 | Single-line comments have only a prefix, so 92 | ```json 93 | { 94 | "single": "//" 95 | } 96 | ``` 97 | would match comments like 98 | ```rust 99 | // TODO: item 100 | ``` 101 | 102 | #### Block Comments 103 | Block comments have both a prefix and a suffix, so 104 | ```json 105 | { 106 | "prefix": "/*", 107 | "suffix": "*/" 108 | } 109 | ``` 110 | would match items like 111 | ```rust 112 | /* TODO: item */ 113 | ``` 114 | 115 | --- 116 | Note that `src/default_config.json` uses the config `"default_comments"` so that adding new comment types only overrides the comment types you want to override. 117 | -------------------------------------------------------------------------------- /ideas.md: -------------------------------------------------------------------------------- 1 | Future Ideas 2 | ====== 3 | 4 | - tracks todos since last commit 5 | - keeps separate large todo file 6 | -------------------------------------------------------------------------------- /src/bin/todor/clap_app.rs: -------------------------------------------------------------------------------- 1 | use clap::{App, Arg}; 2 | 3 | #[cfg(windows)] 4 | macro_rules! global_config_path { 5 | () => { 6 | r"~\AppData\Roaming\lavifb\todor\todor.conf" 7 | }; 8 | } 9 | #[cfg(unix)] 10 | macro_rules! global_config_path { 11 | () => { 12 | r"$XDG_CONFIG_HOME/todor/todor.conf or ~/.config/todor/todor.conf" 13 | }; 14 | } 15 | 16 | pub fn build_cli() -> App<'static, 'static> { 17 | App::new("Todo_r") 18 | .version(env!("CARGO_PKG_VERSION")) 19 | .author("Lavi Blumberg ") 20 | .about("Lists TODO comments in code.") 21 | .arg( 22 | Arg::with_name("FILE") 23 | .multiple(true) 24 | .help("Sets todor to only search in provided files."), 25 | ) 26 | .arg( 27 | Arg::with_name("CONFIG") 28 | .short("c") 29 | .long("config") 30 | .takes_value(true) 31 | .help("Takes config from file.") 32 | .long_help(concat!( 33 | "Takes configuration from file. This file should be in a JSON format and \ 34 | allows todor to be customized by adding new comment types for extensions and \ 35 | custom colors. An example file called .todor can be created by using the \ 36 | `todor init` command. \ 37 | \n\n\ 38 | You can also set a global config file at `", 39 | global_config_path!(), 40 | "`.") 41 | ) 42 | ) 43 | .arg( 44 | Arg::with_name("NOSTYLE") 45 | .short("s") 46 | .long("no-style") 47 | .help("Prints output with no ANSI colors or styles."), 48 | ) 49 | .arg( 50 | Arg::with_name("TAGS") 51 | .short("t") 52 | .long("tag") 53 | .takes_value(true) 54 | .multiple(true) 55 | .help("Additional TODO tags to search for.") 56 | .long_help( 57 | "Adds additional tags to search for over the ones provided by default and any \ 58 | config files. \nFor example, to add MAYB and NOW tags to your search, use \n\n\ 59 | \t> todor -t mayb now\n\n\ 60 | to find them. This will also find tags defined in any config files." 61 | ), 62 | ) 63 | .arg( 64 | Arg::with_name("OVERRIDE_TAGS") 65 | .short("T") 66 | .long("override-tag") 67 | .takes_value(true) 68 | .multiple(true) 69 | .help("Overrides default TODO tags to only search custom ones.") 70 | .long_help( 71 | "Works the same as `-t` except tags in default and config are not searched for.\ 72 | Thus, only tags explicitly passed after this flag are considered." 73 | ), 74 | ) 75 | .arg( 76 | Arg::with_name("USER") 77 | .short("u") 78 | .long("user") 79 | .takes_value(true) 80 | .multiple(true) 81 | .help("Filter TODOs to only feature ones that are tagged with users.") 82 | .long_help( 83 | "Only searches for TODOs that include provided users.\n\ 84 | For example, to only print TODOs with user1 and user2, use \n\n\ 85 | \t> todor -u user1 user2\n\n" 86 | ), 87 | ) 88 | .arg( 89 | Arg::with_name("IGNORE") 90 | .short("i") 91 | .long("ignore") 92 | .takes_value(true) 93 | .multiple(true) 94 | .help("Files to be ignored."), 95 | ) 96 | .arg( 97 | Arg::with_name("VERBOSE") 98 | .short("v") 99 | .long("verbose") 100 | .help("Provide verbose output."), 101 | ) 102 | .arg( 103 | Arg::with_name("CHECK") 104 | .long("check") 105 | .help("Exits nicely only if no TODO comments are found."), 106 | ) 107 | .arg( 108 | Arg::with_name("FORMAT") 109 | .short("f") 110 | .long("format") 111 | .takes_value(true) 112 | .possible_values(&[ 113 | "json", 114 | "prettyjson", 115 | "markdown", 116 | "usermarkdown", 117 | "csv", 118 | "default", 119 | ]) 120 | .help("Outputs in specified format.") 121 | .long_help( 122 | "Outputs in specified format. The following formats are supported:\n\n\ 123 | json: compacted JSON\n\ 124 | prettyjson: nicely formatted JSON\n\ 125 | markdown: Markdown tables with a table for each tag type\n\ 126 | usermarkdown: Markdown tables for each user\n\ 127 | csv: Comma separated values table\n\ 128 | default: regular output with no ANSI colors for " 129 | ), 130 | ) 131 | .arg( 132 | Arg::with_name("DELETE_MODE") 133 | .short("d") 134 | .long("delete") 135 | .conflicts_with("FORMAT") 136 | .help("Interactive delete mode.") 137 | .long_help( 138 | "Runs todor and lets you delete TODO comments interactively. First you select \ 139 | which file to delete from and then pick which comment to delete." 140 | ), 141 | ) 142 | .arg( 143 | Arg::with_name("EXT") 144 | .short("e") 145 | .long("ext") 146 | .takes_value(true) 147 | .conflicts_with("DELETE_MODE") 148 | .help("Reads piped content as if it has the provided extention.") 149 | .long_help( 150 | "Reads piped content as if it has the provided extention. For example, \n\n\ 151 | \t> cat test.rs | todor -e rs\n\n\ 152 | will take the piped output from cat and read using the .rs comment styles." 153 | ), 154 | ) 155 | .subcommand( 156 | App::new("init") 157 | .about("Creates .todor config file and defines a todor workspace.") 158 | .author("Lavi Blumberg "), 159 | ) 160 | } 161 | -------------------------------------------------------------------------------- /src/bin/todor/global_config.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(target_os = "macos"))] 2 | use dirs::config_dir; 3 | 4 | #[cfg(target_os = "macos")] 5 | use dirs::home_dir; 6 | #[cfg(target_os = "macos")] 7 | use std::env; 8 | #[cfg(target_os = "macos")] 9 | use std::path::PathBuf; 10 | 11 | use config::FileFormat; 12 | use failure::Error; 13 | use log::info; 14 | 15 | use todo_r::TodoRBuilder; 16 | 17 | pub fn load_global_config(builder: &mut TodoRBuilder) -> Result<(), Error> { 18 | #[cfg(target_os = "macos")] 19 | let config_dir_op = env::var_os("XDG_CONFIG_HOME") 20 | .map(PathBuf::from) 21 | .filter(|p| p.is_absolute()) 22 | .or_else(|| home_dir().map(|d| d.join(".config"))); 23 | 24 | #[cfg(not(target_os = "macos"))] 25 | let config_dir_op = config_dir(); 26 | 27 | if let Some(global_config) = config_dir_op.map(|d| d.join("todor/todor.conf")) { 28 | info!( 29 | "searching for global config in '{}'", 30 | global_config.display() 31 | ); 32 | if global_config.exists() && global_config.metadata().unwrap().len() > 2 { 33 | info!("adding global config file..."); 34 | builder.add_config_file_with_format(global_config, FileFormat::Hjson)?; 35 | } 36 | } 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src/bin/todor/logger.rs: -------------------------------------------------------------------------------- 1 | use env_logger::fmt::Formatter; 2 | use log::Record; 3 | use std::io::Write; 4 | 5 | pub fn init_logger(verbose: bool) { 6 | let log_env = if verbose { 7 | env_logger::Env::default().default_filter_or("info") 8 | } else { 9 | env_logger::Env::default().default_filter_or("error") 10 | }; 11 | 12 | env_logger::Builder::from_env(log_env) 13 | .format(todor_fmt) 14 | .target(env_logger::Target::Stderr) 15 | .init(); 16 | } 17 | 18 | fn todor_fmt(buf: &mut Formatter, record: &Record) -> std::io::Result<()> { 19 | let mut style = buf.style(); 20 | 21 | match record.level() { 22 | log::Level::Error => style.set_color(env_logger::fmt::Color::Red), 23 | log::Level::Warn => style.set_color(env_logger::fmt::Color::Yellow), 24 | _ => style.set_color(env_logger::fmt::Color::White), 25 | }; 26 | 27 | let log_prefix = match record.module_path() { 28 | Some(mod_path) => format!("[{} {}]", mod_path, record.level()), 29 | None => format!("[{}]", record.level()), 30 | }; 31 | 32 | writeln!(buf, "{}: {}", style.value(log_prefix), record.args()) 33 | } 34 | -------------------------------------------------------------------------------- /src/bin/todor/main.rs: -------------------------------------------------------------------------------- 1 | // Binary for finding TODOs in specified files 2 | 3 | mod clap_app; 4 | mod global_config; 5 | mod logger; 6 | mod select; 7 | mod walk; 8 | 9 | use atty; 10 | use clap::ArgMatches; 11 | // use env_logger; 12 | use failure::{format_err, Error}; 13 | use ignore::overrides::OverrideBuilder; 14 | use log::*; 15 | use std::env::current_dir; 16 | use std::fs::File; 17 | use std::io::{stdin, Read}; 18 | use std::path::Path; 19 | 20 | use todo_r::format::ReportFormat; 21 | use todo_r::todo::Todo; 22 | use todo_r::TodoRBuilder; 23 | 24 | use self::clap_app::build_cli; 25 | use self::global_config::load_global_config; 26 | use self::logger::init_logger; 27 | use self::select::run_delete; 28 | use self::walk::build_walker; 29 | 30 | /// Parses command line arguments and use TodoR to find TODO comments. 31 | fn main() { 32 | let matches = build_cli().get_matches(); 33 | 34 | let verbose: bool = matches.is_present("VERBOSE"); 35 | // Set up log output 36 | init_logger(verbose); 37 | 38 | // Run program 39 | let exit_code = if matches.subcommand_matches("init").is_some() { 40 | run_init() 41 | } else { 42 | match run(&matches) { 43 | Ok(code) => code, 44 | Err(err) => { 45 | error!("{}", err); 46 | 1 47 | } 48 | } 49 | }; 50 | 51 | std::process::exit(exit_code); 52 | } 53 | 54 | fn run(matches: &ArgMatches) -> Result { 55 | let mut builder = TodoRBuilder::new(); 56 | 57 | // Search for global config file 58 | load_global_config(&mut builder)?; 59 | 60 | if let Some(config_path) = matches.value_of("CONFIG") { 61 | builder.add_config_file(Path::new(config_path))?; 62 | }; 63 | 64 | if let Some(tags_iter) = matches.values_of("TAGS") { 65 | builder.add_tags(tags_iter); 66 | } 67 | 68 | if let Some(tags_iter) = matches.values_of("OVERRIDE_TAGS") { 69 | builder.add_override_tags(tags_iter); 70 | } 71 | 72 | if matches.is_present("NOSTYLE") { 73 | builder.set_no_style(); 74 | } 75 | 76 | let curr_dir = current_dir()?; 77 | let mut ignore_builder = OverrideBuilder::new(&curr_dir); 78 | if let Some(ignore_paths_iter) = matches.values_of("IGNORE") { 79 | for ignore_path in ignore_paths_iter { 80 | ignore_builder.add(&format!("!{}", ignore_path))?; 81 | } 82 | } 83 | 84 | let pred = if let Some(users_iter) = matches.values_of("USER") { 85 | let users: Vec<&str> = users_iter.collect(); 86 | Some(move |t: &Todo| users.iter().any(|u| t.tags_user(*u))) 87 | } else { 88 | None 89 | }; 90 | 91 | let mut todor; 92 | if let Some(ext) = matches.value_of("EXT") { 93 | todor = builder.build()?; 94 | if atty::isnt(atty::Stream::Stdin) { 95 | let mut buffer = String::new(); 96 | stdin().read_to_string(&mut buffer).unwrap(); 97 | 98 | todor.find_todos(&buffer, ext)?; 99 | } 100 | } else { 101 | match matches.values_of("FILE") { 102 | Some(files) => { 103 | let ignores = ignore_builder.build()?; 104 | todor = builder.build()?; 105 | debug!("todor parser built"); 106 | for file in files { 107 | info!("looking at `{}`...", file); 108 | 109 | if !ignores.matched(file, false).is_ignore() { 110 | todor 111 | .open_option_filtered_todos(file, &pred) 112 | .unwrap_or_else(|err| warn!("{}", err)); 113 | } 114 | } 115 | } 116 | None => { 117 | info!("Looking for .git or .todor to use as workspace root..."); 118 | let walk = build_walker(&mut builder, ignore_builder)?; 119 | todor = builder.build()?; 120 | debug!("todor parser built"); 121 | 122 | for entry in walk { 123 | let dir_entry = entry?; 124 | let path = dir_entry.path().strip_prefix(".").unwrap(); 125 | 126 | debug!("found {} in walk", path.display()); 127 | 128 | if path.is_file() { 129 | info!("looking at `{}`...", path.display()); 130 | todor 131 | .open_todos(path) 132 | .unwrap_or_else(|err| warn!("{}", err)); 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | if matches.is_present("DELETE_MODE") { 140 | run_delete(&mut todor)?; 141 | } else if let Some(format) = matches.value_of("FORMAT") { 142 | let report_format = match format { 143 | "json" => ReportFormat::Json, 144 | "prettyjson" => ReportFormat::JsonPretty, 145 | "markdown" => ReportFormat::Markdown, 146 | "usermarkdown" => ReportFormat::UserMarkdown, 147 | "csv" => ReportFormat::Csv, 148 | "default" => ReportFormat::Default, 149 | _ => return Err(format_err!("invalid output format: {}.", format)), 150 | }; 151 | 152 | todor.print_formatted_todos(&report_format)?; 153 | } else { 154 | todor.print_todos(); 155 | } 156 | 157 | if matches.is_present("CHECK") && todor.num_todos() > 0 { 158 | return Ok(1); 159 | } 160 | 161 | Ok(0) 162 | } 163 | 164 | fn run_init() -> i32 { 165 | let mut config_file = match File::create(Path::new(".todor")) { 166 | Ok(file) => file, 167 | Err(err) => { 168 | error!("{}", err); 169 | return 1; 170 | } 171 | }; 172 | 173 | match todo_r::write_example_config(&mut config_file) { 174 | Ok(_) => 0, 175 | Err(err) => { 176 | error!("{}", err); 177 | 1 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/bin/todor/select.rs: -------------------------------------------------------------------------------- 1 | use ansi_term::Color::Red; 2 | use dialoguer::Select; 3 | use failure::Error; 4 | use log::warn; 5 | use std::path::Path; 6 | 7 | use todo_r::TodoR; 8 | 9 | pub fn run_delete(todor: &mut TodoR) -> Result<(), Error> { 10 | loop { 11 | let file_selection = match select_file(&todor) { 12 | Some(file_selection) => file_selection, 13 | None => return Ok(()), 14 | }; 15 | 16 | let filepath = Path::new(&file_selection); 17 | let selected_todo = select_todo(&todor, filepath)?; 18 | 19 | let todo_ind = match selected_todo { 20 | Some(todo_ind) => todo_ind, 21 | None => continue, 22 | }; 23 | 24 | todor 25 | .remove_todo(filepath, todo_ind) 26 | .unwrap_or_else(|err| warn!("{}", err)); 27 | println!("Comment removed"); 28 | } 29 | } 30 | 31 | fn select_file(todor: &TodoR) -> Option { 32 | let option_quit = format!("{}", Red.paint("QUIT")); 33 | let mut tracked_files = todor.get_tracked_files(); 34 | tracked_files.push(&option_quit); 35 | // IMPR: Cache tracked_files for when you go back 36 | 37 | let mut file_selector = Select::new(); 38 | file_selector 39 | .with_prompt("Pick a file to delete comment") 40 | .items(&tracked_files) 41 | .default(0); 42 | 43 | let file_ind = file_selector.interact().unwrap(); 44 | if file_ind + 1 == tracked_files.len() { 45 | return None; 46 | } 47 | 48 | Some(tracked_files[file_ind].to_string()) 49 | } 50 | 51 | fn select_todo(todor: &TodoR, filepath: &Path) -> Result, Error> { 52 | let mut todos_buf: Vec = Vec::new(); 53 | todor.write_todos_from_file(filepath, &mut todos_buf)?; 54 | 55 | let todos_string = String::from_utf8_lossy(&todos_buf); 56 | let mut todos_lines = todos_string.lines(); 57 | let styled_filename = todos_lines.next().unwrap(); 58 | 59 | let option_back = format!("{}", Red.paint("BACK")); 60 | let mut todos_items: Vec<&str> = todos_lines.collect(); 61 | todos_items.push(&option_back); 62 | 63 | let mut todo_selector = Select::new(); 64 | todo_selector 65 | .with_prompt(styled_filename) 66 | .items(&todos_items) 67 | .default(0); 68 | 69 | let todo_ind = todo_selector.interact().unwrap(); 70 | if todo_ind + 1 == todos_items.len() { 71 | return Ok(None); 72 | } 73 | 74 | Ok(Some(todo_ind)) 75 | } 76 | -------------------------------------------------------------------------------- /src/bin/todor/walk.rs: -------------------------------------------------------------------------------- 1 | use config::FileFormat; 2 | use failure::{format_err, Error}; 3 | use ignore::overrides::OverrideBuilder; 4 | use ignore::{Walk, WalkBuilder}; 5 | use log::{debug, info}; 6 | use std::env::current_dir; 7 | use std::path::{self, Path, PathBuf}; 8 | 9 | use todo_r::TodoRBuilder; 10 | 11 | /// Recurses down and try to find either .git or .todor as the root folder. 12 | /// Ignore builder should be initialized relative to current_dir(). 13 | /// 14 | /// Returns an iterator that iterates over all the tracked files. 15 | /// Measures are taken to make sure paths are returned in a nice relative path format. 16 | pub fn build_walker( 17 | todor_builder: &mut TodoRBuilder, 18 | mut ignore_builder: OverrideBuilder, 19 | ) -> Result { 20 | info!("Looking for .git or .todor to use as workspace root..."); 21 | 22 | let mut curr_dir = current_dir()?; 23 | // let mut ignore_builder = OverrideBuilder::new(&curr_dir); 24 | curr_dir.push(".todor"); 25 | let mut relative_path = PathBuf::from("."); 26 | let mut walk_builder = WalkBuilder::new(&relative_path); 27 | let mut found_walker_root = false; 28 | 29 | for abs_path in curr_dir.ancestors() { 30 | // ignore previous directory to not get repeated equivalent paths 31 | let ignore_string = get_ignore_string(abs_path, &relative_path)?; 32 | debug!("adding {} in walker override", &ignore_string); 33 | ignore_builder.add(&ignore_string).unwrap(); 34 | 35 | // check for .todor 36 | let todor_path = abs_path.with_file_name(".todor"); 37 | if todor_path.exists() { 38 | found_walker_root = true; 39 | info!("Found workspace root: '{}'", todor_path.display()); 40 | info!("Applying config file '{}'...", todor_path.display()); 41 | 42 | // check for empty file before adding 43 | if todor_path.metadata().unwrap().len() > 2 { 44 | todor_builder.add_config_file_with_format(todor_path, FileFormat::Hjson)?; 45 | } 46 | break; 47 | } 48 | 49 | // check for .git 50 | let git_path = abs_path.with_file_name(".git"); 51 | if git_path.exists() { 52 | found_walker_root = true; 53 | info!("Found workspace root: '{}'", git_path.display()); 54 | break; 55 | } 56 | 57 | relative_path.push(".."); 58 | walk_builder.add(&relative_path); 59 | } 60 | 61 | if !found_walker_root { 62 | return Err(format_err!( 63 | "No input files provided and no git repo or todor workspace found" 64 | )); 65 | } 66 | 67 | walk_builder 68 | .overrides(ignore_builder.build()?) 69 | .sort_by_file_name(std::ffi::OsStr::cmp) 70 | .add_custom_ignore_filename(".todorignore") 71 | .parents(false); 72 | 73 | Ok(walk_builder.build()) 74 | } 75 | 76 | /// Gets the ignore string for ignore::overrides::OverrideBuilder to use. 77 | /// Uses the fact that the file_name in abs_path is the previous directory. 78 | fn get_ignore_string(abs_path: &Path, rel_path: &Path) -> Result { 79 | let ignore_path = 80 | rel_path 81 | .strip_prefix(".") 82 | .unwrap() 83 | .with_file_name(abs_path.file_name().ok_or_else(|| { 84 | format_err!("No input files provided and no git repo or todor workspace found") 85 | })?); 86 | 87 | let ignore_path_str = ignore_path.to_str().ok_or_else(|| { 88 | format_err!( 89 | "Path `{}` contains invalid Unicode and cannot be processed", 90 | ignore_path.to_string_lossy() 91 | ) 92 | })?; 93 | 94 | let ignore_string = if path::MAIN_SEPARATOR != '/' { 95 | format!( 96 | "!{}", 97 | ignore_path_str.replace(&path::MAIN_SEPARATOR.to_string(), "/") 98 | ) 99 | } else { 100 | format!("!{}", ignore_path_str) 101 | }; 102 | 103 | Ok(ignore_string) 104 | } 105 | -------------------------------------------------------------------------------- /src/comments.rs: -------------------------------------------------------------------------------- 1 | // Module for the structs that hold the comment types 2 | 3 | use regex::escape; 4 | use serde::{Deserialize, Deserializer}; 5 | 6 | /// An enum for custom comment types. 7 | /// 8 | /// There are two types of comments: 9 | /// SingleLine: for single line comments like `// comment` 10 | /// Block: for block comments like `/* comment */` 11 | /// 12 | #[derive(Debug, Clone, Deserialize)] 13 | #[serde(untagged)] 14 | pub enum CommentType { 15 | SingleLine(SingleLineComment), 16 | Block(BlockComment), 17 | } 18 | 19 | impl CommentType { 20 | /// Creates new single-line comment type 21 | pub fn new_single(prefix: &str) -> CommentType { 22 | SingleLineComment::new(prefix).into() 23 | } 24 | 25 | /// Creates new block comment type 26 | pub fn new_block(prefix: &str, suffix: &str) -> CommentType { 27 | BlockComment::new(prefix, suffix).into() 28 | } 29 | 30 | /// Returns prefix token for comment. 31 | pub fn prefix(&self) -> &str { 32 | match self { 33 | CommentType::SingleLine(c) => &c.prefix, 34 | CommentType::Block(c) => &c.prefix, 35 | } 36 | } 37 | 38 | /// Returns suffix token for comment. 39 | pub fn suffix(&self) -> &str { 40 | match self { 41 | CommentType::SingleLine(_c) => "$", 42 | CommentType::Block(c) => &c.suffix, 43 | } 44 | } 45 | } 46 | 47 | /// Stores a single-line comment type. 48 | /// This holds the prefix for single-lines comments. 49 | /// For Rust comments it should hold `//`. 50 | #[derive(Debug, Clone, Deserialize)] 51 | pub struct SingleLineComment { 52 | #[serde(rename = "single")] 53 | #[serde(deserialize_with = "escape_deserialize")] 54 | prefix: String, 55 | } 56 | 57 | impl SingleLineComment { 58 | pub fn new(prefix: &str) -> SingleLineComment { 59 | SingleLineComment { 60 | prefix: escape(prefix), 61 | } 62 | } 63 | } 64 | 65 | impl Into for SingleLineComment { 66 | fn into(self) -> CommentType { 67 | CommentType::SingleLine(self) 68 | } 69 | } 70 | 71 | /// Stores a block comment type. 72 | /// This holds the prefix and suffix for block comments. 73 | /// For Rust comments it should hold `/*` and `*/`. 74 | #[derive(Debug, Clone, Deserialize)] 75 | pub struct BlockComment { 76 | #[serde(deserialize_with = "escape_deserialize")] 77 | prefix: String, 78 | #[serde(deserialize_with = "escape_deserialize")] 79 | suffix: String, 80 | } 81 | 82 | impl BlockComment { 83 | pub fn new(prefix: &str, suffix: &str) -> BlockComment { 84 | BlockComment { 85 | prefix: escape(prefix), 86 | suffix: escape(suffix), 87 | } 88 | } 89 | } 90 | 91 | impl Into for BlockComment { 92 | fn into(self) -> CommentType { 93 | CommentType::Block(self) 94 | } 95 | } 96 | 97 | fn escape_deserialize<'de, D>(deserializer: D) -> Result 98 | where 99 | D: Deserializer<'de>, 100 | { 101 | let s: String = Deserialize::deserialize(deserializer)?; 102 | Ok(escape(&s)) 103 | } 104 | 105 | /// Struct for storing a collection of CommentType enums that correspond to a specifix content type. 106 | /// It behaves as a wrapper for Vec. 107 | #[derive(Debug, Default, Clone, Deserialize)] 108 | pub struct CommentTypes { 109 | comment_types: Vec, 110 | } 111 | 112 | impl CommentTypes { 113 | /// Creates new CommentTypes struct. 114 | pub fn new() -> CommentTypes { 115 | CommentTypes { 116 | ..Default::default() 117 | } 118 | } 119 | 120 | /// Adds a single-line comment type with the provided prefix. 121 | /// For Rust single-line comments you might use `CommentTypes::new().add_single("//")` 122 | pub fn add_single(mut self, prefix: &str) -> Self { 123 | self.comment_types.push(CommentType::new_single(prefix)); 124 | self 125 | } 126 | 127 | /// Adds a block comment type with the provided prefix and suffix. 128 | /// For Rust block comments you might use `CommentTypes::new().add_block("/*", "*/")` 129 | pub fn add_block(mut self, prefix: &str, suffix: &str) -> Self { 130 | self.comment_types 131 | .push(CommentType::new_block(prefix, suffix)); 132 | self 133 | } 134 | 135 | /// Returns an iterator over all of the comment types in the struct. 136 | pub fn iter(&self) -> std::slice::Iter { 137 | self.into_iter() 138 | } 139 | } 140 | 141 | impl IntoIterator for CommentTypes { 142 | type Item = CommentType; 143 | type IntoIter = std::vec::IntoIter; 144 | 145 | fn into_iter(self) -> std::vec::IntoIter { 146 | self.comment_types.into_iter() 147 | } 148 | } 149 | 150 | impl<'a> IntoIterator for &'a CommentTypes { 151 | type Item = &'a CommentType; 152 | type IntoIter = std::slice::Iter<'a, CommentType>; 153 | 154 | fn into_iter(self) -> std::slice::Iter<'a, CommentType> { 155 | self.comment_types.iter() 156 | } 157 | } 158 | 159 | impl From> for CommentTypes { 160 | fn from(comment_types: Vec) -> CommentTypes { 161 | CommentTypes { comment_types } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/configs.rs: -------------------------------------------------------------------------------- 1 | use crate::display::TodoRStyles; 2 | use ansi_term::Color; 3 | use ansi_term::Style; 4 | use failure::Error; 5 | use fnv::FnvHashMap; 6 | use serde::Deserialize; 7 | 8 | use crate::comments::{CommentType, CommentTypes}; 9 | use crate::errors::TodoRError::InvalidConfigFile; 10 | 11 | /// Comments configuration as read from the config file 12 | #[derive(Debug, Default, Clone, Deserialize)] 13 | pub(crate) struct CommentsConfig { 14 | ext: Option, 15 | exts: Option>, 16 | types: Vec, 17 | } 18 | 19 | impl CommentsConfig { 20 | /// Consume the CommentsConfig type and return its parts 21 | pub fn break_apart(self) -> (Option, Option>, CommentTypes) { 22 | (self.ext, self.exts, self.types.into()) 23 | } 24 | } 25 | 26 | /// Style as read from the config file 27 | #[derive(Debug, Clone, Deserialize)] 28 | #[serde(untagged)] 29 | enum StyleConfig { 30 | Named(String), 31 | Fixed(u8), 32 | } 33 | 34 | impl StyleConfig { 35 | /// Converts StyleConfig into ansi_term::Style type 36 | pub fn into_style(self) -> Result { 37 | let style = match self { 38 | StyleConfig::Named(s) => { 39 | let mut style_parts = s.rsplit('_'); 40 | 41 | let color = style_parts.next().unwrap(); 42 | let mut style_from_string = parse_style_color(color)?; 43 | 44 | for modifier in style_parts { 45 | style_from_string = parse_style_modifier(style_from_string, modifier)?; 46 | } 47 | 48 | style_from_string 49 | } 50 | StyleConfig::Fixed(n) => Style::from(Color::Fixed(n)), 51 | }; 52 | 53 | Ok(style) 54 | } 55 | } 56 | 57 | /// Parses color str to get a color Style 58 | fn parse_style_color(color: &str) -> Result { 59 | if let Ok(n) = color.parse::() { 60 | return Ok(Style::from(Color::Fixed(n))); 61 | } 62 | 63 | let colored_style = match color.to_uppercase().as_str() { 64 | "BLACK" => Style::from(Color::Black), 65 | "RED" => Style::from(Color::Red), 66 | "GREEN" => Style::from(Color::Green), 67 | "YELLOW" => Style::from(Color::Yellow), 68 | "BLUE" => Style::from(Color::Blue), 69 | "PURPLE" | "MAGENTA" => Style::from(Color::Purple), 70 | "CYAN" => Style::from(Color::Cyan), 71 | "WHITE" => Style::from(Color::White), 72 | "" => Style::new(), 73 | _ => { 74 | return Err(InvalidConfigFile { 75 | message: format!("'{}' is not a valid ANSI color.", color), 76 | } 77 | .into()); 78 | } 79 | }; 80 | 81 | Ok(colored_style) 82 | } 83 | 84 | /// Parses modifier str to modify a Style and return the result 85 | fn parse_style_modifier(unmodified_style: Style, modifier: &str) -> Result { 86 | let style = match modifier.to_uppercase().as_str() { 87 | "BOLD" | "B" => unmodified_style.bold(), 88 | "ITALIC" | "I" | "IT" => unmodified_style.italic(), 89 | "UNDERLINE" | "U" => unmodified_style.underline(), 90 | _ => { 91 | return Err(InvalidConfigFile { 92 | message: format!( 93 | "'{}' is not a valid ANSI style modifier. Try using 'b', 'i', or 'u'", 94 | modifier 95 | ), 96 | } 97 | .into()); 98 | } 99 | }; 100 | 101 | Ok(style) 102 | } 103 | 104 | /// Styles as read from the config file 105 | #[derive(Debug, Clone, Deserialize)] 106 | pub(crate) struct StylesConfig { 107 | filepath: StyleConfig, 108 | tag: StyleConfig, 109 | content: StyleConfig, 110 | line_number: StyleConfig, 111 | user: StyleConfig, 112 | tags: FnvHashMap, 113 | } 114 | 115 | impl Default for StylesConfig { 116 | fn default() -> StylesConfig { 117 | StylesConfig { 118 | filepath: StyleConfig::Named("U_WHITE".to_string()), 119 | tag: StyleConfig::Named("GREEN".to_string()), 120 | content: StyleConfig::Named("CYAN".to_string()), 121 | line_number: StyleConfig::Fixed(8), 122 | user: StyleConfig::Fixed(8), 123 | tags: FnvHashMap::default(), 124 | } 125 | } 126 | } 127 | 128 | impl StylesConfig { 129 | /// Converts StyleConfig into TodoRStyles type 130 | pub fn into_todo_r_styles(mut self) -> Result { 131 | let mut styles = TodoRStyles::new( 132 | self.filepath.into_style()?, 133 | self.line_number.into_style()?, 134 | self.user.into_style()?, 135 | self.content.into_style()?, 136 | self.tag.into_style()?, 137 | ); 138 | 139 | for (tag, style_conf) in self.tags.drain() { 140 | styles = styles.add_tag_style(&tag, style_conf.into_style()?); 141 | } 142 | 143 | Ok(styles) 144 | } 145 | } 146 | 147 | /// TodoR configuration settings as read from the config file 148 | #[derive(Debug, Default, Clone, Deserialize)] 149 | pub(crate) struct TodoRConfigFileSerial { 150 | #[serde(default)] 151 | pub verbose: bool, 152 | #[serde(default)] 153 | pub tags: Vec, 154 | #[serde(default)] 155 | pub ignore: Vec, 156 | #[serde(default)] 157 | pub default_ext: String, 158 | #[serde(default)] 159 | pub default_comments: Vec, 160 | #[serde(default)] 161 | pub comments: Vec, 162 | #[serde(default)] 163 | pub styles: StylesConfig, 164 | } 165 | -------------------------------------------------------------------------------- /src/custom_tags.rs: -------------------------------------------------------------------------------- 1 | // Module for creating regexs for custom tags 2 | 3 | use regex::Regex; 4 | use std::borrow::Borrow; 5 | 6 | use crate::comments::CommentType; 7 | 8 | /// Returns Regex that matches TODO comment. 9 | /// 10 | /// Optionally, you can specify a user tag in one of two ways: 11 | /// - using perenthesis after the TODO tag such as `// TODO(user): content`. 12 | /// - using @ in the content like this `// TODO: tag @user in this` 13 | /// 14 | /// 15 | /// The capture groups in the Regex are: 16 | /// 1. TODO tag 17 | /// 2. Optional user tag 18 | /// 3. Content 19 | /// 20 | pub(crate) fn get_regex_for_comment( 21 | custom_tags: &[S], 22 | comment_type: &CommentType, 23 | ) -> Result 24 | where 25 | S: Borrow, 26 | { 27 | let tags_string = custom_tags.join("|"); 28 | 29 | Regex::new(&format!( 30 | r"(?i)^\s*{}\s*({})\s?{}[:\s]?(?:\s+{})?\s*{}", // whitespace and optional colon 31 | comment_type.prefix(), // comment prefix token 32 | tags_string, // custom tags 33 | r"(?:\(@?(\S+)\))?", // optional user tag in ()`s 34 | r"(.*?)", // content 35 | comment_type.suffix(), // comment suffix token 36 | )) 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::*; 42 | use crate::comments::CommentType; 43 | 44 | fn test_regex(content: &str, exp_result: Option<&str>, comment_type: &CommentType) { 45 | let re = get_regex_for_comment(&["TODO", "FIXME"], comment_type).unwrap(); 46 | let todo_content = re.captures(content); 47 | match todo_content { 48 | Some(todo_content) => { 49 | assert_eq!(exp_result, todo_content.get(3).map(|s| s.as_str())); 50 | assert_eq!(None, todo_content.get(2).or(todo_content.get(4))); 51 | } 52 | None => assert_eq!(exp_result, None), 53 | }; 54 | } 55 | 56 | fn test_paren_user_regex( 57 | content: &str, 58 | exp_content: Option<&str>, 59 | exp_user: Option<&str>, 60 | comment_type: &CommentType, 61 | ) { 62 | let re = get_regex_for_comment(&["TODO", "FIXME"], comment_type).unwrap(); 63 | let todo_content = re.captures(content); 64 | match todo_content { 65 | Some(todo_content) => { 66 | assert_eq!(exp_content, todo_content.get(3).map(|s| s.as_str())); 67 | assert_eq!(exp_user, todo_content.get(2).map(|s| s.as_str())); 68 | } 69 | None => { 70 | assert_eq!(exp_content, None); 71 | assert_eq!(exp_user, None); 72 | } 73 | }; 74 | } 75 | 76 | #[test] 77 | fn regex_whitespace() { 78 | test_regex( 79 | "\t\t\t\t // TODO: item \t", 80 | Some("item"), 81 | &CommentType::new_single("//"), 82 | ); 83 | } 84 | 85 | #[test] 86 | fn regex_todo_in_comment() { 87 | test_regex( 88 | "// TODO: item // TODO: item \t", 89 | Some("item // TODO: item"), 90 | &CommentType::new_single("//"), 91 | ); 92 | } 93 | 94 | #[test] 95 | fn regex_optional_colon() { 96 | test_regex( 97 | "// TODO item // TODO: item \t", 98 | Some("item // TODO: item"), 99 | &CommentType::new_single("//"), 100 | ); 101 | } 102 | 103 | #[test] 104 | fn regex_case_insensitive() { 105 | test_regex( 106 | "// tODo: case ", 107 | Some("case"), 108 | &CommentType::new_single("//"), 109 | ); 110 | } 111 | 112 | #[test] 113 | fn regex_fixme() { 114 | test_regex( 115 | "\t\t\t\t // fixMe: item for fix \t", 116 | Some("item for fix"), 117 | &CommentType::new_single("//"), 118 | ); 119 | } 120 | 121 | #[test] 122 | fn regex_todop() { 123 | test_regex("// todop: nope ", None, &CommentType::new_single("//")); 124 | } 125 | 126 | #[test] 127 | fn regex_todf() { 128 | test_regex("// todf: nope ", None, &CommentType::new_single("//")); 129 | } 130 | 131 | #[test] 132 | fn regex_todofixme() { 133 | test_regex("// todofixme : nope ", None, &CommentType::new_single("//")); 134 | } 135 | 136 | #[test] 137 | fn regex_py_comment() { 138 | test_regex( 139 | "# todo: item \t ", 140 | Some("item"), 141 | &CommentType::new_single("#"), 142 | ); 143 | } 144 | 145 | #[test] 146 | fn regex_percent_comment() { 147 | test_regex( 148 | "% todo: item \t ", 149 | Some("item"), 150 | &CommentType::new_single("%"), 151 | ); 152 | } 153 | 154 | #[test] 155 | fn regex_ddash_comment() { 156 | test_regex( 157 | "-- todo: item \t ", 158 | Some("item"), 159 | &CommentType::new_single("--"), 160 | ); 161 | } 162 | 163 | #[test] 164 | fn regex_slashstar_comment() { 165 | test_regex( 166 | "/* todo: item \t */ \t ", 167 | Some("item"), 168 | &CommentType::new_block("/*", "*/"), 169 | ); 170 | } 171 | 172 | #[test] 173 | fn regex_slashstar_comment_double_prefix() { 174 | test_regex( 175 | "/* todo: item /* todo: decoy*/\t ", 176 | Some("item /* todo: decoy"), 177 | &CommentType::new_block("/*", "*/"), 178 | ); 179 | } 180 | 181 | #[test] 182 | fn regex_slashstar_comment_double_suffix() { 183 | test_regex( 184 | "/* todo: item */ \t other stuff */ ", 185 | Some("item"), 186 | &CommentType::new_block("/*", "*/"), 187 | ); 188 | } 189 | 190 | #[test] 191 | fn regex_comment_not_on_separate_line() { 192 | test_regex( 193 | "do_things(); // todo: item", 194 | None, 195 | &CommentType::new_single("//"), 196 | ); 197 | } 198 | 199 | #[test] 200 | fn regex_block_todo_before_function() { 201 | test_regex( 202 | "/* todo: item */ do_things();", 203 | Some("item"), 204 | &CommentType::new_block("/*", "*/"), 205 | ); 206 | } 207 | 208 | #[test] 209 | fn regex_no_todo_content_with_space() { 210 | test_regex("// TODO: ", Some(""), &CommentType::new_single("//")); 211 | } 212 | 213 | #[test] 214 | fn regex_no_todo_content() { 215 | test_regex("// TODO:", None, &CommentType::new_single("//")); 216 | } 217 | 218 | #[test] 219 | fn regex_basic_user() { 220 | test_paren_user_regex( 221 | "// TODO(userA): item", 222 | Some("item"), 223 | Some("userA"), 224 | &CommentType::new_single("//"), 225 | ); 226 | } 227 | 228 | #[test] 229 | fn regex_basic_user2() { 230 | test_paren_user_regex( 231 | "// TODO: item @userA", 232 | Some("item @userA"), 233 | None, 234 | &CommentType::new_single("//"), 235 | ); 236 | } 237 | 238 | #[test] 239 | fn regex_basic_user3() { 240 | test_paren_user_regex( 241 | "// TODO: @userA item", 242 | Some("@userA item"), 243 | None, 244 | &CommentType::new_single("//"), 245 | ); 246 | } 247 | 248 | #[test] 249 | fn regex_basic_user4() { 250 | test_paren_user_regex( 251 | "// TODO: item @userA item2", 252 | Some("item @userA item2"), 253 | None, 254 | &CommentType::new_single("//"), 255 | ); 256 | } 257 | 258 | #[test] 259 | fn regex_tricky_user() { 260 | test_paren_user_regex( 261 | "@ TODO: item @userA item2", 262 | Some("item @userA item2"), 263 | None, 264 | &CommentType::new_single("@"), 265 | ); 266 | } 267 | 268 | #[test] 269 | fn regex_user_twice() { 270 | test_paren_user_regex( 271 | "// TODO(user1): item @user2 item2", 272 | Some("item @user2 item2"), 273 | Some("user1"), 274 | &CommentType::new_single("//"), 275 | ); 276 | } 277 | 278 | #[test] 279 | fn regex_user_twice2() { 280 | test_paren_user_regex( 281 | "// TODO: item @user1 item2 @user2", 282 | Some("item @user1 item2 @user2"), 283 | None, 284 | &CommentType::new_single("//"), 285 | ); 286 | } 287 | 288 | #[test] 289 | fn regex_at_in_user() { 290 | test_paren_user_regex( 291 | "// TODO: item @user@web.com ", 292 | Some("item @user@web.com"), 293 | None, 294 | &CommentType::new_single("//"), 295 | ); 296 | } 297 | 298 | #[test] 299 | fn regex_user_block() { 300 | test_paren_user_regex( 301 | "/* TODO: item @user */", 302 | Some("item @user"), 303 | None, 304 | &CommentType::new_block("/*", "*/"), 305 | ); 306 | } 307 | 308 | #[test] 309 | fn regex_user_block2() { 310 | test_paren_user_regex( 311 | "/* TODO(user): item */", 312 | Some("item"), 313 | Some("user"), 314 | &CommentType::new_block("/*", "*/"), 315 | ); 316 | } 317 | 318 | #[test] 319 | fn regex_paren_in_user() { 320 | test_paren_user_regex( 321 | "// TODO(smiles:)): item \t ", 322 | Some("item"), 323 | Some("smiles:)"), 324 | &CommentType::new_single("//"), 325 | ); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/default_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": [ 3 | "todo", 4 | "fix", 5 | "fixme" 6 | ], 7 | 8 | "styles": { 9 | "filepath": "u_", 10 | "tag": "green", 11 | "tags": {}, 12 | "content": "cyan", 13 | "line_number": 8, 14 | "user": 8 15 | }, 16 | 17 | "default_ext": "sh", 18 | 19 | "default_comments": [ 20 | { 21 | "exts": [ 22 | "c", 23 | "h", 24 | "cpp", 25 | "cs", 26 | "go", 27 | "rs", 28 | "scala", 29 | "java", 30 | "js", 31 | "es", 32 | "es6", 33 | "ts", 34 | "tsx", 35 | "styl", 36 | "swift", 37 | "less", 38 | "scss", 39 | "sass", 40 | "m", 41 | "mm", 42 | "php" 43 | ], 44 | "types": [ 45 | { 46 | "single": "//" 47 | }, 48 | { 49 | "prefix": "/*", 50 | "suffix": "*/" 51 | } 52 | ] 53 | }, 54 | { 55 | "ext": "py", 56 | "types": [ 57 | { 58 | "single": "#" 59 | }, 60 | { 61 | "prefix": "\"\"\"", 62 | "suffix": "\"\"\"" 63 | } 64 | ] 65 | }, 66 | { 67 | "exts": [ 68 | "rb", 69 | "pl", 70 | "pm", 71 | "coffee", 72 | "yaml", 73 | "yml", 74 | "sh", 75 | "bash", 76 | "zsh", 77 | "gitignore" 78 | ], 79 | "types": [ 80 | { 81 | "single": "#" 82 | } 83 | ] 84 | }, 85 | { 86 | "ext": "tex", 87 | "types": [ 88 | { 89 | "single": "%" 90 | } 91 | ] 92 | }, 93 | { 94 | "ext": "hs", 95 | "types": [ 96 | { 97 | "single": "--" 98 | } 99 | ] 100 | }, 101 | { 102 | "exts": [ 103 | "html", 104 | "htm", 105 | "md" 106 | ], 107 | "types": [ 108 | { 109 | "prefix": "" 111 | } 112 | ] 113 | } 114 | ] 115 | } -------------------------------------------------------------------------------- /src/display.rs: -------------------------------------------------------------------------------- 1 | // Module for displaying output 2 | 3 | use ansi_term::Style; 4 | use failure::Error; 5 | use log::debug; 6 | use std::io::Write; 7 | 8 | use crate::maps::FallbackHashMap; 9 | use crate::todo::TodoFile; 10 | 11 | /// Struct for holding ansi color printing options 12 | #[derive(Debug, Clone)] 13 | pub struct TodoRStyles { 14 | pub filepath_style: Style, 15 | pub line_number_style: Style, 16 | pub user_style: Style, 17 | pub content_style: Style, 18 | tag_styles: FallbackHashMap, 19 | } 20 | 21 | impl TodoRStyles { 22 | /// Creates new StyleConfig 23 | pub fn new( 24 | filepath_style: Style, 25 | line_number_style: Style, 26 | user_style: Style, 27 | content_style: Style, 28 | default_tag_style: Style, 29 | ) -> TodoRStyles { 30 | TodoRStyles { 31 | filepath_style, 32 | line_number_style, 33 | user_style, 34 | content_style, 35 | tag_styles: FallbackHashMap::new(default_tag_style), 36 | } 37 | } 38 | 39 | /// Creates new StyleConfig with plaintext printing (no colors). 40 | pub fn no_style() -> TodoRStyles { 41 | TodoRStyles { 42 | filepath_style: Style::new(), 43 | line_number_style: Style::new(), 44 | user_style: Style::new(), 45 | content_style: Style::new(), 46 | tag_styles: FallbackHashMap::new(Style::new()), 47 | } 48 | } 49 | 50 | /// Adds style for printing given tag 51 | pub fn add_tag_style(mut self, tag: &str, style: Style) -> Self { 52 | self.tag_styles.insert(tag.to_uppercase(), style); 53 | self 54 | } 55 | 56 | /// Returns tag style for given tag. 57 | pub fn tag_style(&self, tag: &str) -> &Style { 58 | self.tag_styles.get(tag) 59 | } 60 | } 61 | 62 | /// Writes file path and a list of Todos to out_buffer. 63 | /// 64 | /// If no there are no `Todo`s that satisfy `pred` in `todo_file`, nothing is printed. 65 | pub fn write_file_todos( 66 | out_buffer: &mut impl Write, 67 | todo_file: &TodoFile, 68 | styles: &TodoRStyles, 69 | ) -> Result<(), Error> { 70 | let mut todos = todo_file.todos.iter().peekable(); 71 | if todos.peek().is_some() { 72 | writeln!( 73 | out_buffer, 74 | "{}", 75 | styles 76 | .filepath_style 77 | .paint(todo_file.filepath.to_string_lossy()) 78 | )?; 79 | 80 | for todo in todos { 81 | todo.write_style_string(out_buffer, styles)?; 82 | } 83 | } else { 84 | debug!( 85 | "No TODOs found in `{}`", 86 | todo_file.filepath.to_string_lossy() 87 | ) 88 | } 89 | 90 | Ok(()) 91 | } 92 | -------------------------------------------------------------------------------- /src/example_config.hjson: -------------------------------------------------------------------------------- 1 | { 2 | // tags to search for. These are case-insensitive 3 | "tags": [ 4 | "todo", 5 | "fix", 6 | "fixme" 7 | ], 8 | 9 | // Default extension fall-back 10 | "default_ext": "sh", 11 | 12 | // custom comment types 13 | "comments": [ 14 | { 15 | // extension for this comment type 16 | "ext": "py", 17 | "types": [ 18 | // single line comment definition 19 | { 20 | "single": "#" 21 | }, 22 | // block comment definition 23 | { 24 | "prefix": "\"\"\"", 25 | "suffix": "\"\"\"" 26 | } 27 | ] 28 | }, 29 | { 30 | // list of extensions for this comment type 31 | "exts": [ 32 | "rs", 33 | "c", 34 | "h", 35 | ], 36 | "types": [ 37 | { 38 | "single": "//" 39 | }, 40 | { 41 | "prefix": "/*", 42 | "suffix": "*/" 43 | } 44 | ] 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | // Module for printing TODOs in various formats 2 | 3 | use crate::TodoR; 4 | use failure::Error; 5 | use fnv::FnvHashMap; 6 | use serde_json; 7 | use std::fmt::Write as StringWrite; 8 | use std::io::{self, Write}; 9 | 10 | use crate::display::{write_file_todos, TodoRStyles}; 11 | 12 | // MAYB: add more output formats 13 | /// Enum holding the different supported output formats. 14 | pub enum ReportFormat { 15 | Json, 16 | JsonPretty, 17 | Markdown, 18 | UserMarkdown, 19 | Csv, 20 | Default, 21 | } 22 | 23 | impl TodoR { 24 | /// Writes TODOs in TodoR serialized in the JSON format 25 | fn write_json(&self, out_buffer: &mut impl Write) -> Result<(), Error> { 26 | serde_json::to_writer(out_buffer, &self)?; 27 | Ok(()) 28 | } 29 | 30 | /// Writes TODOs in TodoR serialized in a pretty JSON format 31 | fn write_pretty_json(&self, out_buffer: &mut impl Write) -> Result<(), Error> { 32 | serde_json::to_writer_pretty(out_buffer, &self)?; 33 | Ok(()) 34 | } 35 | 36 | /// Writes TODOs in TodoR serialized in a markdown table format. 37 | /// Tables are organized by TODO tag type. 38 | fn write_markdown(&self, out_buffer: &mut impl Write) -> Result<(), Error> { 39 | let mut tag_tables: FnvHashMap<&str, String> = FnvHashMap::default(); 40 | 41 | for ptodo in self.iter() { 42 | let todo = ptodo.todo; 43 | let table_string = tag_tables.entry(&todo.tag).or_insert_with(|| { 44 | format!( 45 | "### {}s\n| Filename | line | {} |\n|:---|:---:|:---|\n", 46 | todo.tag, todo.tag, 47 | ) 48 | }); 49 | 50 | writeln!( 51 | table_string, 52 | "| {} | {} | {} |", 53 | ptodo.file.display(), 54 | todo.line, 55 | todo.content, 56 | )?; 57 | } 58 | 59 | for table_strings in tag_tables.values() { 60 | writeln!(out_buffer, "{}", table_strings)?; 61 | } 62 | 63 | Ok(()) 64 | } 65 | 66 | /// Writes TODOs in TodoR serialized in a markdown table format. 67 | /// Tables are organized by TODO user. 68 | fn write_user_markdown(&self, out_buffer: &mut impl Write) -> Result<(), Error> { 69 | let mut user_tables: FnvHashMap<&str, String> = FnvHashMap::default(); 70 | let mut untagged_todos_string = "".to_string(); 71 | 72 | for ptodo in self.iter() { 73 | let todo = ptodo.todo; 74 | let users = todo.users(); 75 | 76 | if users.is_empty() { 77 | writeln!( 78 | untagged_todos_string, 79 | "| {} | {} | {} | {} |", 80 | ptodo.file.display(), 81 | todo.line, 82 | todo.tag, 83 | todo.content, 84 | )?; 85 | } else { 86 | for user in users { 87 | let table_string = user_tables.entry(user).or_insert_with(|| { 88 | format!( 89 | "### {}\n| Filename | line | type | content |\n|:---|:---:|:---:|:---|\n", 90 | user, 91 | ) 92 | }); 93 | 94 | writeln!( 95 | table_string, 96 | "| {} | {} | {} | {} |", 97 | ptodo.file.display(), 98 | todo.line, 99 | todo.tag, 100 | todo.content, 101 | )?; 102 | } 103 | } 104 | } 105 | 106 | for table_strings in user_tables.values() { 107 | writeln!(out_buffer, "{}", table_strings)?; 108 | } 109 | 110 | if !untagged_todos_string.is_empty() { 111 | writeln!( 112 | out_buffer, 113 | "### Untagged\n| Filename | line | type | content |\n|:---|:---:|:---:|:---|\n{}", 114 | untagged_todos_string, 115 | )?; 116 | } 117 | 118 | Ok(()) 119 | } 120 | 121 | /// Writes TODOs in TodoR serialized in a csv format 122 | fn write_csv(&self, out_buffer: &mut impl Write) -> Result<(), Error> { 123 | writeln!(out_buffer, "Filename, line, type, content")?; 124 | 125 | for ptodo in self.iter() { 126 | let todo = ptodo.todo; 127 | writeln!( 128 | out_buffer, 129 | "{}, {}, {}, {}", 130 | ptodo.file.display(), 131 | todo.line, 132 | todo.tag, 133 | todo.content, 134 | )?; 135 | } 136 | 137 | Ok(()) 138 | } 139 | 140 | /// Writes TODOs to out_buffer with no styles. 141 | fn write_default_todos(&self, out_buffer: &mut impl Write) -> Result<(), Error> { 142 | let styles = TodoRStyles::no_style(); 143 | 144 | for todo_file in &self.todo_files { 145 | write_file_todos(out_buffer, &todo_file, &styles)?; 146 | } 147 | 148 | Ok(()) 149 | } 150 | 151 | /// Prints formatted TODOs to stdout. 152 | pub fn print_formatted_todos(&self, format: &ReportFormat) -> Result<(), Error> { 153 | // lock stdout to print faster 154 | let stdout = io::stdout(); 155 | let lock = stdout.lock(); 156 | let mut out_buffer = io::BufWriter::new(lock); 157 | 158 | self.write_formatted_todos(&mut out_buffer, format) 159 | } 160 | 161 | /// Writes formatted TODOs to out_buffer in the format provided by `report_format` 162 | pub fn write_formatted_todos( 163 | &self, 164 | out_buffer: &mut impl Write, 165 | out_format: &ReportFormat, 166 | ) -> Result<(), Error> { 167 | let formatted_write = match out_format { 168 | ReportFormat::Json => TodoR::write_json, 169 | ReportFormat::JsonPretty => TodoR::write_pretty_json, 170 | ReportFormat::Markdown => TodoR::write_markdown, 171 | ReportFormat::UserMarkdown => TodoR::write_user_markdown, 172 | ReportFormat::Csv => TodoR::write_csv, 173 | ReportFormat::Default => TodoR::write_default_todos, 174 | }; 175 | 176 | formatted_write(self, out_buffer) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod comments; 2 | mod configs; 3 | mod custom_tags; 4 | mod display; 5 | pub mod format; 6 | mod maps; 7 | mod parser; 8 | mod remover; 9 | pub mod todo; 10 | 11 | pub mod errors { 12 | use failure::Fail; 13 | 14 | /// Custom Errors for TodoR 15 | #[derive(Debug, Fail)] 16 | pub enum TodoRError { 17 | /// Error for when provided file path is a directory. 18 | #[fail(display = "'{}' is a directory", filepath)] 19 | InputIsDir { filepath: String }, 20 | /// Error for when provided file cannot be accessed for some reason. 21 | #[fail(display = "cannot access '{}'", filepath)] 22 | CannotAccessFile { filepath: String }, 23 | /// Error for when provided file extension is not supported. 24 | #[fail(display = "'{}' is an invalid extension", ext)] 25 | InvalidExtension { ext: String }, 26 | /// Error for when provided filepath for modification is not tracked. 27 | #[fail(display = "'{}' is not a tracked file", filepath)] 28 | FileNotTracked { filepath: String }, 29 | /// Error for when provided TODO line is not found. 30 | #[fail(display = "TODO comment not found in line {}", line)] 31 | TodoNotFound { line: usize }, 32 | /// Error for when provided default file extension is not supported. 33 | #[fail(display = "'{}' is an invalid default extension", ext)] 34 | InvalidDefaultExtension { ext: String }, 35 | /// Error for invalid config file. 36 | #[fail(display = "invalid config file: {}", message)] 37 | InvalidConfigFile { message: String }, 38 | /// Error for invalid ignore path. 39 | #[fail(display = "invalid ignore path: {}", message)] 40 | InvalidIgnorePath { message: String }, 41 | /// Error for unsupported output format. 42 | #[fail(display = "invalid output format: {}", message)] 43 | InvalidOutputFormat { message: String }, 44 | } 45 | } 46 | 47 | use failure::Error; 48 | use log::debug; 49 | use serde::ser::{Serialize, SerializeSeq, Serializer}; 50 | use std::borrow::Cow; 51 | use std::fs::File; 52 | use std::io::{self, BufReader, Cursor, Write}; 53 | use std::path::Path; 54 | 55 | use crate::comments::CommentTypes; 56 | use crate::configs::TodoRConfigFileSerial; 57 | use crate::display::{write_file_todos, TodoRStyles}; 58 | use crate::errors::TodoRError; 59 | use crate::maps::CommentRegexMultiMap; 60 | use crate::parser::{parse_content, parse_content_with_filter}; 61 | use crate::todo::{PathedTodo, Todo, TodoFile}; 62 | 63 | static DEFAULT_CONFIG: &str = include_str!("default_config.json"); 64 | static EXAMPLE_CONFIG: &str = include_str!("example_config.hjson"); 65 | 66 | /// A builder to create a TodoR with a custom configuration. 67 | /// Customization occurs in two forms: using manual functions and adding config files. 68 | /// 69 | /// ### Functions 70 | /// Use functions such as `add_tag()` to add to the config. 71 | /// Functions that have `override` in them will fully override settings from config files. 72 | /// 73 | /// ### Config files 74 | /// Config files are added using `add_config_file()`. 75 | /// 76 | /// For an example config file, use `todo_r::write_example_config()`. 77 | #[derive(Debug, Default, Clone)] 78 | pub struct TodoRBuilder { 79 | added_tags: Vec, 80 | override_tags: Option>, 81 | override_default_ext: Option, 82 | override_styles: Option, 83 | // Config from files. Parameters with override_ override inner_config. 84 | inner_config: config::Config, 85 | } 86 | 87 | impl TodoRBuilder { 88 | /// Creates TodoRBuilder using the default configuration. 89 | pub fn new() -> TodoRBuilder { 90 | let mut builder = TodoRBuilder::default(); 91 | builder 92 | .inner_config 93 | .merge(config::File::from_str( 94 | DEFAULT_CONFIG, 95 | config::FileFormat::Json, 96 | )) 97 | .unwrap(); 98 | 99 | builder 100 | } 101 | 102 | /// Creates TodoRBuilder with no configuration. 103 | pub fn with_no_config() -> TodoRBuilder { 104 | TodoRBuilder::default() 105 | } 106 | 107 | /// Consumes self and builds TodoR. 108 | pub fn build(self) -> Result { 109 | let config_struct: TodoRConfigFileSerial = 110 | self.inner_config 111 | .try_into() 112 | .map_err(|err| TodoRError::InvalidConfigFile { 113 | message: err.to_string(), 114 | })?; 115 | debug!("configuration successfully loaded"); 116 | 117 | let mut tags = self 118 | .override_tags 119 | .unwrap_or_else(|| config_struct.tags.to_owned()); 120 | let default_ext = self 121 | .override_default_ext 122 | .unwrap_or_else(|| config_struct.default_ext.to_owned()); 123 | tags.append(&mut self.added_tags.clone()); 124 | 125 | let config_styles = config_struct.styles; 126 | let styles = self 127 | .override_styles 128 | .unwrap_or(config_styles.into_todo_r_styles()?); 129 | 130 | let mut ext_to_regexs = CommentRegexMultiMap::new(CommentTypes::new().add_single("#")); 131 | // Iter over default comment types 132 | for comment_config in config_struct 133 | .default_comments 134 | .into_iter() 135 | // Iter over config comment types 136 | .chain(config_struct.comments.into_iter()) 137 | { 138 | let (config_ext, config_exts, comment_types) = comment_config.break_apart(); 139 | let exts = config_exts.into_iter().flatten().chain(config_ext); 140 | 141 | // Add all extensions into ext_to_regexs 142 | ext_to_regexs.insert_keys(exts, comment_types); 143 | } 144 | 145 | ext_to_regexs 146 | .reset_fallback_key(&default_ext) 147 | .ok_or(TodoRError::InvalidDefaultExtension { ext: default_ext })?; 148 | 149 | let config = TodoRConfig { 150 | tags, 151 | styles, 152 | ext_to_regexs, 153 | }; 154 | 155 | debug!("todor parser built: {:?}", config); 156 | 157 | Ok(TodoR::with_config(config)) 158 | } 159 | 160 | /// Adds config file for TodoR. 161 | pub fn add_config_file(&mut self, config_path: impl AsRef) -> Result<&mut Self, Error> { 162 | self.inner_config 163 | .merge(config::File::from(config_path.as_ref()))?; 164 | Ok(self) 165 | } 166 | 167 | /// Adds config file for TodoR that is in the provided format. 168 | pub fn add_config_file_with_format( 169 | &mut self, 170 | config_path: impl AsRef, 171 | format: config::FileFormat, 172 | ) -> Result<&mut Self, Error> { 173 | let mut merge_file = config::File::from(config_path.as_ref()); 174 | merge_file = merge_file.format(format); 175 | self.inner_config.merge(merge_file)?; 176 | Ok(self) 177 | } 178 | 179 | /// Adds tag for TodoR to look for without overriding tags from config files. 180 | pub fn add_tag<'a, S: Into>>(&mut self, tag: S) -> &mut Self { 181 | self.added_tags.push(tag.into().into_owned()); 182 | self 183 | } 184 | 185 | /// Adds tags for TodoR to look for without overriding tags from config files. 186 | pub fn add_tags<'a, I, S>(&mut self, tags: I) -> &mut Self 187 | where 188 | I: IntoIterator, 189 | S: Into>, 190 | { 191 | self.added_tags 192 | .extend(tags.into_iter().map(|s| s.into().into_owned())); 193 | self 194 | } 195 | 196 | /// Adds tag for TodoR to look for. This overrides tags from config files. 197 | pub fn add_override_tag<'t, S: Into>>(&mut self, tag: S) -> &mut Self { 198 | self.override_tags 199 | .get_or_insert_with(Vec::new) 200 | .push(tag.into().into_owned()); 201 | self 202 | } 203 | 204 | /// Adds tags for TodoR to look for. This overrides tags from config files. 205 | pub fn add_override_tags<'t, I, S>(&mut self, tags: I) -> &mut Self 206 | where 207 | I: IntoIterator, 208 | S: Into>, 209 | { 210 | { 211 | let tws = self.override_tags.get_or_insert_with(Vec::new); 212 | 213 | tws.extend(tags.into_iter().map(|s| s.into().into_owned())) 214 | } 215 | self 216 | } 217 | 218 | /// Sets the terminal output of TodoR to be with no styles. 219 | pub fn set_no_style(&mut self) -> &mut Self { 220 | self.override_styles = Some(TodoRStyles::no_style()); 221 | self 222 | } 223 | 224 | /// Sets the default fall-back extension for comments. 225 | /// 226 | /// For instance if you want to parse unknown extensions using C style comments, 227 | /// use `builder.set_default_ext("c")`. 228 | pub fn set_default_ext<'a, S: Into>>(&mut self, ext: S) -> Result<(), Error> { 229 | self.override_default_ext = Some(ext.into().into_owned()); 230 | 231 | Ok(()) 232 | } 233 | } 234 | 235 | /// Writes the default configuration file to out_buffer. 236 | pub fn write_example_config(out_buffer: &mut impl Write) -> Result<(), Error> { 237 | out_buffer.write_all(EXAMPLE_CONFIG.as_bytes())?; 238 | Ok(()) 239 | } 240 | 241 | /// Configuration for `TodoR`. 242 | /// 243 | /// `tags` gives a list of the TODO terms to search for. 244 | #[derive(Debug, Clone)] 245 | struct TodoRConfig { 246 | tags: Vec, 247 | styles: TodoRStyles, 248 | ext_to_regexs: CommentRegexMultiMap, 249 | } 250 | 251 | /// Parser for finding TODOs in comments and storing them on a per-file basis. 252 | #[derive(Debug, Clone)] 253 | pub struct TodoR { 254 | config: TodoRConfig, 255 | todo_files: Vec, 256 | } 257 | 258 | impl<'a> Default for TodoR { 259 | fn default() -> TodoR { 260 | TodoRBuilder::new().build().unwrap() 261 | } 262 | } 263 | 264 | impl TodoR { 265 | /// Creates new TodoR that looks for provided keywords. 266 | pub fn new() -> TodoR { 267 | TodoR::default() 268 | } 269 | 270 | pub fn with_tags<'t, I, S>(tags: I) -> TodoR 271 | where 272 | I: IntoIterator, 273 | S: Into>, 274 | { 275 | let mut builder = TodoRBuilder::new(); 276 | builder.add_override_tags(tags); 277 | builder.build().unwrap() 278 | } 279 | 280 | /// Creates new TodoR using given configuration. 281 | fn with_config(config: TodoRConfig) -> TodoR { 282 | TodoR { 283 | config, 284 | todo_files: Vec::new(), 285 | } 286 | } 287 | 288 | /// Returns the number of files currently tracked by TodoR 289 | pub fn num_files(&self) -> usize { 290 | self.todo_files.len() 291 | } 292 | 293 | /// Returns the number of TODOs currently tracked by TodoR 294 | pub fn num_todos(&self) -> usize { 295 | self.todo_files.iter().map(|tf| tf.todos.len()).sum() 296 | } 297 | 298 | /// Returns all tracked files that contain TODOs 299 | pub fn get_tracked_files(&self) -> Vec<&str> { 300 | self.todo_files 301 | .iter() 302 | .filter(|tf| !tf.todos.is_empty()) 303 | .map(|tf| tf.filepath.to_str().unwrap()) 304 | .collect() 305 | } 306 | 307 | /// Returns all tracked files even if they have no TODOs 308 | pub fn get_all_tracked_files<'b>(&'b self) -> Vec<&'b str> { 309 | self.todo_files 310 | .iter() 311 | .map(|tf| tf.filepath.to_str().unwrap()) 312 | .collect() 313 | } 314 | 315 | /// Opens file at given filepath and process it by finding all its TODOs. 316 | pub fn open_todos(&mut self, filepath: F) -> Result<(), Error> 317 | where 318 | F: AsRef, 319 | { 320 | // using _p just to let the compiler know the correct type for open_option_filtered_todos() 321 | let mut _p = Some(|_t: &Todo| true); 322 | _p = None; 323 | self.open_option_filtered_todos(filepath, &_p) 324 | } 325 | 326 | /// Opens file at given filepath and process it by finding all its TODOs. 327 | /// Only TODOs that satisfy pred are added. 328 | pub fn open_filtered_todos(&mut self, filepath: F, pred: &P) -> Result<(), Error> 329 | where 330 | P: Fn(&Todo) -> bool, 331 | F: AsRef, 332 | { 333 | self.open_option_filtered_todos(filepath, &Some(pred)) 334 | } 335 | 336 | /// Opens file at given filepath and process it by finding all its TODOs. 337 | /// If pred is not None, only TODOs that satisfy pred are added. 338 | /// 339 | /// This method is useful for when you are not sure at compile time if a filter is necessary. 340 | pub fn open_option_filtered_todos( 341 | &mut self, 342 | filepath: F, 343 | pred: &Option

, 344 | ) -> Result<(), Error> 345 | where 346 | P: Fn(&Todo) -> bool, 347 | F: AsRef, 348 | { 349 | let filepath = filepath.as_ref(); 350 | let mut todo_file = TodoFile::new(filepath); 351 | 352 | // Make sure the file is not a directory 353 | if !filepath.is_file() { 354 | if filepath.is_dir() { 355 | return Err(TodoRError::InputIsDir { 356 | filepath: filepath.to_string_lossy().to_string(), 357 | } 358 | .into()); 359 | } else { 360 | return Err(TodoRError::CannotAccessFile { 361 | filepath: filepath.to_string_lossy().to_string(), 362 | } 363 | .into()); 364 | } 365 | } 366 | 367 | let file_ext = match filepath.extension() { 368 | Some(ext) => ext.to_str().unwrap(), 369 | // lots of shell files have no extension 370 | None => "sh", 371 | }; 372 | let parser_regexs = self.config.ext_to_regexs.get(file_ext, &self.config.tags); 373 | 374 | let file = File::open(filepath)?; 375 | let mut file_reader = BufReader::new(file); 376 | todo_file.set_todos(match pred { 377 | Some(p) => parse_content_with_filter(&mut file_reader, &parser_regexs, p)?, 378 | None => parse_content(&mut file_reader, &parser_regexs)?, 379 | }); 380 | 381 | debug!( 382 | "found {} TODOs in `{}`", 383 | todo_file.len(), 384 | filepath.display() 385 | ); 386 | 387 | self.todo_files.push(todo_file); 388 | Ok(()) 389 | } 390 | 391 | /// Finds TODO comments in the given content 392 | pub fn find_todos(&mut self, content: &str, ext: &str) -> Result<(), Error> { 393 | let mut todo_file = TodoFile::new(""); 394 | let mut content_buf = Cursor::new(content); 395 | let parser_regexs = self.config.ext_to_regexs.get(ext, &self.config.tags); 396 | 397 | todo_file.set_todos(parse_content(&mut content_buf, &parser_regexs)?); 398 | 399 | self.todo_files.push(todo_file); 400 | Ok(()) 401 | } 402 | 403 | /// Prints TODOs to stdout. 404 | pub fn print_todos(&self) { 405 | // lock stdout to print faster 406 | let stdout = io::stdout(); 407 | let lock = stdout.lock(); 408 | let mut out_buffer = io::BufWriter::new(lock); 409 | 410 | self.write_todos(&mut out_buffer).unwrap(); 411 | } 412 | 413 | /// Writes TODOs to out_buffer. 414 | pub fn write_todos(&self, out_buffer: &mut impl Write) -> Result<(), Error> { 415 | for todo_file in &self.todo_files { 416 | write_file_todos(out_buffer, &todo_file, &self.config.styles)?; 417 | } 418 | 419 | Ok(()) 420 | } 421 | 422 | /// Writes TODOs to out_buffer. 423 | // MAYB: change self.todo_files to Hashmap for easier finding 424 | pub fn write_todos_from_file( 425 | &self, 426 | filepath: &Path, 427 | out_buffer: &mut impl Write, 428 | ) -> Result<(), Error> { 429 | for todo_file in &self.todo_files { 430 | if todo_file.filepath == filepath { 431 | write_file_todos(out_buffer, &todo_file, &self.config.styles)?; 432 | break; 433 | } 434 | } 435 | 436 | Ok(()) 437 | } 438 | 439 | /// Returns an iterator that Iterates over tracked TODOs along with the 440 | pub fn iter(&self) -> impl Iterator { 441 | self.todo_files.iter().flatten() 442 | } 443 | 444 | /// Deletes TODO line from given filepath corresponding to the given index. 445 | pub fn remove_todo(&mut self, filepath: &Path, todo_index: usize) -> Result<(), Error> { 446 | for mut todo_file in &mut self.todo_files { 447 | if filepath == todo_file.filepath { 448 | remover::remove_todo_by_index(&mut todo_file, todo_index)?; 449 | return Ok(()); 450 | } 451 | } 452 | 453 | Err(TodoRError::FileNotTracked { 454 | filepath: filepath.to_string_lossy().to_string(), 455 | } 456 | .into()) 457 | } 458 | 459 | /// Deletes TODO line from given filepath corresponding to the given line. 460 | pub fn remove_todo_line(&mut self, filepath: &Path, line: usize) -> Result<(), Error> { 461 | for mut todo_file in &mut self.todo_files { 462 | if filepath == todo_file.filepath { 463 | remover::remove_todo_by_line(&mut todo_file, line)?; 464 | 465 | return Ok(()); 466 | } 467 | } 468 | 469 | Err(TodoRError::FileNotTracked { 470 | filepath: filepath.to_string_lossy().to_string(), 471 | } 472 | .into()) 473 | } 474 | } 475 | 476 | impl Serialize for TodoR { 477 | fn serialize(&self, serializer: S) -> Result 478 | where 479 | S: Serializer, 480 | { 481 | let mut seq = serializer.serialize_seq(Some(self.todo_files.len()))?; 482 | for ptodo in self.iter() { 483 | seq.serialize_element(&ptodo)?; 484 | } 485 | seq.end() 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /src/maps.rs: -------------------------------------------------------------------------------- 1 | // Module for CommentMapStruct 2 | 3 | use fnv::FnvHashMap; 4 | use regex::Regex; 5 | use std::borrow::Borrow; 6 | use std::hash::Hash; 7 | 8 | use crate::comments::CommentTypes; 9 | use crate::parser::build_parser_regexs; 10 | 11 | /// FallbackHashMap is a Hashmap that yields a fallback value for `get(k)` if `k` has not been 12 | /// inserted. 13 | /// 14 | /// FallbackHashMap uses `fnv::FnvHashMap` as its hasher. 15 | #[derive(Debug, Clone)] 16 | pub struct FallbackHashMap { 17 | map: FnvHashMap, 18 | fallback_value: V, 19 | } 20 | 21 | impl FallbackHashMap { 22 | /// Creates new FallbackHashMap 23 | pub fn new(fallback_value: V) -> FallbackHashMap { 24 | FallbackHashMap { 25 | map: FnvHashMap::default(), 26 | fallback_value, 27 | } 28 | } 29 | 30 | /// Inserts value `v` into FallbackHashMap with key `k` 31 | pub fn insert(&mut self, k: K, v: V) -> Option { 32 | self.map.insert(k, v) 33 | } 34 | 35 | /// Gets the value for key `k`. If `k` has not been inserted, fallback value is returned 36 | pub fn get(&self, k: &Q) -> &V 37 | where 38 | K: Borrow, 39 | Q: Hash + Eq, 40 | { 41 | self.map.get(k).unwrap_or(&self.fallback_value) 42 | } 43 | 44 | /// Gets the value for key `k`. Returns `None` instead of a fallback if the key is not found 45 | pub fn get_without_fallback(&self, k: &Q) -> Option<&V> 46 | where 47 | K: Borrow, 48 | Q: Hash + Eq, 49 | { 50 | self.map.get(k) 51 | } 52 | } 53 | 54 | /// Hashmap that does not need to copy values for two keys to have same value. 55 | /// Also, it converts `CommentTypes` into `Vec` and caches the value to avoid 56 | /// repeated conversions. 57 | /// 58 | /// Note that CommentRegexMultiMap is not designed to remove items or repeatedly change values for 59 | /// a given key. 60 | /// This is because values are not reference counted and thus cannot be discarded once added. 61 | /// This lets it not have to hash twice and thus be a little more performant. 62 | #[derive(Debug, Clone)] 63 | pub struct CommentRegexMultiMap { 64 | map: FallbackHashMap, 65 | comment_types: Vec, 66 | regexs: Vec>>, 67 | } 68 | 69 | impl CommentRegexMultiMap { 70 | /// Creates new CommentRegexMultiMap 71 | pub fn new(fallback_value: CommentTypes) -> CommentRegexMultiMap { 72 | let mut comment_types = Vec::new(); 73 | comment_types.push(fallback_value); 74 | 75 | let mut regexs = Vec::new(); 76 | regexs.push(None); 77 | 78 | CommentRegexMultiMap { 79 | map: FallbackHashMap::new(0), 80 | comment_types, 81 | regexs, 82 | } 83 | } 84 | 85 | /// Inserts value `v` with key `k` 86 | // MAYB?: reference count to delete unused CommentTypes if inserted over. 87 | #[allow(dead_code)] 88 | pub fn insert(&mut self, k: K, v: CommentTypes) { 89 | let i = self.comment_types.len(); 90 | self.map.insert(k, i); 91 | self.comment_types.push(v); 92 | self.regexs.push(None); 93 | } 94 | 95 | /// Inserts value `v` for all keys in `ks` 96 | pub fn insert_keys(&mut self, ks: impl IntoIterator, v: CommentTypes) { 97 | let i = self.comment_types.len(); 98 | for k in ks { 99 | self.map.insert(k, i); 100 | } 101 | self.comment_types.push(v); 102 | self.regexs.push(None); 103 | } 104 | 105 | /// Gets the the Vec built from the inserted CommentTypes for key `k`. 106 | /// The Vec is cached so the regexs do not need to be rebuilt. 107 | /// If `k` has not been inserted, fallback value is returned 108 | pub fn get(&mut self, k: &Q, tags: &[String]) -> &Vec 109 | where 110 | K: Borrow, 111 | Q: Hash + Eq, 112 | { 113 | let v_i = *self.map.get(k); 114 | let comment_types = &self.comment_types; 115 | self.regexs[v_i].get_or_insert_with(|| build_parser_regexs(&comment_types[v_i], tags)) 116 | } 117 | 118 | /// Same as `get()` except it does not fallback if the key is not found. 119 | #[allow(dead_code)] 120 | pub fn get_without_fallback(&mut self, k: &Q, tags: &[String]) -> Option<&Vec> 121 | where 122 | K: Borrow, 123 | Q: Hash + Eq, 124 | { 125 | match self.map.get_without_fallback(k) { 126 | Some(pv_i) => { 127 | let v_i = *pv_i; 128 | let comment_types = &self.comment_types; 129 | Some( 130 | self.regexs[v_i] 131 | .get_or_insert_with(|| build_parser_regexs(&comment_types[v_i], tags)), 132 | ) 133 | } 134 | None => None, 135 | } 136 | } 137 | 138 | /// Resets the fallback value 139 | #[allow(dead_code)] 140 | pub fn reset_fallback_value(&mut self, new_fallback_value: CommentTypes) { 141 | self.comment_types[0] = new_fallback_value; 142 | self.regexs[0] = None; 143 | } 144 | 145 | /// Resets the fallback value to the one given by `new_fallback_key`. 146 | /// Returns the new fallback `Some(CommentTypes)` if succeeded and `None` otherwise. 147 | pub fn reset_fallback_key(&mut self, new_fallback_key: &Q) -> Option<&CommentTypes> 148 | where 149 | K: Borrow, 150 | Q: Hash + Eq, 151 | { 152 | match self.map.get_without_fallback(new_fallback_key) { 153 | Some(pv_i) => { 154 | let v_i = *pv_i; 155 | if v_i != 0 { 156 | self.comment_types[0] = self.comment_types[v_i].clone(); 157 | self.regexs[0] = None; 158 | } 159 | Some(&self.comment_types[0]) 160 | } 161 | None => None, 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | // Module for finding TODOs in files 2 | 3 | use log::trace; 4 | use regex::Regex; 5 | use std::borrow::Cow; 6 | use std::io::BufRead; 7 | 8 | use crate::comments::CommentTypes; 9 | use crate::custom_tags::get_regex_for_comment; 10 | use crate::todo::Todo; 11 | 12 | /// Builds Regexs for use with parse_content. 13 | pub fn build_parser_regexs(comment_types: &CommentTypes, tags: &[String]) -> Vec { 14 | comment_types 15 | .iter() 16 | .map(|c| get_regex_for_comment(tags, c).unwrap()) 17 | .collect() 18 | } 19 | 20 | /// Parses content and creates a list of TODOs found in content 21 | pub fn parse_content(content_buf: &mut B, regexs: &[Regex]) -> Result, std::io::Error> 22 | where 23 | B: BufRead, 24 | { 25 | trace!("capturing content against {} regexs", regexs.len()); 26 | 27 | let mut todos = Vec::new(); 28 | for (line_num, line_result) in content_buf.lines().enumerate() { 29 | let line = line_result?; 30 | 31 | for re in regexs.iter() { 32 | if let Some(todo_caps) = re.captures(&line) { 33 | let content: Cow = match todo_caps.get(2) { 34 | Some(user) => Cow::Owned(format!( 35 | "@{} {}", 36 | user.as_str(), 37 | todo_caps.get(3).unwrap().as_str() 38 | )), 39 | None => Cow::Borrowed(&todo_caps.get(3).map_or_else(|| "", |s| s.as_str())), 40 | }; 41 | 42 | let todo = Todo::new(line_num + 1, &todo_caps[1], content); 43 | todos.push(todo); 44 | }; 45 | } 46 | } 47 | 48 | Ok(todos) 49 | } 50 | 51 | /// Parses content and creates a list of TODOs found in content. Only adds TODOs that satisfy pred. 52 | pub fn parse_content_with_filter

( 53 | content_buf: &mut impl BufRead, 54 | regexs: &[Regex], 55 | pred: P, 56 | ) -> Result, std::io::Error> 57 | where 58 | P: Fn(&Todo) -> bool, 59 | { 60 | trace!("capturing content against {} regexs", regexs.len()); 61 | 62 | let mut todos = Vec::new(); 63 | for (line_num, line_result) in content_buf.lines().enumerate() { 64 | let line = line_result?; 65 | 66 | for re in regexs.iter() { 67 | if let Some(todo_caps) = re.captures(&line) { 68 | let content: Cow = match todo_caps.get(2) { 69 | Some(user) => Cow::Owned(format!( 70 | "@{} {}", 71 | user.as_str(), 72 | todo_caps.get(3).unwrap().as_str() 73 | )), 74 | None => Cow::Borrowed(&todo_caps.get(3).map_or_else(|| "", |s| s.as_str())), 75 | }; 76 | 77 | let todo = Todo::new(line_num + 1, &todo_caps[1], content); 78 | if pred(&todo) { 79 | todos.push(todo); 80 | } 81 | }; 82 | } 83 | } 84 | 85 | Ok(todos) 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::*; 91 | use std::io::Cursor; 92 | 93 | use crate::comments::CommentTypes; 94 | 95 | fn test_content(content: &str, exp_result: Option<&str>, file_ext: &str) { 96 | let comment_types = match file_ext { 97 | "rs" => CommentTypes::new().add_single("//").add_block("/*", "*/"), 98 | "c" => CommentTypes::new().add_single("//").add_block("/*", "*/"), 99 | "py" => CommentTypes::new() 100 | .add_single("#") 101 | .add_block("\"\"\"", "\"\"\""), 102 | _ => CommentTypes::new().add_single("//").add_block("/*", "*/"), 103 | }; 104 | 105 | let mut content_buf = Cursor::new(content); 106 | let todos = parse_content( 107 | &mut content_buf, 108 | &build_parser_regexs(&comment_types, &["TODO".to_string()]), 109 | ) 110 | .unwrap(); 111 | 112 | if todos.is_empty() { 113 | assert_eq!(exp_result, None); 114 | } else { 115 | assert_eq!(exp_result, Some(todos[0].content.as_str())); 116 | } 117 | } 118 | 119 | fn test_users(content: &str, exp_content: Option<&str>, exp_users: &[&str], file_ext: &str) { 120 | let comment_types = match file_ext { 121 | "rs" => CommentTypes::new().add_single("//").add_block("/*", "*/"), 122 | "c" => CommentTypes::new().add_single("//").add_block("/*", "*/"), 123 | "py" => CommentTypes::new() 124 | .add_single("#") 125 | .add_block("\"\"\"", "\"\"\""), 126 | _ => CommentTypes::new().add_single("//").add_block("/*", "*/"), 127 | }; 128 | 129 | let mut content_buf = Cursor::new(content); 130 | let todos = parse_content( 131 | &mut content_buf, 132 | &build_parser_regexs(&comment_types, &["TODO".to_string()]), 133 | ) 134 | .unwrap(); 135 | 136 | if todos.is_empty() { 137 | assert_eq!(exp_content, None); 138 | } else { 139 | assert_eq!(exp_content, Some(todos[0].content.as_str())); 140 | assert_eq!(exp_users.len(), todos[0].users().len()); 141 | for user in exp_users { 142 | assert!(todos[0].tags_user(user)); 143 | } 144 | } 145 | } 146 | 147 | #[test] 148 | fn find_todos_block_and_line1() { 149 | test_content("/* // todo: item */", None, "rs"); 150 | } 151 | 152 | #[test] 153 | fn find_todos_block_and_line2() { 154 | test_content("/* todo: // item */", Some("// item"), "rs"); 155 | } 156 | 157 | #[test] 158 | fn find_todos_block_and_line3() { 159 | test_content(" // /* todo: item */", None, "rs"); 160 | } 161 | 162 | #[test] 163 | fn find_todos_block_and_line4() { 164 | test_content(" // todo: /* item */", Some("/* item */"), "rs"); 165 | } 166 | 167 | #[test] 168 | fn find_todos_py_in_c_file() { 169 | test_content("# todo: item \t ", None, "c"); 170 | } 171 | 172 | #[test] 173 | fn find_todos_c_comment_in_py_comment() { 174 | test_content("# todo: \\ todo: item \t ", Some("\\ todo: item"), "py"); 175 | } 176 | 177 | #[test] 178 | fn find_todos_c_comment_in_py_comment_in_c_file() { 179 | test_content("# todo: \\ todo: item \t ", None, "c"); 180 | } 181 | 182 | #[test] 183 | fn find_user() { 184 | test_users("// todo(u): item \t ", Some("@u item"), &["u"], "c"); 185 | } 186 | 187 | #[test] 188 | fn find_users() { 189 | test_users( 190 | "// todo(u): @u1 item @u2 \t ", 191 | Some("@u @u1 item @u2"), 192 | &["u", "u1", "u2"], 193 | "c", 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/remover.rs: -------------------------------------------------------------------------------- 1 | // Module for deleting TODO comments from files 2 | 3 | use failure::Error; 4 | use log::debug; 5 | use std::fs::{rename, File}; 6 | use std::io::{BufRead, BufReader, BufWriter, Write}; 7 | 8 | use crate::errors::TodoRError; 9 | use crate::todo::TodoFile; 10 | 11 | pub fn remove_todo_by_index(todo_file: &mut TodoFile, ind: usize) -> Result<(), Error> { 12 | assert!(ind < todo_file.todos.len()); 13 | 14 | let old_file = File::open(&todo_file.filepath)?; 15 | let temp_filepath = todo_file.filepath.with_extension("tmp"); 16 | let temp_file = File::create(&temp_filepath)?; 17 | 18 | let mut file_reader = BufReader::new(old_file); 19 | let mut file_writer = BufWriter::new(temp_file); 20 | 21 | let todo_line = todo_file.todos.remove(ind).line; 22 | 23 | debug!( 24 | "removing content in `{}` on line {}", 25 | todo_file.filepath.display(), 26 | todo_line 27 | ); 28 | copy_except_line(&mut file_reader, &mut file_writer, todo_line)?; 29 | 30 | for todo in &mut todo_file.todos[ind..] { 31 | todo.line -= 1; 32 | } 33 | 34 | // replace old file with temp file 35 | rename(temp_filepath, &todo_file.filepath)?; 36 | 37 | Ok(()) 38 | } 39 | 40 | pub fn remove_todo_by_line(todo_file: &mut TodoFile, line: usize) -> Result<(), Error> { 41 | let del_index: usize; 42 | // Kind of annoyed there is no retain_mut() to make this easier... 43 | { 44 | let mut todos = todo_file 45 | .todos 46 | .iter_mut() 47 | .enumerate() 48 | .skip_while(|(_, t)| t.line < line); 49 | 50 | let (i, todo_to_check) = match todos.next() { 51 | Some((i, todo_to_check)) => (i, todo_to_check), 52 | None => return Err(TodoRError::TodoNotFound { line }.into()), 53 | }; 54 | 55 | // line not found in TODOs 56 | if todo_to_check.line > line { 57 | return Err(TodoRError::TodoNotFound { line }.into()); 58 | } 59 | 60 | del_index = i; 61 | 62 | let old_file = File::open(&todo_file.filepath)?; 63 | let temp_filepath = todo_file.filepath.with_extension("tmp"); 64 | let temp_file = File::create(&temp_filepath)?; 65 | 66 | let mut file_reader = BufReader::new(old_file); 67 | let mut file_writer = BufWriter::new(temp_file); 68 | 69 | debug!( 70 | "removing content in `{}` on line {}", 71 | todo_file.filepath.display(), 72 | line 73 | ); 74 | copy_except_line(&mut file_reader, &mut file_writer, line)?; 75 | 76 | // replace old file with temp file 77 | rename(temp_filepath, &todo_file.filepath)?; 78 | 79 | for (_, todo) in todos { 80 | todo.line -= 1; 81 | } 82 | } 83 | 84 | todo_file.todos.remove(del_index); 85 | 86 | Ok(()) 87 | } 88 | 89 | fn copy_except_line(orig: &mut B, copy: &mut W, line_number: usize) -> Result<(), Error> 90 | where 91 | B: BufRead, 92 | W: Write, 93 | { 94 | let orig_lines = orig.lines(); 95 | let mut line_skip_iter = orig_lines 96 | .enumerate() 97 | .filter(|&(i, _)| i != line_number - 1) 98 | .map(|(_, l)| l); 99 | 100 | // First line needs no '\n' char 101 | { 102 | let first_line = match line_skip_iter.next() { 103 | Some(first_line) => first_line?, 104 | None => return Ok(()), // Input is empty 105 | }; 106 | 107 | copy.write_all(&first_line.into_bytes())?; 108 | } 109 | 110 | // iterate skipping the line with the TODO 111 | for line in line_skip_iter { 112 | let l: String = line?; 113 | copy.write_all(b"\n")?; 114 | copy.write_all(&l.into_bytes())?; 115 | } 116 | 117 | Ok(()) 118 | } 119 | 120 | #[cfg(test)] 121 | mod tests { 122 | use super::*; 123 | use std::io::Cursor; 124 | 125 | fn assert_copy(orig_text: &str, expected_out_text: &str, todo_line: usize) { 126 | let mut out_buf: Cursor> = Cursor::new(Vec::new()); 127 | let mut in_buf = Cursor::new(orig_text); 128 | 129 | copy_except_line(&mut in_buf, &mut out_buf, todo_line).unwrap(); 130 | 131 | let out_bytes = out_buf.into_inner(); 132 | assert_eq!( 133 | expected_out_text.to_string(), 134 | String::from_utf8(out_bytes).unwrap() 135 | ); 136 | } 137 | 138 | #[test] 139 | fn test_remove_line3() { 140 | let todo_line = 3; 141 | let orig_text = "code.run() 142 | // regular comment 143 | // item 144 | // item2 145 | other.stuff() 146 | // another comment"; 147 | 148 | let expected_out_text = "code.run() 149 | // regular comment 150 | // item2 151 | other.stuff() 152 | // another comment"; 153 | 154 | assert_copy(orig_text, expected_out_text, todo_line); 155 | } 156 | 157 | #[test] 158 | fn test_remove_line1() { 159 | let todo_line = 1; 160 | let orig_text = "code.run() 161 | // regular comment 162 | // item 163 | // item2 164 | other.stuff() 165 | // another comment"; 166 | 167 | let expected_out_text = "// regular comment 168 | // item 169 | // item2 170 | other.stuff() 171 | // another comment"; 172 | 173 | assert_copy(orig_text, expected_out_text, todo_line); 174 | } 175 | 176 | #[test] 177 | fn test_remove_line_last() { 178 | let todo_line = 6; 179 | let orig_text = "code.run() 180 | // regular comment 181 | // item 182 | // item2 183 | other.stuff() 184 | // another comment"; 185 | 186 | let expected_out_text = "code.run() 187 | // regular comment 188 | // item 189 | // item2 190 | other.stuff()"; 191 | 192 | assert_copy(orig_text, expected_out_text, todo_line); 193 | } 194 | 195 | #[test] 196 | fn test_remove_line_out_of_bounds() { 197 | let todo_line = 8; 198 | let orig_text = "code.run() 199 | // regular comment 200 | // item 201 | // item2 202 | other.stuff() 203 | // another comment"; 204 | 205 | let expected_out_text = "code.run() 206 | // regular comment 207 | // item 208 | // item2 209 | other.stuff() 210 | // another comment"; 211 | 212 | assert_copy(orig_text, expected_out_text, todo_line); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/todo.rs: -------------------------------------------------------------------------------- 1 | // Module for holding Todo types. 2 | 3 | use failure::Error; 4 | use lazy_static::lazy_static; 5 | use regex::Regex; 6 | use serde::ser::{SerializeSeq, SerializeStruct, Serializer}; 7 | use serde::Serialize; 8 | use std::borrow::Cow; 9 | use std::fmt; 10 | use std::io::Write; 11 | use std::path::{Path, PathBuf}; 12 | 13 | use crate::display::TodoRStyles; 14 | 15 | lazy_static! { 16 | static ref USER_REGEX: Regex = Regex::new(r"(@\S+)").unwrap(); 17 | } 18 | 19 | /// A struct holding the TODO and all the needed meta-information for it. 20 | #[derive(Debug, Clone)] 21 | pub struct Todo { 22 | pub line: usize, 23 | pub tag: String, 24 | pub content: String, 25 | } 26 | 27 | impl Todo { 28 | /// Create new TODO struct 29 | pub fn new<'c>(line: usize, tag_str: &str, content: impl Into>) -> Todo { 30 | Todo { 31 | line, 32 | tag: tag_str.to_uppercase(), 33 | content: content.into().into_owned(), 34 | } 35 | } 36 | 37 | /// Returns ANSI colored output string 38 | pub fn style_string(&self, styles: &TodoRStyles) -> String { 39 | // Paint users using user_style by wrapping users with infix ansi-strings 40 | let cs_to_us = styles.content_style.infix(styles.user_style); 41 | let us_to_cs = styles.user_style.infix(styles.content_style); 42 | let paint_users = |c: ®ex::Captures| format!("{}{}{}", cs_to_us, &c[1], us_to_cs); 43 | let content_out = USER_REGEX.replace_all(&self.content, paint_users); 44 | 45 | let tag_width = &self.tag.len().min(5); 46 | format!( 47 | " {} {}{} {}", 48 | // Columns align for up to 100,000 lines which should be fine 49 | styles 50 | .line_number_style 51 | .paint(format!("line {:<5}", self.line)), 52 | styles 53 | .tag_style(&self.tag) 54 | .paint(format!("{:w$}", &self.tag, w = tag_width)), 55 | format!("{:w$}", "", w = 5 - tag_width), 56 | styles.content_style.paint(content_out), 57 | ) 58 | } 59 | 60 | pub fn write_style_string( 61 | &self, 62 | out_buffer: &mut impl Write, 63 | styles: &TodoRStyles, 64 | ) -> Result<(), Error> { 65 | // Paint users using user_style by wrapping users with infix ansi-strings 66 | let cs_to_us = styles.content_style.infix(styles.user_style); 67 | let us_to_cs = styles.user_style.infix(styles.content_style); 68 | let paint_users = |c: ®ex::Captures| format!("{}{}{}", cs_to_us, &c[1], us_to_cs); 69 | let content_out = USER_REGEX.replace_all(&self.content, paint_users); 70 | 71 | let tag_width = &self.tag.len().min(5); 72 | writeln!( 73 | out_buffer, 74 | " {} {}{} {}", 75 | // Columns align for up to 100,000 lines which should be fine 76 | styles 77 | .line_number_style 78 | .paint(format!("line {:<5}", self.line)), 79 | styles 80 | .tag_style(&self.tag) 81 | .paint(format!("{:w$}", &self.tag, w = tag_width)), 82 | format!("{:w$}", "", w = 5 - tag_width), 83 | styles.content_style.paint(content_out), 84 | )?; 85 | 86 | Ok(()) 87 | } 88 | 89 | /// Returns all is tagged in the Todo. 90 | pub fn users(&self) -> Vec<&str> { 91 | USER_REGEX 92 | .find_iter(&self.content) 93 | .map(|s| s.as_str()) 94 | .collect() 95 | } 96 | 97 | /// Returns true if user is tagged in the Todo. 98 | pub fn tags_user(&self, user: &str) -> bool { 99 | for u in self.users() { 100 | if &u[1..] == user { 101 | return true; 102 | } 103 | } 104 | 105 | false 106 | } 107 | 108 | /// Returns String of TODO serialized in the JSON format 109 | pub fn to_json(&self) -> Result { 110 | Ok(serde_json::to_string(self)?) 111 | } 112 | } 113 | 114 | impl fmt::Display for Todo { 115 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 116 | write!(f, "line {}\t{}\t{}", self.line, self.tag, self.content,) 117 | } 118 | } 119 | 120 | /// A struct holding the a list of TODOs associated with a file. 121 | #[derive(Debug, Clone)] 122 | pub struct TodoFile { 123 | pub filepath: PathBuf, 124 | pub todos: Vec, 125 | } 126 | 127 | impl TodoFile { 128 | pub fn new>(filepath: P) -> TodoFile { 129 | TodoFile { 130 | filepath: filepath.as_ref().to_owned(), 131 | // do not allocate because it will be replaced 132 | todos: Vec::with_capacity(0), 133 | } 134 | } 135 | 136 | pub fn set_todos(&mut self, todos: Vec) { 137 | self.todos = todos; 138 | } 139 | 140 | pub fn is_empty(&self) -> bool { 141 | self.todos.is_empty() 142 | } 143 | 144 | pub fn len(&self) -> usize { 145 | self.todos.len() 146 | } 147 | 148 | /// Writes TODOs in a file serialized in the JSON format 149 | pub fn write_json(&self, out_buffer: &mut impl Write) -> Result<(), Error> { 150 | serde_json::to_writer(out_buffer, &self)?; 151 | Ok(()) 152 | } 153 | } 154 | 155 | impl Serialize for Todo { 156 | fn serialize(&self, serializer: S) -> Result 157 | where 158 | S: Serializer, 159 | { 160 | let mut state = serializer.serialize_struct("Todo", 4)?; 161 | state.serialize_field("line", &self.line)?; 162 | state.serialize_field("tag", &self.tag)?; 163 | state.serialize_field("text", &self.content)?; 164 | state.serialize_field("users", &self.users())?; 165 | state.end() 166 | } 167 | } 168 | 169 | /// Helper struct for printing filename along with other TODO information. 170 | #[derive(Serialize)] 171 | pub struct PathedTodo<'a> { 172 | pub(crate) file: &'a Path, 173 | #[serde(flatten)] 174 | pub(crate) todo: &'a Todo, 175 | } 176 | 177 | impl PathedTodo<'_> { 178 | fn new<'a>(todo: &'a Todo, file: &'a Path) -> PathedTodo<'a> { 179 | PathedTodo { file, todo } 180 | } 181 | } 182 | 183 | /// Iterator for `Todo`s in a `TodoFile` obtained by running `into_iter` on `&TodoFile`. 184 | /// Items returned give the filepath of the `Todo` as well as the underlying `Todo` 185 | pub struct TodoFileIter<'a, I> 186 | where 187 | I: Iterator, 188 | { 189 | inner: I, 190 | file: &'a Path, 191 | } 192 | 193 | type TodoIter<'a> = std::slice::Iter<'a, Todo>; 194 | impl<'a> From<&'a TodoFile> for TodoFileIter<'a, TodoIter<'a>> { 195 | fn from(tf: &TodoFile) -> TodoFileIter<'_, TodoIter<'_>> { 196 | TodoFileIter { 197 | inner: tf.todos.iter(), 198 | file: &tf.filepath, 199 | } 200 | } 201 | } 202 | 203 | impl<'a, I> Iterator for TodoFileIter<'a, I> 204 | where 205 | I: Iterator, 206 | { 207 | type Item = PathedTodo<'a>; 208 | 209 | fn next(&mut self) -> Option { 210 | self.inner.next().map(|t| PathedTodo::new(t, self.file)) 211 | } 212 | 213 | fn size_hint(&self) -> (usize, Option) { 214 | self.inner.size_hint() 215 | } 216 | } 217 | 218 | impl<'a, I> DoubleEndedIterator for TodoFileIter<'a, I> 219 | where 220 | I: Iterator, 221 | I: DoubleEndedIterator, 222 | { 223 | fn next_back(&mut self) -> Option { 224 | self.inner 225 | .next_back() 226 | .map(|t| PathedTodo::new(t, self.file)) 227 | } 228 | } 229 | 230 | impl<'a, I> ExactSizeIterator for TodoFileIter<'a, I> 231 | where 232 | I: Iterator, 233 | I: ExactSizeIterator, 234 | { 235 | fn len(&self) -> usize { 236 | self.inner.len() 237 | } 238 | } 239 | 240 | impl Serialize for TodoFile { 241 | fn serialize(&self, serializer: S) -> Result 242 | where 243 | S: Serializer, 244 | { 245 | let mut seq = serializer.serialize_seq(Some(self.len()))?; 246 | for todo in &self.todos { 247 | let ptodo = PathedTodo::new(todo, &self.filepath); 248 | seq.serialize_element(&ptodo)?; 249 | } 250 | seq.end() 251 | } 252 | } 253 | 254 | impl<'a> IntoIterator for &'a TodoFile { 255 | type Item = PathedTodo<'a>; 256 | type IntoIter = TodoFileIter<'a, TodoIter<'a>>; 257 | 258 | fn into_iter(self) -> TodoFileIter<'a, TodoIter<'a>> { 259 | self.into() 260 | } 261 | } 262 | 263 | #[cfg(test)] 264 | mod test { 265 | use super::*; 266 | // use serde_json; 267 | use std::io::Cursor; 268 | 269 | #[test] 270 | fn json_todo() { 271 | let todo = Todo::new(2, "TODO", "item"); 272 | 273 | assert_eq!( 274 | todo.to_json().unwrap(), 275 | r#"{"line":2,"tag":"TODO","text":"item","users":[]}"#, 276 | ); 277 | } 278 | 279 | #[test] 280 | fn json_todos() { 281 | let mut tf = TodoFile::new(Path::new("tests/test.rs")); 282 | tf.todos.push(Todo::new(2, "TODO", "item1")); 283 | tf.todos.push(Todo::new(5, "TODO", "item2 @u1")); 284 | 285 | let out_vec: Vec = Vec::new(); 286 | let mut out_buf = Cursor::new(out_vec); 287 | tf.write_json(&mut out_buf).unwrap(); 288 | 289 | assert_eq!( 290 | &String::from_utf8(out_buf.into_inner()).unwrap(), 291 | r#"[{"file":"tests/test.rs","line":2,"tag":"TODO","text":"item1","users":[]},{"file":"tests/test.rs","line":5,"tag":"TODO","text":"item2 @u1","users":["@u1"]}]"#, 292 | ); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /tests/.todor: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lavifb/todo_r/1d815a4be773a766fd530d03fbd9880ecfdc4dcd/tests/.todor -------------------------------------------------------------------------------- /tests/.todorignore: -------------------------------------------------------------------------------- 1 | inputt/ignore_this.rs -------------------------------------------------------------------------------- /tests/inputs/config1.toml: -------------------------------------------------------------------------------- 1 | 2 | tags = ["todo", "fixme", "foo"] 3 | 4 | [[comments]] 5 | exts = ["rs", "c"] 6 | 7 | [[comments.types]] 8 | single = "//" 9 | 10 | [[comments.types]] 11 | prefix = "/*" 12 | suffix = "*/" 13 | 14 | [[comments]] 15 | ext = "py" 16 | 17 | [[comments.types]] 18 | single = "#" 19 | 20 | [[comments.types]] 21 | prefix = "\"\"\"" 22 | suffix = "\"\"\"" -------------------------------------------------------------------------------- /tests/inputs/config2.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": [ 3 | "foo", 4 | "item" 5 | ], 6 | "comments": [ 7 | { 8 | "ext": "rs", 9 | "types": [ 10 | { 11 | "single": "//" 12 | }, 13 | { 14 | "single": "^" 15 | }, 16 | { 17 | "prefix": "/*", 18 | "suffix": "*/" 19 | } 20 | ] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/inputs/config2.toml: -------------------------------------------------------------------------------- 1 | 2 | tags = ["foo", "item"] 3 | 4 | [[comments]] 5 | ext = "rs" 6 | 7 | [[comments.types]] 8 | single = "//" 9 | 10 | [[comments.types]] 11 | single = "^" 12 | 13 | [[comments.types]] 14 | prefix = "/*" 15 | suffix = "*/" 16 | -------------------------------------------------------------------------------- /tests/inputs/config2.yaml: -------------------------------------------------------------------------------- 1 | tags: 2 | - foo 3 | - item 4 | comments: 5 | - ext: rs 6 | types: 7 | - single: // 8 | - single: '^' 9 | - prefix: /* 10 | suffix: '*/' 11 | -------------------------------------------------------------------------------- /tests/inputs/test1.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | // TODO: item 3 | /* TAG: item tag */ 4 | // FOO: bar 5 | ^ITEM: item2 6 | } -------------------------------------------------------------------------------- /tests/inputs/test2.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | """ todo: docstring comment """ 3 | a = 1 # todo: does not count 4 | # todo: item -------------------------------------------------------------------------------- /tests/inputt/ignore_this.rs: -------------------------------------------------------------------------------- 1 | // TODO(user1): ignore1 2 | // TODO(user1): ignore2 @user2 3 | // TODO: ignore3 @user1 @user3 -------------------------------------------------------------------------------- /tests/inputt/test1.rs: -------------------------------------------------------------------------------- 1 | // TODO: item2 2 | fn main() { 3 | // FOO: bar2 4 | } 5 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | // Integratino tests for todor 2 | 3 | use assert_cmd::prelude::*; 4 | use std::fs; 5 | use std::path::Path; 6 | use std::process::Command; 7 | 8 | fn dir_sep() -> &'static str { 9 | if cfg!(windows) { 10 | "\\" 11 | } else { 12 | "/" 13 | } 14 | } 15 | 16 | fn todor() -> Command { 17 | let mut cmd = Command::cargo_bin("todor").unwrap(); 18 | cmd.current_dir(format!("tests{0}inputs", dir_sep())); 19 | cmd.arg("--no-style"); 20 | cmd 21 | } 22 | 23 | #[test] 24 | fn basic() { 25 | todor() 26 | .arg("test1.rs") 27 | .assert() 28 | .success() 29 | .stdout( 30 | "test1.rs 31 | line 2 TODO item\n", 32 | ) 33 | .stderr(""); 34 | } 35 | 36 | #[test] 37 | fn colors() { 38 | let mut cmd = Command::cargo_bin("todor").unwrap(); 39 | cmd.current_dir("tests/inputs") 40 | .arg("test1.rs") 41 | .assert() 42 | .success() 43 | .stdout( 44 | "test1.rs 45 | line 2  TODO item\n", 46 | ) 47 | .stderr(""); 48 | } 49 | 50 | #[test] 51 | fn custom_tags1() { 52 | todor() 53 | .arg("test1.rs") 54 | .arg("-T") 55 | .arg("foo") 56 | .assert() 57 | .success() 58 | .stdout( 59 | "test1.rs 60 | line 4 FOO bar\n", 61 | ) 62 | .stderr(""); 63 | } 64 | 65 | #[test] 66 | fn custom_tags2() { 67 | todor() 68 | .arg("test1.rs") 69 | .arg("-T") 70 | .arg("todo") 71 | .arg("foo") 72 | .arg("tag") 73 | .assert() 74 | .success() 75 | .stdout( 76 | "test1.rs 77 | line 2 TODO item 78 | line 3 TAG item tag 79 | line 4 FOO bar\n", 80 | ) 81 | .stderr(""); 82 | } 83 | 84 | #[test] 85 | fn custom_tags3() { 86 | todor() 87 | .arg("test1.rs") 88 | .arg("-t") 89 | .arg("foo") 90 | .assert() 91 | .success() 92 | .stdout( 93 | "test1.rs 94 | line 2 TODO item 95 | line 4 FOO bar\n", 96 | ) 97 | .stderr(""); 98 | } 99 | 100 | #[test] 101 | fn py_extension() { 102 | todor() 103 | .arg("test2.py") 104 | .assert() 105 | .success() 106 | .stdout( 107 | "test2.py 108 | line 2 TODO docstring comment 109 | line 4 TODO item\n", 110 | ) 111 | .stderr(""); 112 | } 113 | 114 | #[test] 115 | fn dir_todos() { 116 | todor().arg("..").assert().success().stdout("").stderr(""); 117 | } 118 | 119 | #[test] 120 | fn config1() { 121 | todor() 122 | .arg("test1.rs") 123 | .arg("-c") 124 | .arg("config1.toml") 125 | .assert() 126 | .success() 127 | .stdout( 128 | "test1.rs 129 | line 2 TODO item 130 | line 4 FOO bar\n", 131 | ) 132 | .stderr(""); 133 | } 134 | 135 | #[test] 136 | fn config2() { 137 | todor() 138 | .arg("test1.rs") 139 | .arg("-c") 140 | .arg("config2.toml") 141 | .assert() 142 | .success() 143 | .stdout( 144 | "test1.rs 145 | line 4 FOO bar 146 | line 5 ITEM item2\n", 147 | ) 148 | .stderr(""); 149 | } 150 | 151 | #[test] 152 | fn config2json() { 153 | todor() 154 | .arg("test1.rs") 155 | .arg("-c") 156 | .arg("config2.json") 157 | .assert() 158 | .success() 159 | .stdout( 160 | "test1.rs 161 | line 4 FOO bar 162 | line 5 ITEM item2\n", 163 | ) 164 | .stderr(""); 165 | } 166 | 167 | #[test] 168 | fn config2yaml() { 169 | todor() 170 | .arg("test1.rs") 171 | .arg("-c") 172 | .arg("config2.yaml") 173 | .assert() 174 | .success() 175 | .stdout( 176 | "test1.rs 177 | line 4 FOO bar 178 | line 5 ITEM item2\n", 179 | ) 180 | .stderr(""); 181 | } 182 | 183 | #[test] 184 | fn multiple() { 185 | todor() 186 | .arg("test1.rs") 187 | .arg("test2.py") 188 | .assert() 189 | .success() 190 | .stdout( 191 | "test1.rs 192 | line 2 TODO item 193 | test2.py 194 | line 2 TODO docstring comment 195 | line 4 TODO item\n", 196 | ) 197 | .stderr(""); 198 | } 199 | 200 | #[test] 201 | fn ignore() { 202 | todor() 203 | .arg("test1.rs") 204 | .arg("test2.py") 205 | .arg("-i") 206 | .arg("test2.py") 207 | .assert() 208 | .success() 209 | .stdout( 210 | "test1.rs 211 | line 2 TODO item\n", 212 | ) 213 | .stderr(""); 214 | } 215 | 216 | #[test] 217 | fn init() { 218 | let mut cmd = Command::cargo_bin("todor").unwrap(); 219 | cmd.current_dir("tests/inputs") 220 | .arg("init") 221 | .assert() 222 | .success() 223 | .stdout("") 224 | .stderr(""); 225 | 226 | let todor_config = Path::new("tests/inputs/.todor"); 227 | // check that file is created 228 | assert!(todor_config.is_file(), "{}", true); 229 | 230 | // remove file 231 | fs::remove_file(todor_config).unwrap(); 232 | } 233 | 234 | #[test] 235 | fn walk1() { 236 | todor() 237 | .current_dir("tests") 238 | .assert() 239 | .success() 240 | .stdout(format!( 241 | "inputs{0}test1.rs 242 | line 2 TODO item 243 | inputs{0}test2.py 244 | line 2 TODO docstring comment 245 | line 4 TODO item 246 | inputt{0}test1.rs 247 | line 1 TODO item2\n", 248 | dir_sep() 249 | )) 250 | .stderr(""); 251 | } 252 | 253 | #[test] 254 | fn walk2() { 255 | todor() 256 | .current_dir("tests") 257 | .arg("-T") 258 | .arg("foo") 259 | .assert() 260 | .success() 261 | .stdout(format!( 262 | "inputs{0}test1.rs 263 | line 4 FOO bar 264 | inputt{0}test1.rs 265 | line 3 FOO bar2\n", 266 | dir_sep() 267 | )) 268 | .stderr(""); 269 | } 270 | 271 | #[test] 272 | fn walk3() { 273 | todor() 274 | .arg("-T") 275 | .arg("foo") 276 | .assert() 277 | .success() 278 | .stdout(format!( 279 | "test1.rs 280 | line 4 FOO bar 281 | ..{0}inputt{0}test1.rs 282 | line 3 FOO bar2\n", 283 | dir_sep() 284 | )) 285 | .stderr(""); 286 | } 287 | 288 | #[test] 289 | fn check1() { 290 | todor().arg("--check").assert().failure(); 291 | } 292 | 293 | #[test] 294 | fn check2() { 295 | todor() 296 | .arg("--check") 297 | .arg("-T") 298 | .arg("none") 299 | .assert() 300 | .success(); 301 | } 302 | 303 | #[test] 304 | fn users() { 305 | todor() 306 | .current_dir("tests/inputt") 307 | .arg("ignore_this.rs") 308 | .assert() 309 | .success() 310 | .stdout( 311 | "ignore_this.rs 312 | line 1 TODO @user1 ignore1 313 | line 2 TODO @user1 ignore2 @user2 314 | line 3 TODO ignore3 @user1 @user3\n", 315 | ) 316 | .stderr(""); 317 | } 318 | 319 | #[test] 320 | fn users_color() { 321 | let mut cmd = Command::cargo_bin("todor").unwrap(); 322 | cmd.current_dir("tests/inputt") 323 | .arg("ignore_this.rs") 324 | .assert() 325 | .success() 326 | .stdout( 327 | "ignore_this.rs 328 | line 1  TODO @user1 ignore1 329 | line 2  TODO @user1 ignore2 @user2 330 | line 3  TODO ignore3 @user1 @user3\n", 331 | ) 332 | .stderr(""); 333 | } 334 | 335 | #[test] 336 | fn select_users1() { 337 | todor() 338 | .current_dir("tests/inputt") 339 | .arg("ignore_this.rs") 340 | .arg("-u") 341 | .arg("user2") 342 | .assert() 343 | .success() 344 | .stdout( 345 | "ignore_this.rs 346 | line 2 TODO @user1 ignore2 @user2\n", 347 | ) 348 | .stderr(""); 349 | } 350 | 351 | #[test] 352 | fn select_users2() { 353 | todor() 354 | .current_dir("tests/inputt") 355 | .arg("ignore_this.rs") 356 | .arg("-u") 357 | .arg("user2") 358 | .arg("user3") 359 | .assert() 360 | .success() 361 | .stdout( 362 | "ignore_this.rs 363 | line 2 TODO @user1 ignore2 @user2 364 | line 3 TODO ignore3 @user1 @user3\n", 365 | ) 366 | .stderr(""); 367 | } 368 | 369 | #[test] 370 | fn json() { 371 | todor() 372 | .current_dir("tests/inputs") 373 | .arg("test1.rs") 374 | .arg("-f") 375 | .arg("json") 376 | .assert() 377 | .success() 378 | .stdout(r#"[{"file":"test1.rs","line":2,"tag":"TODO","text":"item","users":[]}]"#) 379 | .stderr(""); 380 | } 381 | 382 | #[test] 383 | fn json_check1() { 384 | todor() 385 | .current_dir("tests/inputs") 386 | .arg("test1.rs") 387 | .arg("-f") 388 | .arg("json") 389 | .arg("--check") 390 | .assert() 391 | .failure() 392 | .stdout(r#"[{"file":"test1.rs","line":2,"tag":"TODO","text":"item","users":[]}]"#) 393 | .stderr(""); 394 | } 395 | 396 | #[test] 397 | fn json_check2() { 398 | todor() 399 | .current_dir("tests/inputs") 400 | .arg("test1.rs") 401 | .arg("-f") 402 | .arg("json") 403 | .arg("-T") 404 | .arg("TOO") 405 | .arg("--check") 406 | .assert() 407 | .success() 408 | .stdout("[]") 409 | .stderr(""); 410 | } 411 | --------------------------------------------------------------------------------