Forge
csharpdeeb25a2
1#nullable enable
2
3using CascadeIDE.Services;
4
5namespace CascadeIDE.Features.Chat;
6
7/// <summary>Цепочка правил slash-preview (P5); порядок = приоритет.</summary>
8internal static class SlashCommandPreviewRulePipeline
9{
10 private static readonly ISlashCommandPreviewRule[] Rules =
11 [
12 new NotSlashPreviewRule(),
13 new AnchorPeekPreviewRule(),
14 new EditorLineSelectPreviewRule(),
15 new CatalogSlashPreviewRule(),
16 new UnknownSlashPreviewRule(),
17 ];
18
19 public static SlashCommandPreviewResult Evaluate(
20 string? bufferText,
21 SlashCommandAnchorPreviewResolver? resolveAnchor)
22 {
23 foreach (var rule in Rules)
24 {
25 if (rule.TryEvaluate(bufferText, resolveAnchor, out var result))
26 return result;
27 }
28
29 return SlashCommandPreviewResult.Empty;
30 }
31
32 private interface ISlashCommandPreviewRule
33 {
34 bool TryEvaluate(
35 string? bufferText,
36 SlashCommandAnchorPreviewResolver? resolveAnchor,
37 out SlashCommandPreviewResult result);
38 }
39
40 private sealed class NotSlashPreviewRule : ISlashCommandPreviewRule
41 {
42 public bool TryEvaluate(
43 string? bufferText,
44 SlashCommandAnchorPreviewResolver? resolveAnchor,
45 out SlashCommandPreviewResult result)
46 {
47 if (ChatSlashCommandParser.IsSlashLine(bufferText))
48 return TryReject(out result);
49
50 result = SlashCommandPreviewResult.Empty;
51 return true;
52 }
53 }
54
55 private sealed class AnchorPeekPreviewRule : ISlashCommandPreviewRule
56 {
57 public bool TryEvaluate(
58 string? bufferText,
59 SlashCommandAnchorPreviewResolver? resolveAnchor,
60 out SlashCommandPreviewResult result)
61 {
62 if (!SlashLineResolver.TryResolveSlashLine((bufferText ?? "").Trim(), out var line)
63 || !line.IsCatalogMatch
64 || !SlashPathAliases.IsAnchorPeekPath(line.CanonicalPath))
65 {
66 return TryReject(out result);
67 }
68
69 result = SlashCommandPreviewRuleHelpers.BuildAnchorPeek(
70 SlashPathAliases.ExtractPeekArgs(line.CanonicalPath, line.ArgTail),
71 resolveAnchor);
72 return true;
73 }
74 }
75
76 private sealed class EditorLineSelectPreviewRule : ISlashCommandPreviewRule
77 {
78 public bool TryEvaluate(
79 string? bufferText,
80 SlashCommandAnchorPreviewResolver? resolveAnchor,
81 out SlashCommandPreviewResult result)
82 {
83 if (!SlashLineResolver.TryResolveSlashLine((bufferText ?? "").Trim(), out var line)
84 || !line.IsCatalogMatch
85 || !SlashCommandPreviewRuleHelpers.IsEditorLineSelect(line.CanonicalPath))
86 {
87 return TryReject(out result);
88 }
89
90 result = SlashCommandPreviewRuleHelpers.BuildParametricPreview(
91 line.ArgTail,
92 "Строки",
93 line.CanonicalPath);
94 return true;
95 }
96 }
97
98 private sealed class CatalogSlashPreviewRule : ISlashCommandPreviewRule
99 {
100 public bool TryEvaluate(
101 string? bufferText,
102 SlashCommandAnchorPreviewResolver? resolveAnchor,
103 out SlashCommandPreviewResult result)
104 {
105 if (!ChatSlashCommandCatalog.TryResolveInput(bufferText, out var descriptor, out var resolvedArgTail))
106 return TryReject(out result);
107
108 if (SlashIntercomPreviewPolicies.TryBuild(
109 descriptor.SlashPath,
110 resolvedArgTail,
111 resolveAnchor,
112 out result))
113 {
114 return true;
115 }
116
117 result = SlashCommandPreviewRuleHelpers.BuildCatalogCommandPreview(descriptor.SlashPath, resolvedArgTail);
118 return true;
119 }
120 }
121
122 private sealed class UnknownSlashPreviewRule : ISlashCommandPreviewRule
123 {
124 public bool TryEvaluate(
125 string? bufferText,
126 SlashCommandAnchorPreviewResolver? resolveAnchor,
127 out SlashCommandPreviewResult result)
128 {
129 if (!ChatSlashCommandParser.IsSlashLine(bufferText))
130 return TryReject(out result);
131
132 result = new("Нет такой команды.", SlashCommandPreviewKind.Error);
133 return true;
134 }
135 }
136
137 private static bool TryReject(out SlashCommandPreviewResult result)
138 {
139 result = default;
140 return false;
141 }
142}
143
144internal static class SlashCommandPreviewRuleHelpers
145{
146 public static SlashCommandPreviewResult BuildCatalogCommandPreview(string slashPath, string? argsTail)
147 {
148 var tail = (argsTail ?? "").Trim();
149 if (tail.Length == 0)
150 return new($"Команда «{slashPath}» — Enter для выполнения.", SlashCommandPreviewKind.Ok);
151
152 return new($"Команда «{slashPath}».", SlashCommandPreviewKind.Ok);
153 }
154
155 public static SlashCommandPreviewResult BuildAnchorPeek(string? tail, SlashCommandAnchorPreviewResolver? resolveAnchor)
156 {
157 var raw = (tail ?? "").Trim();
158 if (raw.Length == 0)
159 return new("Укажи № якоря (1…) или 8 hex: /anchor peek 1.", SlashCommandPreviewKind.Incomplete);
160
161 if (resolveAnchor is not null && resolveAnchor(raw, out var resolved))
162 return resolved;
163
164 if (raw.Length > 0 && raw.Length < 8 && IntercomAnchorSlash.IsPartialHexAnchorId(raw))
165 return new("Id якоря — 8 hex (как a:abcd1234).", SlashCommandPreviewKind.Incomplete);
166
167 if (!IntercomAnchorSlash.TryNormalizeAnchorId(tail, out _, out var syntaxError))
168 return new(syntaxError, SlashCommandPreviewKind.Error);
169
170 return new($"Peek: {raw}", SlashCommandPreviewKind.Incomplete);
171 }
172
173 public static SlashCommandPreviewResult BuildParametricPreview(
174 string tail,
175 string unitLabel,
176 string slashPath)
177 {
178 var trimmed = tail.Trim();
179 if (trimmed.Length == 0)
180 {
181 if (!ParametricSegmentListParser.TryParse("", out _, out var needArgsError))
182 needArgsError = "Укажи аргументы диапазона.";
183
184 return new(needArgsError, SlashCommandPreviewKind.Incomplete);
185 }
186
187 if (!ParametricSegmentListParser.TryParse(trimmed, out var segments, out var error))
188 return new(error, SlashCommandPreviewKind.Error);
189
190 if (!ChatSlashCommandCatalog.TryResolveCanonical(slashPath, tail, out _))
191 return new("Нет такой команды.", SlashCommandPreviewKind.Error);
192
193 return new(
194 ParametricSegmentListParser.FormatSummary(segments, unitLabel),
195 SlashCommandPreviewKind.Ok);
196 }
197
198 public static bool IsEditorLineSelect(string canonicalPath) =>
199 string.Equals(canonicalPath, "/editor line select", StringComparison.OrdinalIgnoreCase);
200}
201
View only · write via MCP/CIDE