Skip to content

Commit 26e0cc3

Browse files
committed
add watch command
1 parent 634a530 commit 26e0cc3

File tree

4 files changed

+196
-153
lines changed

4 files changed

+196
-153
lines changed

src/docfx/Models/BuildCommand.cs

Lines changed: 2 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -22,37 +22,10 @@ public override int Execute(CommandContext context, BuildCommandOptions settings
2222

2323
var (config, baseDirectory) = Docset.GetConfig(settings.ConfigFile);
2424
MergeOptionsToConfig(settings, config.build, baseDirectory);
25-
var conf = new BuildOptions();
26-
var serveDirectory = RunBuild.Exec(config.build, conf, baseDirectory, settings.OutputFolder);
25+
var serveDirectory = RunBuild.Exec(config.build, new(), baseDirectory, settings.OutputFolder);
2726

28-
void onChange()
29-
{
30-
RunBuild.Exec(config.build, conf, baseDirectory, settings.OutputFolder);
31-
}
32-
33-
if (settings is { Serve: true, Watch: true })
34-
{
35-
using var watcher = Watch(baseDirectory, config.build, onChange);
36-
RunServe.Exec(serveDirectory, settings.Host, settings.Port, settings.OpenBrowser, settings.OpenFile);
37-
}
38-
else if (settings.Watch)
39-
{
40-
using var watcher = Watch(baseDirectory, config.build, onChange);
41-
42-
// just block but here we can't use the host mecanism
43-
// since we didn't start the server so use console one
44-
using var canceller = new CancellationTokenSource();
45-
Console.CancelKeyPress += (sender, args) => canceller.Cancel();
46-
Task.Delay(Timeout.Infinite, canceller.Token).Wait();
47-
}
48-
else if (settings.Serve)
49-
{
27+
if (settings.Serve)
5028
RunServe.Exec(serveDirectory, settings.Host, settings.Port, settings.OpenBrowser, settings.OpenFile);
51-
}
52-
else
53-
{
54-
onChange();
55-
}
5629
});
5730
}
5831

@@ -150,124 +123,4 @@ void SetGlobalMetadataFromCommandLineArgs()
150123
}
151124
}
152125
}
153-
154-
// For now it is a simplistic implementation, in particular on the glob to filter mappping
155-
// but it should be sufficient for most cases.
156-
internal static IDisposable Watch(string baseDir, BuildJsonConfig config, Action onChange)
157-
{
158-
FileSystemWatcher watcher = new(baseDir)
159-
{
160-
IncludeSubdirectories = true,
161-
NotifyFilter = NotifyFilters.Attributes | NotifyFilters.Size | NotifyFilters.FileName |
162-
NotifyFilters.DirectoryName | NotifyFilters.LastWrite
163-
};
164-
165-
if (WatchAll(config))
166-
{
167-
watcher.Filters.Add("*.*");
168-
}
169-
else
170-
{
171-
RegisterFiles(watcher, config.Content);
172-
RegisterFiles(watcher, config.Resource);
173-
174-
IEnumerable<string> forcedFiles = ["docfx.json", "*.md", "toc.yml"];
175-
foreach (var forcedFile in forcedFiles)
176-
{
177-
if (!watcher.Filters.Any(f => f == forcedFile))
178-
{
179-
watcher.Filters.Add(forcedFile);
180-
}
181-
}
182-
}
183-
184-
// avoid to call onChange() in chain so await "last" event before re-rendering
185-
var cancellation = new CancellationTokenSource[] { null };
186-
async void debounce()
187-
{
188-
var token = new CancellationTokenSource();
189-
lock (cancellation)
190-
{
191-
ResetToken(cancellation);
192-
cancellation[0] = token;
193-
}
194-
195-
await Task.Delay(100, token.Token);
196-
if (!token.IsCancellationRequested)
197-
{
198-
onChange();
199-
}
200-
}
201-
202-
watcher.Changed += (_, _) => debounce();
203-
watcher.Created += (_, _) => debounce();
204-
watcher.Deleted += (_, _) => debounce();
205-
watcher.Renamed += (_, _) => debounce();
206-
watcher.EnableRaisingEvents = true;
207-
208-
return new DisposableAction(() =>
209-
{
210-
watcher.Dispose();
211-
lock (cancellation)
212-
{
213-
ResetToken(cancellation);
214-
}
215-
});
216-
}
217-
218-
private static void ResetToken(CancellationTokenSource[] cancellation)
219-
{
220-
var token = cancellation[0];
221-
if (token is not null && !token.IsCancellationRequested)
222-
{
223-
token.Cancel();
224-
token.Dispose();
225-
}
226-
}
227-
228-
internal static bool WatchAll(BuildJsonConfig config)
229-
{
230-
return ((IEnumerable<FileMapping>)[config.Resource, config.Content])
231-
.Where(it => it is not null)
232-
.SelectMany(it => it.Items)
233-
.SelectMany(it => it.Files)
234-
.Any(it => it.EndsWith("**"));
235-
}
236-
237-
internal static void RegisterFiles(FileSystemWatcher watcher, FileMapping content)
238-
{
239-
foreach (var pattern in content?
240-
.Items?
241-
.SelectMany(it => it.Files)
242-
.SelectMany(SanitizePatternForWatcher)
243-
.Distinct()
244-
.ToList())
245-
{
246-
watcher.Filters.Add(pattern);
247-
}
248-
}
249-
250-
// as of now it can list too much files but will less hurt to render more often with deboucning
251-
// than not rendering when needed.
252-
internal static IEnumerable<string> SanitizePatternForWatcher(string file)
253-
{
254-
var name = file[(file.LastIndexOf('.') + 1)..]; // "**/images/**/*.png" => "*.png"
255-
if (name.EndsWith('}')) // "**/*.{md,yml}" => "*.md" and "*.yml"
256-
{
257-
var start = name.IndexOf('{');
258-
if (start > 0)
259-
{
260-
var prefix = file[0..start];
261-
return file[(start + 1)..^1]
262-
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
263-
.Select(extension => $"{prefix}{extension}");
264-
}
265-
}
266-
return [name];
267-
}
268-
269-
internal class DisposableAction(Action action) : IDisposable
270-
{
271-
public void Dispose() => action();
272-
}
273126
}

src/docfx/Models/BuildCommandOptions.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ internal class BuildCommandOptions : LogOptions
4040
[CommandOption("-s|--serve")]
4141
public bool Serve { get; set; }
4242

43-
[Description("Should directory be watched and website re-rendered on changes.")]
44-
[CommandOption("-w|--watch")]
45-
public bool Watch { get; set; }
46-
4743
[Description("Specify the hostname of the hosted website (e.g., 'localhost' or '*')")]
4844
[CommandOption("-n|--hostname")]
4945
public string Host { get; set; }

src/docfx/Models/WatchCommand.cs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Docfx.Common;
5+
using Spectre.Console.Cli;
6+
7+
namespace Docfx;
8+
9+
internal class WatchCommand : Command<WatchCommandOptions>
10+
{
11+
public override int Execute(CommandContext context, WatchCommandOptions settings)
12+
{
13+
return CommandHelper.Run(settings, () =>
14+
{
15+
var (config, baseDirectory) = Docset.GetConfig(settings.ConfigFile);
16+
BuildCommand.MergeOptionsToConfig(settings, config.build, baseDirectory);
17+
var conf = new BuildOptions();
18+
var serveDirectory = RunBuild.Exec(config.build, conf, baseDirectory, settings.OutputFolder);
19+
20+
void onChange()
21+
{
22+
RunBuild.Exec(config.build, conf, baseDirectory, settings.OutputFolder);
23+
}
24+
25+
if (settings is { Serve: true, Watch: true })
26+
{
27+
using var watcher = Watch(baseDirectory, config.build, onChange);
28+
Serve(serveDirectory, settings.Host, settings.Port, settings.OpenBrowser, settings.OpenFile);
29+
}
30+
else if (settings.Watch)
31+
{
32+
using var watcher = Watch(baseDirectory, config.build, onChange);
33+
34+
// just block but here we can't use the host mecanism
35+
// since we didn't start the server so use console one
36+
using var canceller = new CancellationTokenSource();
37+
Console.CancelKeyPress += (sender, args) => canceller.Cancel();
38+
Task.Delay(Timeout.Infinite, canceller.Token).Wait();
39+
}
40+
else if (settings.Serve)
41+
{
42+
RunServe.Exec(serveDirectory, settings.Host, settings.Port, settings.OpenBrowser, settings.OpenFile);
43+
}
44+
else
45+
{
46+
onChange();
47+
}
48+
});
49+
}
50+
51+
internal void Serve(string serveDirectory, string host, int? port, bool openBrowser, string openFile) {
52+
if (CommandHelper.IsTcpPortAlreadyUsed(host, port))
53+
{
54+
Logger.LogError($"Serve option specified. But TCP port {port ?? 8080} is already being in use.");
55+
return;
56+
}
57+
RunServe.Exec(serveDirectory, host, port, openBrowser, openFile);
58+
}
59+
60+
// For now it is a simplistic implementation, in particular on the glob to filter mappping
61+
// but it should be sufficient for most cases.
62+
internal static IDisposable Watch(string baseDir, BuildJsonConfig config, Action onChange)
63+
{
64+
FileSystemWatcher watcher = new(baseDir)
65+
{
66+
IncludeSubdirectories = true,
67+
NotifyFilter = NotifyFilters.Attributes | NotifyFilters.Size | NotifyFilters.FileName |
68+
NotifyFilters.DirectoryName | NotifyFilters.LastWrite
69+
};
70+
71+
if (WatchAll(config))
72+
{
73+
watcher.Filters.Add("*.*");
74+
}
75+
else
76+
{
77+
RegisterFiles(watcher, config.Content);
78+
RegisterFiles(watcher, config.Resource);
79+
80+
IEnumerable<string> forcedFiles = ["docfx.json", "*.md", "toc.yml"];
81+
foreach (var forcedFile in forcedFiles)
82+
{
83+
if (!watcher.Filters.Any(f => f == forcedFile))
84+
{
85+
watcher.Filters.Add(forcedFile);
86+
}
87+
}
88+
}
89+
90+
// avoid to call onChange() in chain so await "last" event before re-rendering
91+
var cancellation = new CancellationTokenSource[] { null };
92+
async void debounce()
93+
{
94+
var token = new CancellationTokenSource();
95+
lock (cancellation)
96+
{
97+
ResetToken(cancellation);
98+
cancellation[0] = token;
99+
}
100+
101+
await Task.Delay(100, token.Token);
102+
if (!token.IsCancellationRequested)
103+
{
104+
onChange();
105+
}
106+
}
107+
108+
watcher.Changed += (_, _) => debounce();
109+
watcher.Created += (_, _) => debounce();
110+
watcher.Deleted += (_, _) => debounce();
111+
watcher.Renamed += (_, _) => debounce();
112+
watcher.EnableRaisingEvents = true;
113+
114+
return new DisposableAction(() =>
115+
{
116+
watcher.Dispose();
117+
lock (cancellation)
118+
{
119+
ResetToken(cancellation);
120+
}
121+
});
122+
}
123+
124+
private static void ResetToken(CancellationTokenSource[] cancellation)
125+
{
126+
var token = cancellation[0];
127+
if (token is not null && !token.IsCancellationRequested)
128+
{
129+
token.Cancel();
130+
token.Dispose();
131+
}
132+
}
133+
134+
internal static bool WatchAll(BuildJsonConfig config)
135+
{
136+
return ((IEnumerable<FileMapping>)[config.Resource, config.Content])
137+
.Where(it => it is not null)
138+
.SelectMany(it => it.Items)
139+
.SelectMany(it => it.Files)
140+
.Any(it => it.EndsWith("**"));
141+
}
142+
143+
internal static void RegisterFiles(FileSystemWatcher watcher, FileMapping content)
144+
{
145+
foreach (var pattern in content?
146+
.Items?
147+
.SelectMany(it => it.Files)
148+
.SelectMany(SanitizePatternForWatcher)
149+
.Distinct()
150+
.ToList())
151+
{
152+
watcher.Filters.Add(pattern);
153+
}
154+
}
155+
156+
// as of now it can list too much files but will less hurt to render more often with deboucning
157+
// than not rendering when needed.
158+
internal static IEnumerable<string> SanitizePatternForWatcher(string file)
159+
{
160+
var name = file[(file.LastIndexOf('.') + 1)..]; // "**/images/**/*.png" => "*.png"
161+
if (name.EndsWith('}')) // "**/*.{md,yml}" => "*.md" and "*.yml"
162+
{
163+
var start = name.IndexOf('{');
164+
if (start > 0)
165+
{
166+
var prefix = file[0..start];
167+
return file[(start + 1)..^1]
168+
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
169+
.Select(extension => $"{prefix}{extension}");
170+
}
171+
}
172+
return [name];
173+
}
174+
175+
internal class DisposableAction(Action action) : IDisposable
176+
{
177+
public void Dispose() => action();
178+
}
179+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.ComponentModel;
5+
using Spectre.Console.Cli;
6+
7+
namespace Docfx;
8+
9+
[Description("Generate client-only website combining API in YAML files and conceptual files and watch them for changes")]
10+
internal class WatchCommandOptions : BuildCommandOptions
11+
{
12+
[Description("Should directory be watched and website re-rendered on changes.")]
13+
[CommandOption("-w|--watch")]
14+
public bool Watch { get; set; }
15+
}

0 commit comments

Comments
 (0)