diff --git a/readme.md b/readme.md index e2ccf60..052548e 100644 --- a/readme.md +++ b/readme.md @@ -103,6 +103,8 @@ The host supports a small set of commands that were useful to me: `reset`: resets the state of the incremental compiler - removes all trees and references, and asks the target to resend dependencies. +`watch {path}`: relative to the last changed file's directory, recursively finds all .cs in at `path` and below and adds them to the incremental compilation. e.g. `watch .` to include all files in the same directory as the current file being changed, without having to trigger changes against them all individually + ### target commands Commands prefixed with '!' will be sent to targets rather than the host. There are no built-in commands supported by the target, but you can add support for arbitrary commands to your `IReloadManager` by implementing the `ExecuteCommand` method. For example, a `goto` command in a prism app could be implemented using the code below, which walks the service container for pages and allows them to be chosen from a menu (if not specified as an argument to the command): diff --git a/src/components/tbc.host/Components/FileWatcher/FileWatcher.cs b/src/components/tbc.host/Components/FileWatcher/FileWatcher.cs index 305b406..1c419d1 100644 --- a/src/components/tbc.host/Components/FileWatcher/FileWatcher.cs +++ b/src/components/tbc.host/Components/FileWatcher/FileWatcher.cs @@ -1,23 +1,29 @@ using System; +using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using RxFileSystemWatcher; using Tbc.Host.Components.Abstractions; +using Tbc.Host.Components.CommandProcessor.Models; using Tbc.Host.Components.FileWatcher.Models; using Tbc.Host.Config; namespace Tbc.Host.Components.FileWatcher { - public class FileWatcher : ComponentBase, IFileWatcher + public class FileWatcher : ComponentBase, IFileWatcher, IExposeCommands { private readonly FileWatchConfig _config; private readonly IFileSystem _fileSystem; public string WatchPath { get; private set; } - + public ChangedFile LastChangedFile { get; private set; } + + private Subject _manualWatchFiles = new Subject(); public IObservable Changes { get; set; } public FileWatcher(FileWatchConfig config, IFileSystem fileSystem, ILogger logger) : base(logger) @@ -30,7 +36,10 @@ public FileWatcher(FileWatchConfig config, IFileSystem fileSystem, ILogger CreateFileSystemWatcher(string path, string mask) @@ -53,37 +62,82 @@ public IObservable CreateFileSystemWatcher(string path, string mask Logger.LogInformation("File watcher {FileWatcher} started with config {@Config}", ofsw, _config); - var ret = + var ret = Observable .Merge(ofsw.Changed, ofsw.Created, ofsw.Renamed, ofsw.Deleted) .Where(x => !_config.Ignore.Any(i => x.FullPath.Contains((string) i))) + .Select(x => x.FullPath) .Select(TryGetChangedFile) .Where(x => x != null) .Do(f => Logger.LogInformation("Changed File: {ChangedFile}", f.Path.Substring(WatchPath.Length))); - + ofsw.Start(); - + return ret; } - private ChangedFile TryGetChangedFile(FileSystemEventArgs x) + private ChangedFile TryGetChangedFile(string filePath) { try { - return new ChangedFile + return LastChangedFile = new ChangedFile { - Path = x.FullPath, - Contents = _fileSystem.File.ReadAllText(x.FullPath) + Path = filePath, + Contents = _fileSystem.File.ReadAllText(filePath) }; } catch (Exception ex) { Logger.LogError(ex, "An error occurred when attempting to read changed file at '{FilePath}'", - x.FullPath); + filePath); return null; } } + + public string Identifier => $"fw"; + public IEnumerable Commands => new[] + { + new TbcCommand + { + Command = "watch", + Execute = HandleWatchCommand + } + }; + + private Task HandleWatchCommand(string cmd, string[] args) + { + if (args.Length != 1) + { + Logger.LogWarning("Need exactly one argument ('relative path') for command 'watch'"); + return Task.CompletedTask; + } + + if (LastChangedFile is null) + { + Logger.LogWarning("No previously changed file relative to which to watch"); + return Task.CompletedTask; + } + + var inputPath = args[0]; + + var lastPath = LastChangedFile.Path; + var lastDirectory = Path.GetDirectoryName(lastPath); + + var targetPath = Path.Combine(lastDirectory, inputPath); + var filesInPath = + _fileSystem.Directory.GetFiles(targetPath, "*.cs", SearchOption.AllDirectories) + .Select(x => new FileInfo(x).FullName) + .ToList(); + + Logger.LogInformation("Watch with target {Target} resolve to {ResolvedPath} and includes files {@Files}", + inputPath, targetPath, filesInPath); + + foreach (var file in filesInPath) + _manualWatchFiles.OnNext(TryGetChangedFile(file)); + + return Task.CompletedTask; + } } }