Forge
csharpdeeb25a2
1#nullable enable
2using CascadeIDE.Features.UiChrome;
3using CascadeIDE.Models;
4
5namespace CascadeIDE.Services;
6
7/// <summary>
8/// Шипнутые пресеты подписей ветвей IF: <c>CodeNavigation/condition-branch-label-presets.toml</c>
9/// (диск / embedded); overlay репозитория <c>.cascade/workspace.toml</c>;
10/// затем <c>[[code_navigation_map.condition_branch.presets]]</c> в settings.
11/// Merge по <see cref="CodeNavigationMapConditionBranchPresetEntry.Id"/>.
12/// </summary>
13public static class CodeNavigationMapConditionBranchPresetsLoader
14{
15 public const string BundledRelativePath = "CodeNavigation/condition-branch-label-presets.toml";
16
17 private sealed class BundledRoot
18 {
19 public CodeNavigationMapSettings? CodeNavigationMap { get; set; }
20 }
21
22 public static string GetEmbeddedBundledToml()
23 {
24 if (!BundledAppContent.TryReadDiskThenEmbedded(BundledRelativePath, out var text) || string.IsNullOrWhiteSpace(text))
25 throw new InvalidOperationException(
26 $"Missing bundled {BundledRelativePath} (disk under AppContext.BaseDirectory or embedded resource in CascadeIDE assembly).");
27 return text;
28 }
29
30 public static IReadOnlyList<CodeNavigationMapConditionBranchPresetEntry> LoadBundledEntriesOrFallback()
31 {
32 if (!BundledAppContent.TryReadDiskThenEmbedded(BundledRelativePath, out var raw))
33 return [];
34 try
35 {
36 var root = CascadeTomlSerializer.Deserialize<BundledRoot>(raw.Trim());
37 return root?.CodeNavigationMap?.ConditionBranch?.Presets is { Count: > 0 } list
38 ? list
39 : [];
40 }
41 catch
42 {
43 return [];
44 }
45 }
46
47 /// <summary>Бандл → репо (<paramref name="solutionPath"/>) → пользовательский overlay в <paramref name="map"/>.</summary>
48 public static IReadOnlyList<CodeNavigationMapConditionBranchPresetEntry> GetEffectivePresets(
49 CodeNavigationMapSettings? map,
50 string? solutionPath = null)
51 {
52 var bundled = LoadBundledEntriesOrFallback();
53 var repository = LoadRepositoryPresetsFromSolutionDirectory(solutionPath);
54 var afterRepo = MergeLayers(bundled, repository);
55 var user = map?.ConditionBranch?.Presets ?? [];
56 return MergeLayers(afterRepo, user);
57 }
58
59 /// <summary>Читает <c>.cascade/workspace.toml</c>; возвращает пресеты ветвей или пустой список.</summary>
60 public static IReadOnlyList<CodeNavigationMapConditionBranchPresetEntry> LoadRepositoryPresetsFromSolutionDirectory(
61 string? solutionPath)
62 {
63 var dir = NormalizeRepositoryRoot(solutionPath);
64 if (dir is null)
65 return [];
66
67 try
68 {
69 var path = Path.Combine(dir, ".cascade", "workspace.toml");
70 if (!File.Exists(path))
71 return [];
72
73 var text = File.ReadAllText(path);
74 var ui = CascadeTomlSerializer.Deserialize<Features.Workspace.RepositoryWorkspaceToml>(text);
75 if (ui?.CodeNavigationMap?.ConditionBranch?.Presets is not { Count: > 0 } presets)
76 return [];
77
78 return presets;
79 }
80 catch
81 {
82 return [];
83 }
84 }
85
86 public static List<CodeNavigationMapConditionBranchPresetEntry> MergeLayers(
87 IReadOnlyList<CodeNavigationMapConditionBranchPresetEntry> baseLayer,
88 IReadOnlyList<CodeNavigationMapConditionBranchPresetEntry> overlay)
89 {
90 var dict = new Dictionary<string, CodeNavigationMapConditionBranchPresetEntry>(StringComparer.OrdinalIgnoreCase);
91 foreach (var e in baseLayer)
92 {
93 if (string.IsNullOrWhiteSpace(e.Id))
94 continue;
95 dict[e.Id.Trim()] = CloneEntry(e);
96 }
97
98 foreach (var e in overlay)
99 {
100 if (string.IsNullOrWhiteSpace(e.Id))
101 continue;
102 dict[e.Id.Trim()] = CloneEntry(e);
103 }
104
105 return dict.Values.OrderBy(x => x.Id, StringComparer.Ordinal).ToList();
106 }
107
108 /// <summary>Alias for tests: bundled + user without repository.</summary>
109 public static List<CodeNavigationMapConditionBranchPresetEntry> MergeBundledWithUser(
110 IReadOnlyList<CodeNavigationMapConditionBranchPresetEntry> bundled,
111 IReadOnlyList<CodeNavigationMapConditionBranchPresetEntry> user) =>
112 MergeLayers(bundled, user);
113
114 private static string? NormalizeRepositoryRoot(string? solutionPath)
115 {
116 if (string.IsNullOrWhiteSpace(solutionPath))
117 return null;
118 try
119 {
120 var p = CanonicalFilePath.Normalize(solutionPath.Trim());
121 if (File.Exists(p))
122 return Path.GetDirectoryName(p);
123 return Directory.Exists(p) ? p : null;
124 }
125 catch
126 {
127 return null;
128 }
129 }
130
131 private static CodeNavigationMapConditionBranchPresetEntry CloneEntry(CodeNavigationMapConditionBranchPresetEntry e) =>
132 new()
133 {
134 Id = e.Id,
135 Positive = e.Positive,
136 Negative = e.Negative
137 };
138}
139
View only · write via MCP/CIDE