Forge
csharpdeeb25a2
1#nullable enable
2
3using CascadeIDE.Features.Chat.AnchorPeek;
4using CascadeIDE.Models.Intercom;
5using CascadeIDE.Services.Intercom;
6
7namespace CascadeIDE.Features.Chat;
8
9/// <summary>Фасад форматирования и anchor peek для slash/CCL (ADR 0128 §10.1).</summary>
10internal static class IntercomAnchorSlash
11{
12 public static bool IsAnchorPeekPath(string canonicalPath) =>
13 SlashPathAliases.IsAnchorPeekPath(canonicalPath);
14
15 public static string? ExtractPeekIdTail(string canonicalPath, string? argTail) =>
16 SlashPathAliases.ExtractPeekArgs(canonicalPath, argTail);
17
18 public static bool IsAnchorPeekHexIdEntryBody(string body) =>
19 isAnchorPeekHexIdEntryBody(SlashPathAliases.NormalizeCompletionBody(body));
20
21 internal static bool IsPartialHexAnchorId(string raw) =>
22 AnchorPeekTargetParser.IsPartialHex(raw);
23
24 public static bool TryResolvePeekOrdinal(
25 string? raw,
26 IReadOnlyList<AttachmentAnchor> selectedMessageAnchors,
27 out AttachmentAnchor anchor,
28 out int ordinal) =>
29 tryResolveOrdinalOnly(raw, selectedMessageAnchors, out anchor, out ordinal);
30
31 public static bool TryFormatPeekOrdinalError(string? raw, int attachmentCount, out string error)
32 {
33 error = "";
34 if (!AnchorPeekTargetParser.TryParse(raw ?? "", out var target, out _))
35 return false;
36
37 if (target.Kind != AnchorPeekTargetKind.Ordinal)
38 return false;
39
40 if (attachmentCount > 0 && target.Ordinal >= 1 && target.Ordinal <= attachmentCount)
41 return false;
42
43 error = target.Ordinal < 1
44 ? "№ якоря — целое число от 1."
45 : attachmentCount <= 0
46 ? "Выбери сообщение с вложениями: /intercom message select <n>."
47 : $"Якоря #{target.Ordinal} нет (в сообщении {attachmentCount}). /intercom message anchors list";
48 return true;
49 }
50
51 public static bool TryNormalizeAnchorId(string? raw, out string shortId, out string error)
52 {
53 shortId = "";
54 error = "";
55 if (!AnchorPeekTargetParser.TryParse(raw, out var target, out error))
56 return false;
57
58 if (target.Kind != AnchorPeekTargetKind.HexId)
59 {
60 error = AnchorPeekTargetParser.InvalidHexError;
61 return false;
62 }
63
64 shortId = target.HexId;
65 return true;
66 }
67
68 public static string FormatListLine(AttachmentAnchor anchor, int? messageOrdinal = null)
69 {
70 var id = string.IsNullOrWhiteSpace(anchor.Id) ? "?" : $"a:{anchor.Id}";
71 var label = anchor.DisplayLabel ?? anchor.MemberKey ?? anchor.File ?? "—";
72 var outcome = FormatOutcomeShort(anchor.ResolveOutcome);
73 var loc = FormatLocationShort(anchor);
74 var ord = messageOrdinal is { } o ? $"#{o} " : "";
75 return $"{ord}{id} {label} {outcome} {loc}".TrimEnd();
76 }
77
78 public static string FormatOutcomeShort(string? resolveOutcome)
79 {
80 var o = resolveOutcome?.Trim();
81 if (string.Equals(o, IntercomAttachmentRevealPlan.OutcomeResolved, StringComparison.OrdinalIgnoreCase))
82 return "resolved";
83 if (string.Equals(o, IntercomAttachmentRevealPlan.OutcomeFileMissing, StringComparison.OrdinalIgnoreCase))
84 return "file_missing";
85 if (string.Equals(o, IntercomAttachmentRevealPlan.OutcomeMemberNotFound, StringComparison.OrdinalIgnoreCase))
86 return "member_not_found";
87 if (string.Equals(o, IntercomAttachmentRevealPlan.OutcomeExcerptOnly, StringComparison.OrdinalIgnoreCase))
88 return "excerpt_only";
89 return string.IsNullOrWhiteSpace(o) ? "—" : o;
90 }
91
92 private static bool tryResolveOrdinalOnly(
93 string? raw,
94 IReadOnlyList<AttachmentAnchor> selectedMessageAnchors,
95 out AttachmentAnchor anchor,
96 out int ordinal)
97 {
98 anchor = new AttachmentAnchor();
99 ordinal = 0;
100 var context = new AnchorPeekResolveContext(
101 SelectedMessageIndex: -1,
102 selectedMessageAnchors,
103 new Dictionary<string, AttachmentAnchor>(),
104 []);
105
106 return AnchorPeekResolver.TryResolve(raw, context, out anchor, out _, out ordinal, out _);
107 }
108
109 private static bool isAnchorPeekHexIdEntryBody(string body)
110 {
111 if (string.IsNullOrWhiteSpace(body))
112 return false;
113
114 var tokens = body.TrimEnd().Split(' ', StringSplitOptions.RemoveEmptyEntries);
115 if (tokens.Length == 0)
116 return false;
117
118 if (tokens[0].Equals("anchor", StringComparison.OrdinalIgnoreCase))
119 return isHexArgAfterPeek(tokens, startIndex: 1);
120
121 if (tokens.Length >= 2
122 && tokens[0].Equals("intercom", StringComparison.OrdinalIgnoreCase)
123 && tokens[1].Equals("anchor", StringComparison.OrdinalIgnoreCase))
124 {
125 return isHexArgAfterPeek(tokens, startIndex: 2);
126 }
127
128 return false;
129 }
130
131 private static bool isHexArgAfterPeek(string[] tokens, int startIndex)
132 {
133 if (tokens.Length <= startIndex)
134 return false;
135
136 var peekToken = tokens[startIndex];
137 if (!peekToken.StartsWith("peek", StringComparison.OrdinalIgnoreCase))
138 return false;
139
140 if (peekToken.Equals("peek", StringComparison.OrdinalIgnoreCase))
141 {
142 return tokens.Length > startIndex + 1
143 && AnchorPeekTargetParser.LooksLikeHexEntry(tokens[startIndex + 1]);
144 }
145
146 return peekToken.Length > 4 && AnchorPeekTargetParser.LooksLikeHexEntry(peekToken[4..]);
147 }
148
149 private static string FormatLocationShort(AttachmentAnchor anchor)
150 {
151 if (string.IsNullOrWhiteSpace(anchor.File))
152 return "—";
153
154 var file = anchor.File.Replace('\\', '/');
155 if (anchor.LineStart is { } ls && anchor.LineEnd is { } le)
156 return le == ls ? $"{file} L{ls}" : $"{file} L{ls}–{le}";
157 return file;
158 }
159}
160
View only · write via MCP/CIDE