using File_Folder_Helper.Helpers; using File_Folder_Helper.Models; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Diagnostics; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; namespace File_Folder_Helper.ADO2024.PI2; internal static partial class Helper20240623 { [GeneratedRegex("([A-Z]+(.))")] private static partial Regex UpperCase(); [GeneratedRegex("[\\s!?.,@:;|\\\\/\"'`£$%\\^&*{}[\\]()<>~#+\\-=_¬]+")] private static partial Regex InvalidCharacter(); private record H1AndParamCase(string H1, string ParamCase); private record SubTaskLine(string Text, bool Started, bool Completed, long? Ticks, int? Line); private record Record(int? CodeInsidersLine, FileInfo FileInfo, LineNumber LineNumber, int? StopLine, int? SubTasksLine); private record Input(long? AfterEpochTotalMilliseconds, string CodeInsiders, ReadOnlyCollection DestinationDirectories, string DirectoryFilter, string Done, string IndexFile, string SearchPattern, string SubTasks, string SourceDirectory, ReadOnlyCollection Tasks); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Input))] private partial class InputSourceGenerationContext : JsonSerializerContext { } private static Record GetRecord(Input input, FileInfo fileInfo) { Record result; string line; int? stopLine = null; int? subTasksLine = null; int? codeInsidersLine = null; LineNumber lineNumber = HelperMarkdown.GetLineNumbers(fileInfo); for (int i = 0; i < lineNumber.Lines.Count; i++) { line = lineNumber.Lines[i]; if (line.StartsWith(input.CodeInsiders) && line[^1] == ')') codeInsidersLine = i; if (line != input.SubTasks) continue; subTasksLine = i; if (codeInsidersLine is null) break; if (lineNumber.Lines.Count > i) { for (int j = i + 1; j < lineNumber.Lines.Count; j++) { if (lineNumber.Lines[j].Length > 0 && lineNumber.Lines[j][0] == '#') { stopLine = j; break; } } } stopLine ??= lineNumber.Lines.Count; break; } result = new(codeInsidersLine, fileInfo, lineNumber, stopLine, subTasksLine); return result; } private static List GetRecords(Input input) { List results = []; Record record; FileInfo fileInfo; string sourceDirectory = input.SourceDirectory; ReadOnlyCollection directoryNames = HelperDirectory.GetDirectoryNames(input.SourceDirectory); if (!directoryNames.Any(l => l.StartsWith(input.DirectoryFilter, StringComparison.CurrentCultureIgnoreCase))) { string directoryName; string[] checkDirectories = Directory.GetDirectories(input.SourceDirectory, "*", SearchOption.TopDirectoryOnly); foreach (string checkDirectory in checkDirectories) { directoryName = Path.GetFileName(checkDirectory); if (directoryName.StartsWith(input.DirectoryFilter, StringComparison.CurrentCultureIgnoreCase)) { sourceDirectory = checkDirectory; break; } } } string[] subDirectories = Directory.GetDirectories(sourceDirectory, "*", SearchOption.TopDirectoryOnly); List files = Directory.GetFiles(sourceDirectory, input.SearchPattern, SearchOption.TopDirectoryOnly).ToList(); foreach (string subDirectory in subDirectories) files.AddRange(Directory.GetFiles(subDirectory, input.SearchPattern, SearchOption.TopDirectoryOnly)); foreach (string file in files) { fileInfo = new(file); record = GetRecord(input, fileInfo); results.Add(record); } return results; } private static string GetParamCase(string value) { string result; StringBuilder stringBuilder = new(value); Match[] matches = UpperCase().Matches(value).ToArray(); for (int i = matches.Length - 1; i > -1; i--) _ = stringBuilder.Insert(matches[i].Index, '-'); string[] segments = InvalidCharacter().Split(stringBuilder.ToString().ToLower()); result = string.Join('-', segments).Trim('-'); return result; } private static ReadOnlyCollection GetSubTaskLines(Input input, bool? foundStarted, bool? foundCompleted, string fallbackLine, Record record) { List results = []; char done; string line; string text; bool startedValue; bool completedValue; SubTaskLine subTaskLine; bool foundSubTasks = false; int tasksZeroLength = input.Tasks[0].Length; long ticks = record.FileInfo.LastWriteTime.Ticks; for (int i = 0; i < record.LineNumber.Lines.Count; i++) { line = record.LineNumber.Lines[i]; if (!foundSubTasks && line == input.SubTasks) foundSubTasks = true; if (!foundSubTasks) continue; if (line.Length <= tasksZeroLength || !line.StartsWith(input.Tasks[0]) || line[tasksZeroLength] is not ' ' and not 'x' || line[tasksZeroLength + 1] != ']') continue; startedValue = foundStarted is not null && foundStarted.Value; completedValue = foundCompleted is not null && foundCompleted.Value; subTaskLine = new(Text: $" {line}", Started: startedValue, Completed: completedValue, Ticks: ticks, Line: i); results.Add(subTaskLine); } startedValue = foundStarted is not null && foundStarted.Value; completedValue = foundCompleted is not null && foundCompleted.Value; if (record.LineNumber.H1 is null) subTaskLine = new(Text: fallbackLine, Started: startedValue, Completed: completedValue, Ticks: ticks, Line: null); else { done = foundCompleted is null || !foundCompleted.Value ? ' ' : 'x'; string codeInsidersLine = record.CodeInsidersLine is null ? string.Empty : $" ~~{record.LineNumber.Lines[record.CodeInsidersLine.Value]}~~"; text = $"- [{done}] {ticks} {record.LineNumber.Lines[record.LineNumber.H1.Value]}{codeInsidersLine}"; subTaskLine = new(Text: text, Started: startedValue, Completed: completedValue, Ticks: ticks, Line: 0); } results.Add(subTaskLine); return new(results); } private static string GetSeasonName(int dayOfYear) { string result = dayOfYear switch { < 78 => "0.Winter", < 124 => "1.Spring", < 171 => "2.Spring", < 217 => "3.Summer", < 264 => "4.Summer", < 309 => "5.Fall", < 354 => "6.Fall", _ => "7.Winter" }; return result; } private static string[] GetIndexLines(string h1, ReadOnlyCollection h1ParamCaseCollection) => [ "---", "startedColumns:", " - 'In Progress'", "completedColumns:", " - Done", "---", string.Empty, h1[0] == '#' ? h1 : $"# {h1}", string.Empty, "## Backlog", string.Empty, string.Join(Environment.NewLine, h1ParamCaseCollection.Select(l => $"- [{l.ParamCase}](tasks/{l.ParamCase}.md)")), string.Empty, "## Todo", string.Empty, "## In Progress", string.Empty, "## Done", string.Empty ]; private static string[] GetCascadingStyleSheetsLines() => [ ".kanbn-column-done .kanbn-column-task-list {", " border-color: #198038;", "}", string.Empty, ".kanbn-task-data-created {", " display: none;", "}", string.Empty, ".kanbn-task-data-workload {", " display: none;", "}" ]; private static string GetSettingsLines() => /*lang=json,strict*/ """ { "[markdown]": { "editor.wordWrap": "off" }, "cSpell.words": [ "kanbn" ] } """; private static string GetTasksLines(string directory) => /*lang=json,strict*/ """ { "version": "2.0.0", "tasks": [ { "label": "File-Folder-Helper AOT s X Day-Helper-2024-06-23", "type": "shell", "command": "L:/DevOps/Mesa_FI/File-Folder-Helper/bin/Release/net8.0/win-x64/publish/File-Folder-Helper.exe", "args": [ "s", "X", "{}", "Day-Helper-2024-06-23", "*.md", "##_Sub-tasks", "-_[code-insiders](", "index.md", "-_[,](", "##_Done", ".kan", "D:/5-Other-Small/Kanban/Year-Season", "316940400000" ], "problemMatcher": [] } ] } """.Replace("{}", directory.Replace('\\', '/')); private static void FileWriteAllText(string path, string contents) { // string checkJson = Regex.Replace(File.ReadAllText(path), @"\s+", " ", RegexOptions.Multiline); // if (Regex.Replace(singletonJson, @"\s+", " ", RegexOptions.Multiline) != checkJson) // File.WriteAllText(path, singletonJson); string old = !File.Exists(path) ? string.Empty : File.ReadAllText(path); if (old != contents) File.WriteAllText(path, contents); } private static void FileWriteAllText(string path, string[] contents) => FileWriteAllText(path, string.Join(Environment.NewLine, contents)); private static ReadOnlyCollection GetH1ParamCaseCollection(Input input, ReadOnlyCollection lines) { List results = []; string h1; string line; string paramCase; bool foundSubTasks = false; H1AndParamCase h1AndParamCase; int tasksZeroLength = input.Tasks[0].Length; for (int i = 0; i < lines.Count; i++) { line = lines[i]; if (!foundSubTasks && line == input.SubTasks) foundSubTasks = true; if (!foundSubTasks) continue; if (line.Length <= tasksZeroLength || !line.StartsWith(input.Tasks[0]) || line[tasksZeroLength] is not ' ' and not 'x' || line[tasksZeroLength + 1] != ']') continue; h1 = line[(tasksZeroLength + 3)..]; if (string.IsNullOrEmpty(h1)) continue; paramCase = GetParamCase(h1); h1AndParamCase = new(h1, paramCase); results.Add(h1AndParamCase); } return results.AsReadOnly(); } private static void CreateFiles(string directory, ReadOnlyCollection h1ParamCaseCollection) { foreach (H1AndParamCase h1ParamCase in h1ParamCaseCollection) FileWriteAllText(Path.Combine(directory, $"{h1ParamCase.ParamCase}.md"), $"# {h1ParamCase.H1}"); } private static string WriteAndGetIndexFile(string h1, string verifiedDirectory, ReadOnlyCollection h1ParamCaseCollection) { string result; string[] indexLines = GetIndexLines(h1, h1ParamCaseCollection); string kanbanDirectory = Path.Combine(verifiedDirectory, ".kanbn"); string tasksKanbanDirectory = Path.Combine(kanbanDirectory, "tasks"); if (!Directory.Exists(tasksKanbanDirectory)) _ = Directory.CreateDirectory(tasksKanbanDirectory); string verifiedVisualStudioCodeDirectory = Path.Combine(verifiedDirectory, ".vscode"); if (!Directory.Exists(verifiedVisualStudioCodeDirectory)) _ = Directory.CreateDirectory(verifiedVisualStudioCodeDirectory); result = Path.Combine(kanbanDirectory, "index.md"); CreateFiles(tasksKanbanDirectory, h1ParamCaseCollection); FileWriteAllText(result, indexLines); FileWriteAllText(Path.Combine(kanbanDirectory, "board.css"), GetCascadingStyleSheetsLines()); FileWriteAllText(Path.Combine(verifiedVisualStudioCodeDirectory, "settings.json"), GetSettingsLines()); FileWriteAllText(Path.Combine(verifiedVisualStudioCodeDirectory, "tasks.json"), GetTasksLines(verifiedDirectory)); return result; } private static ReadOnlyCollection GetXColumns(Input input, int frontMatterYamlEnd, int value, ReadOnlyCollection lines) { List results = []; string[] segments; for (int i = value + 1; i < frontMatterYamlEnd; i++) { segments = lines[i].Replace("\t", " ").Split(" - "); if (segments.Length != 2) break; results.Add($"## {segments[1].Replace("'", string.Empty)}"); } if (results.Count == 0) results.Add(input.Done); return results.AsReadOnly(); } private static ReadOnlyCollection GetCompletedColumns(Input input, LineNumber lineNumber) { List results; if (lineNumber.FrontMatterYamlEnd is null || lineNumber.CompletedColumns is null) results = []; else results = GetXColumns(input, lineNumber.FrontMatterYamlEnd.Value, lineNumber.CompletedColumns.Value, lineNumber.Lines).ToList(); if (results.Count == 0) results.Add(input.Done); return results.AsReadOnly(); } private static ReadOnlyCollection GetStartedColumns(Input input, LineNumber lineNumber) { List results; if (lineNumber.FrontMatterYamlEnd is null || lineNumber.StartedColumns is null) results = []; else results = GetXColumns(input, lineNumber.FrontMatterYamlEnd.Value, lineNumber.StartedColumns.Value, lineNumber.Lines).ToList(); if (results.Count == 0) results.Add(input.Done); return results.AsReadOnly(); } private static ReadOnlyCollection GetSubTaskLines(Input input, FileInfo fileInfo, LineNumber lineNumber) { List results = []; FileInfo f; Record record; char completed; string[] segments; bool startedValue; bool completedValue; string fallbackLine; bool? foundStarted = null; bool? foundCompleted = null; ReadOnlyCollection subTaskLines; ReadOnlyCollection startedColumns = GetStartedColumns(input, lineNumber); ReadOnlyCollection completedColumns = GetCompletedColumns(input, lineNumber); int start = lineNumber.FrontMatterYamlEnd is null ? 0 : lineNumber.FrontMatterYamlEnd.Value + 1; for (int i = start; i < lineNumber.Lines.Count; i++) { if ((foundStarted is null || !foundStarted.Value) && startedColumns.Any(lineNumber.Lines[i].StartsWith)) foundStarted = true; if ((foundCompleted is null || !foundCompleted.Value) && completedColumns.Any(lineNumber.Lines[i].StartsWith)) foundCompleted = true; segments = lineNumber.Lines[i].Split(input.Tasks[1]); startedValue = foundStarted is not null && foundStarted.Value; completedValue = foundCompleted is not null && foundCompleted.Value; if (segments.Length > 2 || !segments[0].StartsWith(input.Tasks[0])) continue; completed = foundCompleted is null || !foundCompleted.Value ? ' ' : 'x'; fallbackLine = $"- [{completed}] {segments[0][input.Tasks[0].Length..]} ~~FallbackLine~~"; if (string.IsNullOrEmpty(fileInfo.DirectoryName)) continue; f = new(Path.GetFullPath(Path.Combine(fileInfo.DirectoryName, segments[1][..^1]))); if (!f.Exists) { results.Add(new(Text: fallbackLine, Started: startedValue, Completed: completedValue, Ticks: null, Line: null)); continue; } record = GetRecord(input, f); subTaskLines = GetSubTaskLines(input, startedValue, completedValue, fallbackLine, record); for (int j = subTaskLines.Count - 1; j >= 0; j--) results.Add(subTaskLines[j]); } return results.AsReadOnly(); } private static Input GetInput(List args) { string indexFile = args[5]; string searchPattern = args[2]; string directoryFilter = args[8]; string done = args[7].Replace('_', ' '); string subTasks = args[3].Replace('_', ' '); string codeInsiders = args[4].Replace('_', ' '); string sourceDirectory = Path.GetFullPath(args[0]); string[] tasks = args[6].Split(',').Select(l => l.Replace('_', ' ')).ToArray(); long? afterEpochTotalMilliseconds = args.Count < 11 ? null : long.Parse(args[10]); string[] destinationDirectories = args.Count < 10 ? [] : args.Count < 12 ? [Path.GetFullPath(args[9])] : [Path.GetFullPath(args[9]), Path.GetFullPath(args[11])]; Input input = new(AfterEpochTotalMilliseconds: afterEpochTotalMilliseconds, CodeInsiders: codeInsiders, DestinationDirectories: destinationDirectories.AsReadOnly(), DirectoryFilter: directoryFilter, Done: done, IndexFile: indexFile, SearchPattern: searchPattern, SubTasks: subTasks, SourceDirectory: sourceDirectory, Tasks: tasks.AsReadOnly()); if (input.Tasks[0] != "- [" || input.Tasks[1] != "](") throw new Exception(JsonSerializer.Serialize(input, InputSourceGenerationContext.Default.Input)); return input; } private static string? MaybeWriteAndGetIndexFile(Input input, Record record, string? checkDirectory) { string? result; if (string.IsNullOrEmpty(checkDirectory) || input.AfterEpochTotalMilliseconds is null || input.DestinationDirectories.Count == 0) result = null; else { if (!input.DestinationDirectories.Any(checkDirectory.Contains)) result = null; else { if (record.LineNumber.H1 is null) result = null; else { string segment = Path.GetFileName(checkDirectory); string h1 = record.LineNumber.Lines[record.LineNumber.H1.Value]; DateTime utcEpochDateTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); long utcEpochTotalMilliseconds = (long)Math.Floor(DateTime.UtcNow.Subtract(utcEpochDateTime).TotalMilliseconds); if (!long.TryParse(segment, out long check) || check < input.AfterEpochTotalMilliseconds || check > utcEpochTotalMilliseconds) result = null; else { ReadOnlyCollection h1ParamCaseCollection = GetH1ParamCaseCollection(input, record.LineNumber.Lines); if (h1ParamCaseCollection.Count == 0) result = null; else { DateTime dateTime = utcEpochDateTime.AddMilliseconds(check).ToLocalTime(); string seasonName = GetSeasonName(dateTime.DayOfYear); ReadOnlyCollection directoryNames = HelperDirectory.GetDirectoryNames(checkDirectory); if (!directoryNames.Contains(dateTime.Year.ToString()) || !directoryNames.Contains($"{dateTime.Year}-{seasonName}") || !directoryNames.Contains(check.ToString())) result = null; else result = WriteAndGetIndexFile(h1, checkDirectory, h1ParamCaseCollection); } } } } } return result; } private static bool FileWrite(long ticks, Record record, List newLines, double percent) { bool result = false; if (record.StopLine is not null && record.SubTasksLine is not null) { string contents; string progressLine; List resultLines; if (record.FileInfo.LastWriteTime.Ticks <= ticks) resultLines = record.LineNumber.Lines.ToList(); else resultLines = File.ReadAllLines(record.FileInfo.FullName).ToList(); if (record.LineNumber.FrontMatterYamlEnd is not null) { progressLine = $"progress: {percent}"; if (record.LineNumber.Progress is not null) resultLines[record.LineNumber.Progress.Value] = progressLine; else { resultLines.Insert(record.LineNumber.FrontMatterYamlEnd.Value, progressLine); contents = string.Join(Environment.NewLine, resultLines); FileWriteAllText(record.FileInfo.FullName, contents); result = true; } if (!result && record.LineNumber.Completed is null && percent > 99.9) { resultLines.Insert(record.LineNumber.FrontMatterYamlEnd.Value, $"completed: {DateTime.Now:yyyy-MM-dd}"); contents = string.Join(Environment.NewLine, resultLines); FileWriteAllText(record.FileInfo.FullName, contents); result = true; } if (!result && record.LineNumber.Completed is not null && percent < 99.9) { resultLines.RemoveAt(record.LineNumber.Completed.Value); contents = string.Join(Environment.NewLine, resultLines); FileWriteAllText(record.FileInfo.FullName, contents); result = true; } } if (!result) { for (int i = record.StopLine.Value - 1; i > record.SubTasksLine.Value + 1; i--) resultLines.RemoveAt(i); if (record.StopLine.Value == record.LineNumber.Lines.Count && resultLines[^1].Length == 0) resultLines.RemoveAt(resultLines.Count - 1); for (int i = 0; i < newLines.Count; i++) resultLines.Insert(record.SubTasksLine.Value + 1 + i, newLines[i]); resultLines.Insert(record.SubTasksLine.Value + 1, string.Empty); contents = string.Join(Environment.NewLine, resultLines); FileWriteAllText(record.FileInfo.FullName, contents); } } return result; } private static string? GetInferredCheckDirectory(string directory) { string? result = null; List directoryNames = []; DirectoryInfo directoryInfo; string? checkDirectory = directory; directoryNames.Add(Path.GetFileName(checkDirectory)); string pathRoot = Path.GetPathRoot(directory) ?? throw new Exception(); for (int i = 0; i < directory.Length; i++) { checkDirectory = Path.GetDirectoryName(checkDirectory); if (string.IsNullOrEmpty(checkDirectory) || checkDirectory == pathRoot) break; directoryInfo = new(checkDirectory); if (!directoryInfo.Exists) directoryNames.Add(directoryInfo.Name); else { directoryNames.Reverse(); result = string.IsNullOrEmpty(directoryInfo.LinkTarget) ? checkDirectory : directoryInfo.LinkTarget; for (int j = 0; j < directoryNames.Count; j++) result = Path.GetDirectoryName(result) ?? throw new Exception(); foreach (string directoryName in directoryNames) result = Path.Combine(result, directoryName); break; } } return result; } private static void UpdateFileAndStartNewProcess(ILogger logger, Input input, Record record, string inferredCheckDirectory) { if (record.CodeInsidersLine is null) throw new Exception(); List lines = record.LineNumber.Lines.ToList(); lines[record.CodeInsidersLine.Value] = $"{input.CodeInsiders}{inferredCheckDirectory})"; string text = string.Join(Environment.NewLine, lines); File.WriteAllText(record.FileInfo.FullName, text); record.FileInfo.Refresh(); string file = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "Microsoft VS Code Insiders", "Code - Insiders.exe"); try { _ = Process.Start(file, $"\"{inferredCheckDirectory}\""); } catch (Exception) { logger.LogWarning("Failed to start code-insiders!"); } } private static FileInfo GetIndexFileInfo(ILogger logger, Input input, Record record) { FileInfo result; string? indexFile; List results; if (record.CodeInsidersLine is null) throw new Exception(); string codeInsidersLine = record.LineNumber.Lines[record.CodeInsidersLine.Value]; string raw = codeInsidersLine[input.CodeInsiders.Length..^1]; string checkDirectory = $"{raw[..2].ToUpper()}{raw[2..]}"; if (!Directory.Exists(checkDirectory)) { if (input.DestinationDirectories.Count > 0 && input.DestinationDirectories.Any(checkDirectory.Contains)) { string? inferredCheckDirectory = GetInferredCheckDirectory(checkDirectory); if (!string.IsNullOrEmpty(inferredCheckDirectory)) { checkDirectory = inferredCheckDirectory; _ = Directory.CreateDirectory(inferredCheckDirectory); UpdateFileAndStartNewProcess(logger, input, record, inferredCheckDirectory); } } } if (!Directory.Exists(checkDirectory)) results = []; else { results = Directory.GetFiles(checkDirectory, input.IndexFile, SearchOption.AllDirectories).ToList(); if (results.Count != 1) { for (int i = results.Count - 1; i > -1; i--) { if (!results[i].Contains(input.DirectoryFilter, StringComparison.CurrentCultureIgnoreCase)) results.RemoveAt(i); } } if (results.Count == 0) { indexFile = MaybeWriteAndGetIndexFile(input, record, checkDirectory); if (!string.IsNullOrEmpty(indexFile)) results.Add(indexFile); else logger.LogInformation("<{checkDirectory}>", checkDirectory); } } result = results.Count == 0 ? new(Path.Combine(checkDirectory, input.IndexFile)) : new(results[0]); return result; } internal static void UpdateSubTasksInMarkdownFiles(ILogger logger, List args) { bool reload; int allCount; int lineCheck; double percent; string replace; FileInfo fileInfo; double startedCount; List records; double completedCount; LineNumber lineNumber; List newLines; bool reloadAny = false; string? checkDirectory; List oldLines = []; Input input = GetInput(args); string fileNameWithoutExtension; long ticks = DateTime.Now.Ticks; ReadOnlyCollection subTaskLines; for (int z = 0; z < 9; z++) { records = GetRecords(input); foreach (Record record in from l in records orderby l.SubTasksLine is null, l.CodeInsidersLine is null select l) { if (record.SubTasksLine is null) continue; fileNameWithoutExtension = Path.GetFileNameWithoutExtension(record.FileInfo.FullName); if (record.CodeInsidersLine is not null) logger.LogInformation("<{file}> has [{subTasks}]", fileNameWithoutExtension, input.SubTasks); else { logger.LogWarning("<{file}> has [{subTasks}] but doesn't have [{codeInsiders}]!", fileNameWithoutExtension, input.SubTasks, input.CodeInsiders); continue; } if (record.StopLine is null) continue; fileInfo = GetIndexFileInfo(logger, input, record); if (!fileInfo.Exists) { logger.LogError("<{checkDirectory}> doesn't have a [{indexFile}]", fileInfo.DirectoryName, input.IndexFile); continue; } oldLines.Clear(); checkDirectory = fileInfo.DirectoryName; lineNumber = HelperMarkdown.GetLineNumbers(fileInfo); subTaskLines = GetSubTaskLines(input, fileInfo, lineNumber); if (subTaskLines.Count == 0) continue; lineCheck = 0; for (int i = record.SubTasksLine.Value + 1; i < record.StopLine.Value - 1; i++) oldLines.Add(record.LineNumber.Lines[i]); if (subTaskLines.Count == 0) { percent = 0; replace = "0"; } else { allCount = (from l in subTaskLines where l.Line is not null && l.Line.Value == 0 select 1).Count(); completedCount = (from l in subTaskLines where l.Line is not null && l.Line.Value == 0 && l.Completed select 1).Count(); startedCount = (from l in subTaskLines where l.Line is not null && l.Line.Value == 0 && l.Started && !l.Completed select 1).Count(); percent = allCount == 0 ? 0 : Math.Round(completedCount / allCount, 3); // newLines.Insert(0, $"- [{done}] Sub-tasks {doneCount} of {allCount} [{percent * 100}%]"); replace = $"{allCount} » {startedCount} ✓ {completedCount} {Math.Floor(percent * 100)}%".Replace(" ✓ 0 0%", string.Empty).Replace(" 100%", string.Empty).Replace(" » 0", string.Empty); } if (subTaskLines.Any(l => l.Ticks is null)) newLines = (from l in subTaskLines select l.Text).ToList(); else { newLines = (from l in subTaskLines orderby l.Completed descending, l.Started descending, l.Ticks, l.Line select l.Text.Replace($"{l.Ticks}", replace)).ToList(); } if (newLines.Count == oldLines.Count) { for (int i = 0; i < newLines.Count; i++) { if (newLines[i] != record.LineNumber.Lines[record.SubTasksLine.Value + 1 + i]) continue; lineCheck++; } if (lineCheck == newLines.Count) continue; } if (string.IsNullOrEmpty(checkDirectory)) continue; checkDirectory = Path.Combine(checkDirectory, DateTime.Now.Ticks.ToString()); _ = Directory.CreateDirectory(checkDirectory); Thread.Sleep(500); Directory.Delete(checkDirectory); reload = FileWrite(ticks, record, newLines, percent); if (!reloadAny && reload) reloadAny = true; } if (!reloadAny) break; } } }