| 1 | #nullable enable |
| 2 | |
| 3 | using CascadeIDE.Services; |
| 4 | |
| 5 | namespace CascadeIDE.Features.Chat; |
| 6 | |
| 7 | /// <summary>Цепочка правил slash-preview (P5); порядок = приоритет.</summary> |
| 8 | internal 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 | |
| 144 | internal 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 | |