Skip to content

Commit 7eb685a

Browse files
Mikeclaude
andcommitted
feat(Workspace.Roslyn): Add NullRoslynWorkspace and ActiveWorkspaceProxy
- NullRoslynWorkspace: Null object pattern that throws helpful errors when accessed without a loaded solution - ActiveWorkspaceProxy: Delegates to IWorkspaceManager.ActiveWorkspace for multi-workspace support These enable the MCP server to start without a solution loaded, providing clear error messages when tools are used. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7542a82 commit 7eb685a

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using FractalDataWorks.Results;
4+
using Microsoft.CodeAnalysis;
5+
6+
namespace FractalDataWorks.Workspace.Roslyn;
7+
8+
/// <summary>
9+
/// A proxy implementation of <see cref="IRoslynWorkspace"/> that delegates to the active workspace
10+
/// from the workspace manager. This allows singleton tools to always use the current active workspace.
11+
/// </summary>
12+
public sealed class ActiveWorkspaceProxy : IRoslynWorkspace
13+
{
14+
private readonly IWorkspaceManager _workspaceManager;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="ActiveWorkspaceProxy"/> class.
18+
/// </summary>
19+
public ActiveWorkspaceProxy(IWorkspaceManager workspaceManager)
20+
{
21+
_workspaceManager = workspaceManager ?? throw new ArgumentNullException(nameof(workspaceManager));
22+
}
23+
24+
private IRoslynWorkspace GetWorkspace() =>
25+
_workspaceManager.ActiveWorkspace
26+
?? throw new InvalidOperationException("No solution is loaded. Use the OpenSolution tool to load a solution first.");
27+
28+
/// <inheritdoc/>
29+
public Solution CurrentSolution => GetWorkspace().CurrentSolution;
30+
31+
/// <inheritdoc/>
32+
public Solution? BaselineSolution => GetWorkspace().BaselineSolution;
33+
34+
/// <inheritdoc/>
35+
public Solution Current => GetWorkspace().Current;
36+
37+
/// <inheritdoc/>
38+
public Solution? Baseline => GetWorkspace().Baseline;
39+
40+
/// <inheritdoc/>
41+
public int SnapshotCount => _workspaceManager.ActiveWorkspace?.SnapshotCount ?? 0;
42+
43+
/// <inheritdoc/>
44+
public bool HasChanges => _workspaceManager.ActiveWorkspace?.HasChanges ?? false;
45+
46+
/// <inheritdoc/>
47+
public void UpdateSolution(Solution solution) => GetWorkspace().UpdateSolution(solution);
48+
49+
/// <inheritdoc/>
50+
public void Update(Solution state) => GetWorkspace().Update(state);
51+
52+
/// <inheritdoc/>
53+
public void SetBaseline(Solution state) => GetWorkspace().SetBaseline(state);
54+
55+
/// <inheritdoc/>
56+
public string CreateSnapshot(string name, string description) =>
57+
GetWorkspace().CreateSnapshot(name, description);
58+
59+
/// <inheritdoc/>
60+
public IGenericResult<Solution> RestoreSnapshot(string snapshotId) =>
61+
GetWorkspace().RestoreSnapshot(snapshotId);
62+
63+
/// <inheritdoc/>
64+
public IEnumerable<SnapshotInfo> ListSnapshots() =>
65+
_workspaceManager.ActiveWorkspace?.ListSnapshots() ?? [];
66+
67+
/// <inheritdoc/>
68+
public bool RemoveSnapshot(string snapshotId) =>
69+
_workspaceManager.ActiveWorkspace?.RemoveSnapshot(snapshotId) ?? false;
70+
71+
/// <inheritdoc/>
72+
public void ClearSnapshots() => _workspaceManager.ActiveWorkspace?.ClearSnapshots();
73+
74+
/// <inheritdoc/>
75+
public IReadOnlyDictionary<string, string> GetChangesFromBaseline() =>
76+
_workspaceManager.ActiveWorkspace?.GetChangesFromBaseline()
77+
?? new Dictionary<string, string>(StringComparer.Ordinal);
78+
79+
/// <inheritdoc/>
80+
public IReadOnlyDictionary<string, string>? GetChangesFromSnapshot(string snapshotId) =>
81+
_workspaceManager.ActiveWorkspace?.GetChangesFromSnapshot(snapshotId);
82+
83+
/// <inheritdoc/>
84+
public void ApplyDocumentChanges(IReadOnlyDictionary<string, string> documentChanges) =>
85+
GetWorkspace().ApplyDocumentChanges(documentChanges);
86+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using FractalDataWorks.Results;
4+
using Microsoft.CodeAnalysis;
5+
6+
namespace FractalDataWorks.Workspace.Roslyn;
7+
8+
/// <summary>
9+
/// A null object implementation of <see cref="IRoslynWorkspace"/> that throws helpful errors when accessed.
10+
/// </summary>
11+
/// <remarks>
12+
/// This allows the MCP server to start without a solution loaded. Tools will fail gracefully
13+
/// at execution time with a clear message to use OpenSolution first.
14+
/// </remarks>
15+
public sealed class NullRoslynWorkspace : IRoslynWorkspace
16+
{
17+
/// <summary>
18+
/// Gets the singleton instance of the null workspace.
19+
/// </summary>
20+
public static NullRoslynWorkspace Instance { get; } = new();
21+
22+
private NullRoslynWorkspace() { }
23+
24+
private static InvalidOperationException NoWorkspaceError() =>
25+
new("No solution is loaded. Use the OpenSolution tool to load a solution first.");
26+
27+
/// <inheritdoc/>
28+
public Solution CurrentSolution => throw NoWorkspaceError();
29+
30+
/// <inheritdoc/>
31+
public Solution? BaselineSolution => throw NoWorkspaceError();
32+
33+
/// <inheritdoc/>
34+
public Solution Current => throw NoWorkspaceError();
35+
36+
/// <inheritdoc/>
37+
public Solution? Baseline => throw NoWorkspaceError();
38+
39+
/// <inheritdoc/>
40+
public int SnapshotCount => 0;
41+
42+
/// <inheritdoc/>
43+
public bool HasChanges => false;
44+
45+
/// <inheritdoc/>
46+
public void UpdateSolution(Solution solution) => throw NoWorkspaceError();
47+
48+
/// <inheritdoc/>
49+
public void Update(Solution state) => throw NoWorkspaceError();
50+
51+
/// <inheritdoc/>
52+
public void SetBaseline(Solution state) => throw NoWorkspaceError();
53+
54+
/// <inheritdoc/>
55+
public string CreateSnapshot(string name, string description) => throw NoWorkspaceError();
56+
57+
/// <inheritdoc/>
58+
public IGenericResult<Solution> RestoreSnapshot(string snapshotId) =>
59+
GenericResult<Solution>.Failure("No solution is loaded. Use the OpenSolution tool to load a solution first.");
60+
61+
/// <inheritdoc/>
62+
public IEnumerable<SnapshotInfo> ListSnapshots() => [];
63+
64+
/// <inheritdoc/>
65+
public bool RemoveSnapshot(string snapshotId) => false;
66+
67+
/// <inheritdoc/>
68+
public void ClearSnapshots() { }
69+
70+
/// <inheritdoc/>
71+
public IReadOnlyDictionary<string, string> GetChangesFromBaseline() =>
72+
new Dictionary<string, string>(StringComparer.Ordinal);
73+
74+
/// <inheritdoc/>
75+
public IReadOnlyDictionary<string, string>? GetChangesFromSnapshot(string snapshotId) => null;
76+
77+
/// <inheritdoc/>
78+
public void ApplyDocumentChanges(IReadOnlyDictionary<string, string> documentChanges) =>
79+
throw NoWorkspaceError();
80+
}

0 commit comments

Comments
 (0)