diff --git a/CodeMonitor/Models/CleanupCodeFile.cs b/CodeMonitor/Models/CleanupCodeFile.cs new file mode 100644 index 0000000..e6cbc0f --- /dev/null +++ b/CodeMonitor/Models/CleanupCodeFile.cs @@ -0,0 +1,12 @@ +namespace CodeMonitor +{ + public class CleanupCodeFile + { + public CleanupCodeFile(string path) + { + Path = path; + } + + public string Path {get;} + } +} diff --git a/CodeMonitor/Models/CleanupCodeWatcher.cs b/CodeMonitor/Models/CleanupCodeWatcher.cs new file mode 100644 index 0000000..65e5b01 --- /dev/null +++ b/CodeMonitor/Models/CleanupCodeWatcher.cs @@ -0,0 +1,187 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using System.Collections.Generic; +using System.IO; +using System.Diagnostics; +using System.Text; +using System.Reactive.Subjects; +using System.Reactive.Linq; +using System.Reactive.Concurrency; +using DynamicData; + +namespace CodeMonitor +{ + public class CleanupCodeWatcher + { + public CleanupCodeWatcher(string slnPath) + { + Watch = Path.GetDirectoryName(slnPath); + Sln = Path.GetFileName(slnPath); + } + + private readonly SourceList results = new SourceList(); + public IObservableList ToClean => results; + + private readonly ReplaySubject active = new ReplaySubject(1); + public IObservable Active => active; + + private readonly ReplaySubject status = new ReplaySubject(); + public IObservable Status => status; + + private string Watch { get; } + private string Sln { get; } + + private readonly object _mutex = new object(); + private readonly HashSet _changed = new HashSet(); + + private void Handle(string s) + { + if (ShouldHandle(s)) + { + lock (_mutex) + { + var path = Path.GetRelativePath(Watch, s).Replace('\\', '/'); + if (_changed.Add(path)) + { + results.Add(new CleanupCodeFile(path)); + } + } + } + } + + private bool ShouldHandle(string s) + { + if (s.StartsWith(Path.Combine(Watch, ".git")) || + s.StartsWith(Path.Combine(Watch, ".vs")) || + s.EndsWith("~") || + s.EndsWith(".csproj") || + s.EndsWith(".TMP") + ) return false; + + if (Directory.Exists(s)) + { + return false; + } + + var gitPath = new Uri(Watch).MakeRelativeUri(new Uri(s)).OriginalString; + var proc = Process.Start(new ProcessStartInfo + { + WorkingDirectory = Watch, + FileName = "git", + Arguments = "check-ignore " + gitPath, + RedirectStandardOutput = true, + CreateNoWindow = true + }); + var result = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(); + + if (result != "") return false; + + return true; + } + + public void ResetChanged() + { + status.OnNext("Reseting cleanup tally against origin/dev"); + + var proc = Process.Start(new ProcessStartInfo + { + WorkingDirectory = Watch, + FileName = "git", + Arguments = "diff origin/dev --name-only", + RedirectStandardOutput = true, + CreateNoWindow = true + }); + var result = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(); + + lock (_mutex) + { + results.Clear(); + _changed.Clear(); + + var files = result.Split("\n", StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim('\r')); + + _changed.UnionWith(files); + results.AddRange(files.Select(x => new CleanupCodeFile(x))); + } + } + + public void CleanupFiles() + { + var psi = new ProcessStartInfo + { + FileName = @"c:\bin\cleanupcode.exe", + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + var config = $@" + + false + INFO + {Path.Combine(Watch, Sln)} + {Path.Combine(Watch, Sln + ".DotSettings")} + + false + + + + Built-in: Reformat Code + {string.Join(";", _changed)} +"; + + File.WriteAllText("cleanupcode.config", config); + + psi.ArgumentList.Add("--config=cleanupcode.config"); + + status.OnNext("Starting cleanup"); + var proc = Process.Start(psi); + var opt = new StringBuilder(); + proc.OutputDataReceived += (o, e) => { opt.AppendLine(e.Data); status.OnNext(e.Data); }; + proc.ErrorDataReceived += (o, e) => { opt.AppendLine(e.Data); status.OnNext(e.Data); }; + proc.BeginOutputReadLine(); + proc.BeginErrorReadLine(); + proc.WaitForExit(); + if (proc.ExitCode != 0) + { + throw new Exception("CleanupCode was sad"); + } + + lock(_mutex) + { + _changed.Clear(); + results.Clear(); + } + } + + private FileSystemWatcher w; + + internal void Stop() + { + w.Dispose(); + } + + public void Start() + { + // watch + + w = new FileSystemWatcher(Watch) + { + IncludeSubdirectories = true, + EnableRaisingEvents = true, + NotifyFilter = NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size, + Filter = "*.*" + }; + + w.Changed += (o, e) => Handle(e.FullPath); + w.Created += (o, e) => Handle(e.FullPath); + w.Renamed += (o, e) => Handle(e.FullPath); + + ResetChanged(); + } + + } +} diff --git a/CodeMonitor/Models/InspectCodeLoop.cs b/CodeMonitor/Models/InspectCodeLoop.cs index d68efca..4597474 100644 --- a/CodeMonitor/Models/InspectCodeLoop.cs +++ b/CodeMonitor/Models/InspectCodeLoop.cs @@ -21,7 +21,10 @@ public InspectCodeLoop(string slnPath) } private readonly SourceCache results = new SourceCache(x => x.File); - public IObservable> Problems => results.Connect(); + public IObservableCache Problems => results; + + private readonly ReplaySubject active = new ReplaySubject(1); + public IObservable Active => active; private readonly ReplaySubject status = new ReplaySubject(); public IObservable Status => status; @@ -65,7 +68,7 @@ private bool ShouldHandle(string s) FileName = "git", Arguments = "check-ignore " + gitPath, RedirectStandardOutput = true, - CreateNoWindow=true + CreateNoWindow = true }); var result = proc.StandardOutput.ReadToEnd(); proc.WaitForExit(); @@ -82,7 +85,8 @@ private void ExamineFiles(ICollection filePaths) var psi = new ProcessStartInfo { FileName = @"c:\bin\inspectcode.exe", - CreateNoWindow = true + CreateNoWindow = true, + RedirectStandardOutput = true }; var args = new List @@ -99,12 +103,19 @@ private void ExamineFiles(ICollection filePaths) args.Add($"--profile={tryProfile}"); } - args.AddRange(filePaths.Select(x => $"--input={x}")); + //TODO: Use a config file + if(filePaths.Count < 100) + { + args.AddRange(filePaths.Select(x => $"--input={x}")); + } + args.Add($"{Path.Combine(Watch, Sln)}"); args.ForEach(psi.ArgumentList.Add); var proc = Process.Start(psi); + proc.OutputDataReceived += (o,e) => status.OnNext(e.Data); + proc.BeginOutputReadLine(); proc.WaitForExit(); var xml = File.ReadAllText(outFile); @@ -127,7 +138,7 @@ private void ExamineFiles(ICollection filePaths) results.RemoveKeys(kees); - foreach(var x in problems) + foreach (var x in problems) { _examined.Add(x.Key); results.AddOrUpdate(new InspectCodeFileProblems(x.Key, x.Value.ToList())); @@ -139,13 +150,14 @@ private void ExamineFiles(ICollection filePaths) internal void Stop() { subscription.Dispose(); + w.Dispose(); } public void Start() { // watch - var w = new FileSystemWatcher(Watch) + w = new FileSystemWatcher(Watch) { IncludeSubdirectories = true, EnableRaisingEvents = true, @@ -163,18 +175,21 @@ public void Start() .Subscribe(Loop); } - private bool initial = true; - + private bool initial = true; + private FileSystemWatcher w; + private void Loop(long _) { // initial if (initial) { + active.OnNext(true); ExamineFiles(new List()); initial = false; + active.OnNext(false); return; - } - + } + lock (_mutex) { var gone = _examined.Select(key => (key, full: Path.Combine(Watch, key))) @@ -200,18 +215,25 @@ private void Loop(long _) } changeNote.AppendLine("Analyzing..."); - status.OnNext(changeNote.ToString()); - foreach(var g in gone) + foreach (var g in gone) { results.Remove(g); } - ExamineFiles(_changed); - - status.OnNext("Idle"); - - _changed.Clear(); + active.OnNext(true); + status.OnNext(changeNote.ToString()); + try + { + ExamineFiles(_changed); + _changed.Clear(); + } + finally + { + + status.OnNext("Idle"); + active.OnNext(false); + } } } } diff --git a/CodeMonitor/Models/InspectCodeProblem.cs b/CodeMonitor/Models/InspectCodeProblem.cs index 8a5a042..d2ddd25 100644 --- a/CodeMonitor/Models/InspectCodeProblem.cs +++ b/CodeMonitor/Models/InspectCodeProblem.cs @@ -6,7 +6,7 @@ public InspectCodeProblem(string message, int line, string type) { Message = message; Line = line; - Type = type; + Type = type; } public string Message { get; } diff --git a/CodeMonitor/ViewModels/FileToCleanViewModel.cs b/CodeMonitor/ViewModels/FileToCleanViewModel.cs new file mode 100644 index 0000000..da1e646 --- /dev/null +++ b/CodeMonitor/ViewModels/FileToCleanViewModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.ObjectModel; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using DynamicData; +using ReactiveUI; +using SharpDX.Direct2D1; + +#warning subscriptions need disposing +namespace CodeMonitor.ViewModels +{ + public class FileToCleanViewModel + { + public FileToCleanViewModel(string path) + { + Path = path; + } + + public string Path {get;} + } +} diff --git a/CodeMonitor/ViewModels/MonitoredDirectoryViewModel.cs b/CodeMonitor/ViewModels/MonitoredDirectoryViewModel.cs index 83c94e0..ffd5f44 100644 --- a/CodeMonitor/ViewModels/MonitoredDirectoryViewModel.cs +++ b/CodeMonitor/ViewModels/MonitoredDirectoryViewModel.cs @@ -5,25 +5,37 @@ using System.Linq; using System.Reactive; using System.Reactive.Linq; +using System.Threading.Tasks; using DynamicData; using ReactiveUI; - +using SharpDX.Direct2D1; + #warning subscriptions need disposing namespace CodeMonitor.ViewModels { - public class MonitoredDirectoryViewModel : ViewModelBase { public string Name { get; } + private ObservableAsPropertyHelper updating; + public bool Updating => updating.Value; + private ObservableAsPropertyHelper status; public string Status => status?.Value; public ReadOnlyObservableCollection ProblemGroups => problemGroups; private ReadOnlyObservableCollection problemGroups; - private InspectCodeLoop _inspectLoop; + public ReadOnlyObservableCollection FilesToClean => filesToClean; + private ReadOnlyObservableCollection filesToClean; + private InspectCodeLoop _inspectLoop; + private CleanupCodeWatcher _cleanupWatcher; + + + public ReactiveCommand CleanFiles { get; } + public ReactiveCommand ResetCleanFiles { get; } + public MonitoredDirectoryViewModel(string directory) { Name = Path.GetFileName(directory); @@ -35,16 +47,32 @@ public MonitoredDirectoryViewModel(string directory) _inspectLoop = new InspectCodeLoop(slns[0]); _inspectLoop.Start(); - status = _inspectLoop.Status.ToProperty(this, x => x.Status); - _inspectLoop.Problems - .TransformMany(x => x.Problems.Select(y => new ProblemViewModel(x.File, y.Message, y.Line, y.Type)), x => x.File) - .Group(x => x.File) + .Connect() .Transform(x => new ProblemGroupViewModel(x)) .ObserveOn(RxApp.MainThreadScheduler) .Bind(out problemGroups) + .DisposeMany() .Subscribe(); - } - } + + _cleanupWatcher = new CleanupCodeWatcher(slns[0]); + _cleanupWatcher.Start(); + + _cleanupWatcher.ToClean + .Connect() + .Transform(x => new FileToCleanViewModel(x.Path)) + .ObserveOn(RxApp.MainThreadScheduler) + .Bind(out filesToClean) + .DisposeMany() + .Subscribe(); + + status = _inspectLoop.Status.Concat(_cleanupWatcher.Status).ToProperty(this, x => x.Status); + updating = _inspectLoop.Active.Concat(_cleanupWatcher.Active).ToProperty(this, x => x.Updating); + + CleanFiles = ReactiveCommand.CreateFromTask(() => Task.Run(() => _cleanupWatcher.CleanupFiles())); + + ResetCleanFiles = ReactiveCommand.CreateFromTask(() => Task.Run(() => _cleanupWatcher.ResetChanged())); + } + } } } diff --git a/CodeMonitor/ViewModels/ProblemGroupViewModel.cs b/CodeMonitor/ViewModels/ProblemGroupViewModel.cs index 8b5abe0..ea18aef 100644 --- a/CodeMonitor/ViewModels/ProblemGroupViewModel.cs +++ b/CodeMonitor/ViewModels/ProblemGroupViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Dynamic; using System.IO; @@ -12,19 +13,15 @@ namespace CodeMonitor.ViewModels { #warning subscriptions need disposing - public class ProblemGroupViewModel :ViewModelBase + public class ProblemGroupViewModel : ViewModelBase { - public ProblemGroupViewModel(IGroup x) + public ProblemGroupViewModel(InspectCodeFileProblems model) { - Group = x.Key; - x.Cache.Connect() - .ObserveOn(RxApp.MainThreadScheduler) - .Bind(out problems) - .Subscribe(); + Group = model.File; + Problems = model.Problems.Select(y => new ProblemViewModel(model.File, y.Message, y.Line, y.Type)).ToList(); } public string Group { get; } - public ReadOnlyObservableCollection Problems => problems; - private readonly ReadOnlyObservableCollection problems; + public List Problems { get; } } } diff --git a/CodeMonitor/ViewModels/ProblemViewModel.cs b/CodeMonitor/ViewModels/ProblemViewModel.cs index a6a54c1..2ea666d 100644 --- a/CodeMonitor/ViewModels/ProblemViewModel.cs +++ b/CodeMonitor/ViewModels/ProblemViewModel.cs @@ -1,4 +1,6 @@ -namespace CodeMonitor.ViewModels +using System; + +namespace CodeMonitor.ViewModels { public class ProblemViewModel : ViewModelBase @@ -14,6 +16,20 @@ public ProblemViewModel(string file, string message, int line, string type) public string File { get; } public string Message { get; } public int Line { get; } - public string Type { get; } + public string Type { get; } + + public override bool Equals(object obj) + { + return obj is ProblemViewModel model && + File == model.File && + Message == model.Message && + Line == model.Line && + Type == model.Type; + } + + public override int GetHashCode() + { + return HashCode.Combine(File, Message, Line, Type); + } } } diff --git a/CodeMonitor/Views/MainWindow.xaml b/CodeMonitor/Views/MainWindow.xaml index 82ed8f9..02c85c6 100644 --- a/CodeMonitor/Views/MainWindow.xaml +++ b/CodeMonitor/Views/MainWindow.xaml @@ -1,12 +1,14 @@ - @@ -43,6 +45,21 @@ + + + + + + + + + + + + + + + + + + + + - + + + + \ No newline at end of file diff --git a/CodeMonitor/Views/MainWindow.xaml.cs b/CodeMonitor/Views/MainWindow.xaml.cs index 81416d9..b4dcff6 100644 --- a/CodeMonitor/Views/MainWindow.xaml.cs +++ b/CodeMonitor/Views/MainWindow.xaml.cs @@ -23,7 +23,12 @@ public class dmd ("file5", "msg",1,"tp1"), ("file5", "msg",1,"tp1"), ("file5", "msg",1,"tp1") - }.Select(x => new { File = x.Item1, Message = x.Item2, Line = x.Item3, Type = x.Item4 }).ToList(); + }.Select(x => new { File = x.Item1, Message = x.Item2, Line = x.Item3, Type = x.Item4 }) + .ToLookup(x => x.File) + .Select(x => new { Group = x.Key, Problems = x.Select(x => new { x.Message, x.Line, x.Type }) }).ToList(); + public object FilesToClean => Enumerable.Range(0,100).Select(x=>new { Path = "file-number-" + x }).ToArray(); + public ReactiveCommand CleanFiles => ReactiveCommand.Create(delegate{ }); + public ReactiveCommand ResetCleanFiles => ReactiveCommand.Create(delegate{ }); } public class dvm @@ -65,9 +70,9 @@ private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - var mt = this.FindControl("moveThumb"); - - Point? _lastPoint = null; + var mt = this.FindControl("moveThumb"); + + Point? _lastPoint = null; var originScren = PixelPoint.Origin; var windowStart = PixelPoint.Origin;