Forge
csharpdeeb25a2
1using CascadeIDE.Features.Workspace;
2using CascadeIDE.Models;
3using CascadeIDE.Services;
4
5namespace CascadeIDE.Features.UiChrome;
6
7/// <summary>
8/// Слияние слоёв <see cref="RepositoryWorkspaceToml"/> (ADR 0021 §2.1): <c>chrome</c>, <c>loc_limits</c>, <c>routing</c>, <c>code_navigation.presets</c>.
9/// </summary>
10public static class RepositoryWorkspaceTomlMerger
11{
12 public static RepositoryWorkspaceToml? Merge(RepositoryWorkspaceToml? lower, RepositoryWorkspaceToml? higher)
13 {
14 if (lower is null && higher is null)
15 return null;
16
17 return new RepositoryWorkspaceToml
18 {
19 Workspace = MergeWorkspace(lower?.Workspace, higher?.Workspace),
20 Chrome = MergeWorkspaceChrome(lower?.Chrome, higher?.Chrome),
21 LocLimits = MergeLocLimits(lower?.LocLimits, higher?.LocLimits),
22 Routing = MergeRouting(lower?.Routing, higher?.Routing),
23 CodeNavigation = MergeCodeNavigation(lower?.CodeNavigation, higher?.CodeNavigation),
24 CodeNavigationMap = higher?.CodeNavigationMap ?? lower?.CodeNavigationMap,
25 };
26 }
27
28 private static RepositoryWorkspaceSectionToml? MergeWorkspace(
29 RepositoryWorkspaceSectionToml? lower,
30 RepositoryWorkspaceSectionToml? higher)
31 {
32 if (lower is null && higher is null)
33 return null;
34
35 return new RepositoryWorkspaceSectionToml
36 {
37 Adr = MergeWorkspaceAdr(lower?.Adr, higher?.Adr),
38 Features = MergeWorkspaceFeatures(lower?.Features, higher?.Features),
39 DocsTemplates = MergeDocsTemplates(lower?.DocsTemplates, higher?.DocsTemplates),
40 };
41 }
42
43 private static RepositoryAdrToml? MergeWorkspaceAdr(
44 RepositoryAdrToml? lower,
45 RepositoryAdrToml? higher)
46 {
47 var map = MergeObjectDictionary(lower?.Map, higher?.Map);
48 if (map is null)
49 return null;
50 return new RepositoryAdrToml
51 {
52 AutoInclude = higher?.AutoInclude ?? lower?.AutoInclude,
53 MaxRelated = higher?.MaxRelated ?? lower?.MaxRelated,
54 Map = map
55 };
56 }
57
58 private static RepositoryFeaturesToml? MergeWorkspaceFeatures(
59 RepositoryFeaturesToml? lower,
60 RepositoryFeaturesToml? higher)
61 {
62 if (lower?.Feature is not { Count: > 0 } && higher?.Feature is not { Count: > 0 })
63 return null;
64
65 var merged = new Dictionary<string, RepositoryFeatureToml>(StringComparer.OrdinalIgnoreCase);
66 if (lower?.Feature is { Count: > 0 })
67 {
68 foreach (var f in lower.Feature)
69 {
70 var id = (f.Id ?? "").Trim();
71 if (id.Length == 0)
72 continue;
73 merged[id] = CloneFeature(f);
74 }
75 }
76
77 if (higher?.Feature is { Count: > 0 })
78 {
79 foreach (var f in higher.Feature)
80 {
81 var id = (f.Id ?? "").Trim();
82 if (id.Length == 0)
83 continue;
84 merged[id] = CloneFeature(f); // higher overrides by id
85 }
86 }
87
88 return merged.Count == 0 ? null : new RepositoryFeaturesToml { Feature = merged.Values.ToList() };
89 }
90
91 private static RepositoryFeatureToml CloneFeature(RepositoryFeatureToml f) => new()
92 {
93 Id = f.Id?.Trim(),
94 Title = f.Title?.Trim(),
95 Paths = f.Paths?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).ToList() ?? [],
96 Docs = f.Docs?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).ToList() ?? [],
97 Tags = f.Tags?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).ToList() ?? [],
98 };
99
100 private static RepositoryDocsTemplatesToml? MergeDocsTemplates(
101 RepositoryDocsTemplatesToml? lower,
102 RepositoryDocsTemplatesToml? higher)
103 {
104 if (lower is null && higher is null)
105 return null;
106
107 // Higher wins for catalog path; inline templates are merged by id (higher overrides).
108 var merged = new Dictionary<string, DocsTemplateToml>(StringComparer.OrdinalIgnoreCase);
109 if (lower?.Template is { Count: > 0 })
110 {
111 foreach (var t in lower.Template)
112 {
113 var id = (t.Id ?? "").Trim();
114 if (id.Length == 0)
115 continue;
116 merged[id] = CloneTemplate(t);
117 }
118 }
119 if (higher?.Template is { Count: > 0 })
120 {
121 foreach (var t in higher.Template)
122 {
123 var id = (t.Id ?? "").Trim();
124 if (id.Length == 0)
125 continue;
126 merged[id] = CloneTemplate(t);
127 }
128 }
129
130 return new RepositoryDocsTemplatesToml
131 {
132 CatalogPath = higher?.CatalogPath ?? lower?.CatalogPath,
133 Template = merged.Values.ToList()
134 };
135 }
136
137 private static DocsTemplateToml CloneTemplate(DocsTemplateToml t) => new()
138 {
139 Id = t.Id?.Trim(),
140 Title = t.Title?.Trim(),
141 Kind = t.Kind?.Trim(),
142 Source = t.Source?.Trim(),
143 Path = t.Path?.Trim(),
144 KnowledgeRootId = t.KnowledgeRootId?.Trim(),
145 FilePath = t.FilePath?.Trim(),
146 };
147
148 private static Dictionary<string, object>? MergeObjectDictionary(
149 Dictionary<string, object>? lower,
150 Dictionary<string, object>? higher)
151 {
152 if (lower is not { Count: > 0 } && higher is not { Count: > 0 })
153 return null;
154
155 var merged = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
156 if (lower is { Count: > 0 })
157 {
158 foreach (var kv in lower)
159 {
160 if (string.IsNullOrWhiteSpace(kv.Key))
161 continue;
162 merged[kv.Key.Trim()] = kv.Value;
163 }
164 }
165
166 if (higher is { Count: > 0 })
167 {
168 foreach (var kv in higher)
169 {
170 if (string.IsNullOrWhiteSpace(kv.Key))
171 continue;
172 merged[kv.Key.Trim()] = kv.Value;
173 }
174 }
175
176 return merged.Count > 0 ? merged : null;
177 }
178
179 private static UiWorkspaceChromeToml? MergeWorkspaceChrome(
180 UiWorkspaceChromeToml? lower,
181 UiWorkspaceChromeToml? higher)
182 {
183 if (lower is null && higher is null)
184 return null;
185
186 return new UiWorkspaceChromeToml
187 {
188 PfdRegionDefaultWidthPixels = higher?.PfdRegionDefaultWidthPixels ?? lower?.PfdRegionDefaultWidthPixels,
189 MainGridColumnSplitterWidthPixels = higher?.MainGridColumnSplitterWidthPixels ?? lower?.MainGridColumnSplitterWidthPixels,
190 BottomPanelMinRowPixels = higher?.BottomPanelMinRowPixels ?? lower?.BottomPanelMinRowPixels,
191 MfdRegionCollapsedWidthPixels = higher?.MfdRegionCollapsedWidthPixels ?? lower?.MfdRegionCollapsedWidthPixels,
192 MfdRegionExpandedDefaultWidthPixels = higher?.MfdRegionExpandedDefaultWidthPixels ?? lower?.MfdRegionExpandedDefaultWidthPixels,
193 MfdRegionExpandedPowerWidthPixels = higher?.MfdRegionExpandedPowerWidthPixels ?? lower?.MfdRegionExpandedPowerWidthPixels,
194 MfdRegionExpandedAgentChatWidthPixels = higher?.MfdRegionExpandedAgentChatWidthPixels ?? lower?.MfdRegionExpandedAgentChatWidthPixels,
195 MarkdownPreviewPlacement = higher?.MarkdownPreviewPlacement ?? lower?.MarkdownPreviewPlacement
196 };
197 }
198
199 private static UiWorkspaceLocLimitsToml? MergeLocLimits(
200 UiWorkspaceLocLimitsToml? lower,
201 UiWorkspaceLocLimitsToml? higher)
202 {
203 if (lower is null && higher is null)
204 return null;
205
206 return new UiWorkspaceLocLimitsToml
207 {
208 MediumMin = higher?.MediumMin ?? lower?.MediumMin,
209 HighMin = higher?.HighMin ?? lower?.HighMin
210 };
211 }
212
213 private static UiWorkspaceRoutingToml? MergeRouting(
214 UiWorkspaceRoutingToml? lower,
215 UiWorkspaceRoutingToml? higher)
216 {
217 var attention = MergeStringDictionary(lower?.Attention, higher?.Attention);
218 var instruments = MergeStringDictionary(lower?.Instruments, higher?.Instruments);
219 if (attention is null && instruments is null)
220 return null;
221
222 return new UiWorkspaceRoutingToml
223 {
224 Attention = attention,
225 Instruments = instruments
226 };
227 }
228
229 private static Dictionary<string, string>? MergeStringDictionary(
230 Dictionary<string, string>? lower,
231 Dictionary<string, string>? higher)
232 {
233 if (lower is not { Count: > 0 } && higher is not { Count: > 0 })
234 return null;
235
236 var merged = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
237 if (lower is { Count: > 0 })
238 {
239 foreach (var kv in lower)
240 {
241 if (string.IsNullOrWhiteSpace(kv.Key))
242 continue;
243 merged[kv.Key.Trim()] = kv.Value ?? "";
244 }
245 }
246
247 if (higher is { Count: > 0 })
248 {
249 foreach (var kv in higher)
250 {
251 if (string.IsNullOrWhiteSpace(kv.Key))
252 continue;
253 var v = kv.Value?.Trim();
254 if (string.IsNullOrEmpty(v))
255 continue;
256 merged[kv.Key.Trim()] = v;
257 }
258 }
259
260 return merged.Count > 0 ? merged : null;
261 }
262
263 private static CodeNavigationSettings? MergeCodeNavigation(CodeNavigationSettings? lower, CodeNavigationSettings? higher)
264 {
265 if (lower is null && higher is null)
266 return null;
267
268 var merged = CodeNavigationPresetsLoader.MergeBundledWithUser(
269 lower?.Presets ?? [],
270 higher?.Presets ?? []);
271 return new CodeNavigationSettings { Presets = merged };
272 }
273}
274
View only · write via MCP/CIDE