Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions Docs/architecture/grid-style-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,19 @@ GridStyleEditorWindow

- **Facade.** `GridStyleEditorFacade` is the only public Core seam for the editor:
`Load(configDir)` → `Result<GridStyleOptions>`, `Validate(GridStyleOptions)` → `Result`,
`Save(configDir, GridStyleOptions)` → `Result`. The loader, validator, writer, and the ~12 DTOs stay
`internal`. The UI never touches Core internals directly. (Layering: the config stays in
`SemiStep.Core`, settled by review; the editor reaches it only through this facade.)
`Save(configDir, GridStyleOptions)` → `Task<Result>`. `Save` is async and runs the file write off the
UI thread (mirroring `Load`); the editor's `SaveCommand` awaits it. `Validate` stays synchronous —
`RecomputeCanSave` calls it per keystroke, and it is an in-memory pass with no I/O. The loader,
validator, writer, and the ~12 DTOs stay `internal`. The UI never touches Core internals directly.
(Layering: the config stays in `SemiStep.Core`, settled by review; the editor reaches it only through
this facade.)
- **Writer.** `GridStyleWriter` maps the record back to the DTO (`GridStyleDtoMapper`), serializes with
the underscored naming convention, re-prepends the file's leading comment block (the header), then
normalizes to LF and writes UTF-8 no-BOM via a temp-then-move atomic replace in the same `ui/` dir.
Every color DTO property carries `[YamlMember(ScalarStyle = ScalarStyle.DoubleQuoted)]` so hex values
emit quoted and quoting stays uniform.
The temp write and the header read are async (`WriteAllTextAsync` / `ReadAllLinesAsync`); the final
`File.Move` stays synchronous — there is no async move, and it is a fast metadata rename inside the
same try/catch. Every color DTO property carries `[YamlMember(ScalarStyle = ScalarStyle.DoubleQuoted)]`
so hex values emit quoted and quoting stays uniform.

The editor edits the **merged** record (defaults already applied), so `Save` writes a
fully-populated file — every key is emitted, no omitted-key preservation. That is acceptable for a
Expand Down
4 changes: 2 additions & 2 deletions SemiStep/SemiStep.Core/Configuration/GridStyleEditorFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ public Result Validate(GridStyleOptions options)
return GridStyleValidator.Validate(GridStyleDtoMapper.Map(options));
}

public Result Save(string configDir, GridStyleOptions options)
public async Task<Result> Save(string configDir, GridStyleOptions options)
{
var validation = Validate(options);
if (validation.IsFailed)
{
return validation;
}

return _gridStyleWriter.Save(configDir, options);
return await _gridStyleWriter.SaveAsync(configDir, options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ public interface IGridStyleEditorFacade

Result Validate(GridStyleOptions options);

Result Save(string configDir, GridStyleOptions options);
Task<Result> Save(string configDir, GridStyleOptions options);
}
14 changes: 7 additions & 7 deletions SemiStep/SemiStep.Core/Configuration/Loaders/GridStyleWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal sealed class GridStyleWriter

private static readonly UTF8Encoding _utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);

public Result Save(string configDirectory, GridStyleOptions options)
public async Task<Result> SaveAsync(string configDirectory, GridStyleOptions options)
{
var uiDir = Path.Combine(configDirectory, "ui");
var filePath = Path.Combine(uiDir, "grid_style.yaml");
Expand All @@ -26,11 +26,11 @@ public Result Save(string configDirectory, GridStyleOptions options)
{
Directory.CreateDirectory(uiDir);

var header = ReadLeadingCommentBlock(filePath);
var header = await ReadLeadingCommentBlockAsync(filePath);
var body = _serializer.Serialize(GridStyleDtoMapper.Map(options));
var content = Normalize(header + body);

WriteAtomic(uiDir, filePath, content);
await WriteAtomicAsync(uiDir, filePath, content);

return Result.Ok();
}
Expand All @@ -40,14 +40,14 @@ public Result Save(string configDirectory, GridStyleOptions options)
}
}

private static string ReadLeadingCommentBlock(string filePath)
private static async Task<string> ReadLeadingCommentBlockAsync(string filePath)
{
if (!File.Exists(filePath))
{
return string.Empty;
}

var lines = File.ReadAllLines(filePath);
var lines = await File.ReadAllLinesAsync(filePath);
var headerLines = new List<string>();

foreach (var line in lines)
Expand Down Expand Up @@ -76,11 +76,11 @@ private static string Normalize(string content)
return content.Replace("\r\n", "\n").Replace("\r", "\n");
}

private static void WriteAtomic(string uiDir, string filePath, string content)
private static async Task WriteAtomicAsync(string uiDir, string filePath, string content)
{
var tempPath = Path.Combine(uiDir, $".grid_style.{Guid.NewGuid():N}.tmp");

File.WriteAllText(tempPath, content, _utf8NoBom);
await File.WriteAllTextAsync(tempPath, content, _utf8NoBom);

try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public async Task SaveThenLoad_RoundTrips()

var original = (await facade.Load(tempDir.Path)).Value;

facade.Save(tempDir.Path, original).IsSuccess.Should().BeTrue();
(await facade.Save(tempDir.Path, original)).IsSuccess.Should().BeTrue();

var reloaded = (await facade.Load(tempDir.Path)).Value;

Expand All @@ -61,7 +61,7 @@ public async Task SaveThenLoad_DistinctFixture_PreservesEveryMappedField()
using var tempDir = new TempDirectory();
var facade = new GridStyleEditorFacade();

facade.Save(tempDir.Path, GridStyleOptionsTestData.Distinct()).IsSuccess.Should().BeTrue();
(await facade.Save(tempDir.Path, GridStyleOptionsTestData.Distinct())).IsSuccess.Should().BeTrue();

var reloaded = (await facade.Load(tempDir.Path)).Value;

Expand Down Expand Up @@ -101,7 +101,7 @@ public async Task Save_InvalidRecord_FailsValidationGuardBeforeWriting()
with
{ ExecutionDepth0Color = "not-a-color" };

facade.Save(tempDir.Path, invalid).IsFailed.Should().BeTrue();
(await facade.Save(tempDir.Path, invalid)).IsFailed.Should().BeTrue();

var after = await File.ReadAllTextAsync(filePath, token);
after.Should().Be(before);
Expand All @@ -128,13 +128,13 @@ await File.WriteAllTextAsync(
}

[Fact]
public void Save_WriteFailure_CarriesOriginalExceptionOnCausedBy()
public async Task Save_WriteFailure_CarriesOriginalExceptionOnCausedBy()
{
using var tempDir = new TempDirectory();
var configDirAsFile = Path.Combine(tempDir.Path, "not-a-directory");
File.WriteAllText(configDirAsFile, string.Empty);

var result = new GridStyleWriter().Save(configDirAsFile, GridStyleOptions.Default);
var result = await new GridStyleWriter().SaveAsync(configDirAsFile, GridStyleOptions.Default);

result.IsFailed.Should().BeTrue();
result.Errors.Should().ContainSingle().Which.Should().BeOfType<GridStyleSaveFailedError>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public async Task SaveRoundTrip_PreservesOrientation()
await AppendOrientation(tempDir.Path, "columns_as_steps");
var original = await LoadValidated(tempDir.Path);

new GridStyleWriter().Save(tempDir.Path, original).IsSuccess.Should().BeTrue();
(await new GridStyleWriter().SaveAsync(tempDir.Path, original)).IsSuccess.Should().BeTrue();

var reloaded = await LoadValidated(tempDir.Path);
reloaded.Orientation.Should().Be(GridOrientation.ColumnsAsSteps);
Expand All @@ -95,7 +95,7 @@ public async Task Save_MissingOrientation_EmitsExplicitDefault()
using var tempDir = CopyShippedConfig("MOCVD");
var options = await LoadValidated(tempDir.Path);

new GridStyleWriter().Save(tempDir.Path, options).IsSuccess.Should().BeTrue();
(await new GridStyleWriter().SaveAsync(tempDir.Path, options)).IsSuccess.Should().BeTrue();

var content = await File.ReadAllTextAsync(
Path.Combine(tempDir.Path, "ui", "grid_style.yaml"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public async Task Save_SemanticRoundTrip_PreservesRecord(string equipment)

var original = await LoadOptions(tempDir.Path);

new GridStyleWriter().Save(tempDir.Path, original).IsSuccess.Should().BeTrue();
(await new GridStyleWriter().SaveAsync(tempDir.Path, original)).IsSuccess.Should().BeTrue();

var reloaded = await LoadOptions(tempDir.Path);

Expand All @@ -50,7 +50,7 @@ public async Task Save_PreservesLeadingCommentBlock_WhenHeaderPresent(string equ
originalHeader.Should().NotBeEmpty();
var options = await LoadOptions(tempDir.Path);

new GridStyleWriter().Save(tempDir.Path, options).IsSuccess.Should().BeTrue();
(await new GridStyleWriter().SaveAsync(tempDir.Path, options)).IsSuccess.Should().BeTrue();

var writtenHeader = LeadingCommentBlock(await File.ReadAllTextAsync(filePath, token));
writtenHeader.Should().Be(originalHeader);
Expand All @@ -67,7 +67,7 @@ public async Task Save_InjectsNoHeader_WhenSourceHasNone()
LeadingCommentBlock(await File.ReadAllTextAsync(filePath, token)).Should().BeEmpty();
var options = await LoadOptions(tempDir.Path);

new GridStyleWriter().Save(tempDir.Path, options).IsSuccess.Should().BeTrue();
(await new GridStyleWriter().SaveAsync(tempDir.Path, options)).IsSuccess.Should().BeTrue();

var written = (await File.ReadAllTextAsync(filePath, token)).Replace("\r\n", "\n");
var firstNonBlank = written.Split('\n').First(line => line.TrimStart().Length > 0);
Expand All @@ -81,7 +81,7 @@ public async Task Save_EmitsColorScalarsDoubleQuoted()
var filePath = GridStyleFilePath(tempDir.Path);
var options = await LoadOptions(tempDir.Path);

new GridStyleWriter().Save(tempDir.Path, options).IsSuccess.Should().BeTrue();
(await new GridStyleWriter().SaveAsync(tempDir.Path, options)).IsSuccess.Should().BeTrue();

var content = await File.ReadAllTextAsync(filePath, TestContext.Current.CancellationToken);
content.Should().Contain("\"#");
Expand Down Expand Up @@ -109,7 +109,7 @@ public async Task Save_WhenMoveFails_FailsAndLeavesOriginalIntactWithNoTempOrpha
// Hold an exclusive lock on the target so the atomic File.Move cannot replace it.
using (var locking = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None))
{
result = new GridStyleWriter().Save(tempDir.Path, options);
result = await new GridStyleWriter().SaveAsync(tempDir.Path, options);
}

result.IsFailed.Should().BeTrue();
Expand All @@ -127,7 +127,7 @@ public async Task Save_WritesLfLineEndingsWithoutBom()
var filePath = GridStyleFilePath(tempDir.Path);
var options = await LoadOptions(tempDir.Path);

new GridStyleWriter().Save(tempDir.Path, options).IsSuccess.Should().BeTrue();
(await new GridStyleWriter().SaveAsync(tempDir.Path, options)).IsSuccess.Should().BeTrue();

var bytes = await File.ReadAllBytesAsync(filePath, TestContext.Current.CancellationToken);
bytes.Take(3).Should().NotEqual(new byte[] { 0xEF, 0xBB, 0xBF });
Expand All @@ -145,7 +145,7 @@ public async Task Save_UnderCommaLocale_EmitsInvariantDecimals()
try
{
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
new GridStyleWriter().Save(tempDir.Path, options).IsSuccess.Should().BeTrue();
(await new GridStyleWriter().SaveAsync(tempDir.Path, options)).IsSuccess.Should().BeTrue();
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ public Result Validate(GridStyleOptions options)
return Result.Ok();
}

public Result Save(string configDir, GridStyleOptions options)
public Task<Result> Save(string configDir, GridStyleOptions options)
{
throw failure;
}
Expand All @@ -529,9 +529,9 @@ public Result Validate(GridStyleOptions options)
return Result.Ok();
}

public Result Save(string configDir, GridStyleOptions options)
public Task<Result> Save(string configDir, GridStyleOptions options)
{
return saveResult ?? Result.Ok();
return Task.FromResult(saveResult ?? Result.Ok());
}
}
}
8 changes: 4 additions & 4 deletions SemiStep/SemiStep.UI/StyleEditor/GridStyleEditorViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ public GridStyleEditorViewModel(
_source = source;
_logger = logger;

SaveCommand = ReactiveCommand.Create(
Save,
SaveCommand = ReactiveCommand.CreateFromTask(
SaveAsync,
this.WhenAnyValue(viewModel => viewModel.CanSave));

// Modal editor: a save fault must surface on the editor's own ErrorMessage, not the
Expand Down Expand Up @@ -284,15 +284,15 @@ public GridStyleOptions BuildRecord()
};
}

private bool Save()
private async Task<bool> SaveAsync()
{
if (!CanSave)
{
ErrorMessage = Resources.EditorCannotSave;
return false;
}

var result = _gridStyleEditorFacade.Save(_configDir, BuildRecord());
var result = await _gridStyleEditorFacade.Save(_configDir, BuildRecord());
if (result.IsFailed)
{
ErrorMessage = string.Join("; ", result.Errors.Select(ReasonLocalizer.Localize));
Expand Down
Loading