/* * MIT License * * Copyright (c) 2026 dev@ptrace.dev * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * */ /** +Version: 1.2.0 */ #import "Basic"()( MEMORY_DEBUGGER = MEMORY_DEBUGGER_ENABLED ); #import "File"; #import "File_Utilities"; #import "String"; #import "Sort"; #import "POSIX"; #import "Math"; #import "termcolors"; #import "stringpad"; Re :: #import "uniform"; #load "parse.jai"; #load "display.jai"; /** Limitations: This pattern won't catch keywords inside multiline comments, that have other paragraphs in front on them. Like this: /* My multiline comment that explains something. TODO(me, 10): Check if this `TODO` is reachable! */ Spoiler: This todo comment is not reachable. It wouldn't be a problem to catch this, but this would also catch stuff, that probably isn't inside a comment and set a higher risk to a recursive bomb. */ Filter :: enum_flags u8 { NONE :: 0x1; TODO; NOTE; CONTINUE; IDEA; } main :: () { #if MEMORY_DEBUGGER_ENABLED defer report_memory_leaks(); cmd_args := get_command_line_arguments(); defer this_allocation_is_not_a_leak(cmd_args.data); user_paths: [..]string; defer this_allocation_is_not_a_leak(user_paths.data); visitor: Visitor; defer this_allocation_is_not_a_leak(visitor.paths.data); init_pattern(); termcolors_init_allocator_and_context(); success := args_read(cmd_args, *user_paths); if !success return; if args.help { print(HEEEEEELP); return; } if !user_paths { files_collect(".", *visitor); } else { for user_paths files_collect(it, *visitor); } if !visitor.paths { print("No results\n"); return; }; had_updates := false; for visitor.paths { is_updated, todo, note, idea, cont, other := update(it); had_updates |= is_updated; if is_updated { display(todo, note, idea, cont, other, path_filename(it)); } termcolors_reset_pool(true); reset_temporary_storage(); } if !had_updates print("No results\n"); } set_filter :: () -> Filter { filter: Filter; if args.filter_todo then filter |= .TODO; if args.filter_note then filter |= .NOTE; if args.filter_continue then filter |= .CONTINUE; if args.filter_idea then filter |= .IDEA; if filter == 0 { filter |= .TODO; filter |= .NOTE; filter |= .CONTINUE; filter |= .IDEA; } return filter; } #scope_file args: Args; HEEEEEELP :: #string STR_END Displays TODO, NOTE, IDEA or CONTINUE in files. Usage: track [dir or files] [options] Options: -h Show (this) help menu -r Recursive search -o Omit 'other' results -i Stops ignoring dotfiles dotdirs -sl Follows symlinks -sa Sorts by author instead by priority Filters by: -ft Todo -fn Note -fc Continue -fi Idea -err-io (Debug): shows IO errors -microslop-cringe Normalizes lines endings STR_END; Args :: struct { help := false; recursive := false; omit_other := false; ignore_dot := false; follow_symlinks := false; microslop_cringe := false; sort_by_author := false; show_io_errors := false; // TODO: This is garbage filter_todo := false; filter_note := false; filter_continue := false; filter_idea := false; } // Letting this struct, in case I want to extend it Visitor :: struct { paths: [..]string; } args_read :: (cmd_args: []string, user_paths: *[..]string) -> success: bool { find :: (arg: *bool, patterns: ..string) #expand { for patterns { if array_find(`cmd_args, it) { arg.* = true; array_unordered_remove_by_value(*`cmd_args, it); } } } find(*args.help, "-h", "--help", "-help", "help"); find(*args.recursive, "-r"); find(*args.omit_other, "-o"); find(*args.ignore_dot, "-i"); find(*args.follow_symlinks, "-sl"); find(*args.microslop_cringe, "-microslop-cringe"); find(*args.sort_by_author, "-sa"); find(*args.show_io_errors, "-err-io"); find(*args.filter_todo, "-ft"); find(*args.filter_note, "-fn"); find(*args.filter_continue, "-fc"); find(*args.filter_idea, "-fi"); if cmd_args.count < 2 return true; for i: 1..cmd_args.count-1 array_add(user_paths, cmd_args[i]); return true; } update :: (path: string) -> ( is_updated: bool, todo: [..]Token, note: [..]Token, idea: [..]Token, cont: [..]Token, other: [..]Token ) { does_array_has_data :: (arr: []$T) #expand { `is_updated |= cast(bool, arr); } dummy: [..]Token; dummy.allocator = temp; source, f_success := read_entire_file(path, log_errors = args.show_io_errors); defer free(source); if !f_success { // TODO: Kinda cringe, I have to come up with a way better idea return false, dummy, dummy, dummy, dummy, dummy; } if args.microslop_cringe then normalize_line_endings(*source); has_match, matches := matches_collect(source,, temp); if !has_match { return false, dummy, dummy, dummy, dummy, dummy; } tokens := matches_process(source, matches,, temp); tokens_sorted := sort_tokens(tokens); todo, note, idea, cont, other := filter_tokens(tokens_sorted,, temp); is_updated := false; does_array_has_data(todo); does_array_has_data(note); does_array_has_data(idea); does_array_has_data(cont); does_array_has_data(other); // TODO: This is cringe too return is_updated, todo, note, idea, cont, other; } filter_tokens :: (tokens: []Token) -> (todo: [..]Token, note: [..]Token, idea: [..]Token, cont: [..]Token, other: [..]Token) { should_add :: (expected: Filter, arr: *[..]$T) #expand { if `filter & expected then array_add(arr, `it); } filter := set_filter(); todo, note, idea, cont, other: [..]Token; for tokens { if #complete it.identifier == { case .TODO; should_add(.TODO, *todo); case .NOTE; should_add(.NOTE, *note); case .IDEA; should_add(.IDEA, *idea); case .CONTINUE; should_add(.CONTINUE, *cont); case .NONE; if !args.omit_other then array_add(*other, it); } } return todo, note, idea, cont, other; } sort_tokens :: (tokens: [..]Token) -> []Token { if args.sort_by_author { return quick_sort(tokens, compare_author); } else { return quick_sort(tokens, compare_priority); } } compare_author :: (a: Token, b: Token) -> s64 { return compare_strings(xx a.author, xx b.author); } compare_priority :: (a: Token, b: Token) -> s64 { return compare_floats(xx b.priority, xx a.priority); } files_collect :: (path: string, visitor: *Visitor) { add_if_file :: (path: string) #expand { is_a_file, success := is_file(path); if !success { log_error("Not a valid path: %", path); `return; } if is_a_file { array_add(*`visitor.paths, path); `return; } } collect_files :: (using info: *File_Visit_Info, visitor: *Visitor) { if args.ignore_dot && starts_with(short_name, ".") { descend_into_directory = false; return; } if had_error then return; add_if_file(full_name); } path_trimmed := trim_right(path, "/"); if path != "." { add_if_file(path_trimmed); } complete := visit_files( path_trimmed, recursive = args.recursive, visitor, collect_files, visit_files = true, visit_directories = true, visit_symlinks = args.follow_symlinks ); if !complete { log_error("[Warning]: Some error occured while traversing the file tree"); } } is_file :: (path: string) -> bool, success: bool { stats: stat_t; p := to_c_string(path); ret := stat(p, *stats); this_allocation_is_not_a_leak(p); if ret != 0 return false, false; return S_ISREG(stats.st_mode), true; }