Forge
csharpdeeb25a2
1#nullable enable
2using System.Text.Json;
3using CascadeIDE.Models.Editor;
4using CascadeIDE.Services;
5
6namespace CascadeIDE.Features.Chat;
7
8/// <summary>
9/// Slash-хвост → JSON-args для любой параметрической команды из каталога (wire_class из TOML).
10/// Паритет с <see cref="ParametricIntentMelody.TryResolveParametricExecution"/> для melody <c>c:</c>.
11/// </summary>
12internal static class ChatSlashParametricArgsBuilder
13{
14 public static bool IsParametricCatalogCommand(string commandId) =>
15 IntentMelodyCatalog.TryGetParametricRootByCommandId(commandId, out _);
16
17 public static bool TryBuild(
18 string commandId,
19 string? argsTail,
20 ChatSlashEditorContext editor,
21 out IReadOnlyDictionary<string, JsonElement>? args,
22 out string error)
23 {
24 args = null;
25 error = "";
26
27 if (!IntentMelodyCatalog.TryGetParametricRootByCommandId(commandId, out var root))
28 {
29 error = "Команда не параметрическая в каталоге melody.";
30 return false;
31 }
32
33 if (string.IsNullOrWhiteSpace(root.WireClass)
34 || !IntentMelodyCatalog.TryGetTailWireClass(root.WireClass, out var wire))
35 {
36 error = $"У параметрической команды «{commandId}» не задан wire_class в каталоге.";
37 return false;
38 }
39
40 return wire.Kind switch
41 {
42 TailWireKind.DelimitedSlots when IntentMelodyTailSemantics.CountDelimitedNumericSlots(root.TailSignature) == 2
43 => TryBuildLineRange(root, argsTail, editor, out args, out error),
44 TailWireKind.SingleRemainder when IntentMelodyTailSemantics.HasUrlSlot(root.TailSignature)
45 => TryBuildUrlRemainder(argsTail, out args, out error),
46 TailWireKind.SingleRemainder when IntentMelodyTailSemantics.HasBracketCodeRefSlot(root.TailSignature)
47 => TryBuildBracketCodeRef(argsTail, editor, out args, out error),
48 _ => Fail($"Форма wire_class «{root.WireClass}» для slash ещё не поддержана.", out args, out error),
49 };
50 }
51
52 public static bool RequiresNonEmptyArgsTail(MelodyRootEntry root)
53 {
54 if (root.Shape != IntentMelodyShape.Parametric)
55 return false;
56
57 if (string.IsNullOrWhiteSpace(root.WireClass)
58 || !IntentMelodyCatalog.TryGetTailWireClass(root.WireClass, out var wire))
59 return true;
60
61 return wire.Kind switch
62 {
63 TailWireKind.DelimitedSlots => true,
64 TailWireKind.SingleRemainder when IntentMelodyTailSemantics.HasUrlSlot(root.TailSignature) => false,
65 TailWireKind.SingleRemainder when IntentMelodyTailSemantics.HasBracketCodeRefSlot(root.TailSignature) => true,
66 _ => true,
67 };
68 }
69
70 private static bool TryBuildLineRange(
71 MelodyRootEntry root,
72 string? argsTail,
73 ChatSlashEditorContext editor,
74 out IReadOnlyDictionary<string, JsonElement>? args,
75 out string error)
76 {
77 args = null;
78 if (!ParametricSegmentListParser.TryParseSingleContiguous(argsTail, out var contiguous, out error))
79 return false;
80
81 var start = contiguous.Start;
82 var end = contiguous.End;
83
84 if (!LineNumber.TryCreate(start, out var lnStart)
85 || !LineNumber.TryCreate(end, out var lnEnd)
86 || !LineRange.TryCreate(lnStart, lnEnd, out var lines))
87 {
88 error = "Номера строк должны быть ≥ 1, конец — не раньше начала.";
89 return false;
90 }
91
92 var displayTail = start == end ? $"{root.Slug}:{start}" : $"{root.Slug}:{start}:{end}";
93 var parsed = new ParametricIntentMelody.ParsedLineRange(root.Slug, displayTail, lines);
94
95 if (!ParametricLineRangeArgsBuilder.TryBuild(
96 parsed,
97 editor.CurrentFilePath,
98 editor.EditorText ?? "",
99 out _,
100 out var argsJson,
101 out error))
102 {
103 return false;
104 }
105
106 args = JsonArgsToDictionary(argsJson);
107 return true;
108 }
109
110 private static bool TryBuildBracketCodeRef(
111 string? argsTail,
112 ChatSlashEditorContext editor,
113 out IReadOnlyDictionary<string, JsonElement>? args,
114 out string error)
115 {
116 args = null;
117 error = "";
118 var codeRef = (argsTail ?? "").Trim();
119 if (codeRef.Length == 0)
120 {
121 error = "Укажи bracket-ссылку: [M:Method], [file.cs M:Method] или [F:path; M:name; L:10-20].";
122 return false;
123 }
124
125 var payload = new Dictionary<string, object?> { ["code_ref"] = codeRef };
126 if (!string.IsNullOrWhiteSpace(editor.CurrentFilePath))
127 payload["active_file"] = editor.CurrentFilePath;
128
129 var json = JsonSerializer.Serialize(payload);
130 args = JsonArgsToDictionary(json);
131 return true;
132 }
133
134 private static bool TryBuildUrlRemainder(
135 string? argsTail,
136 out IReadOnlyDictionary<string, JsonElement>? args,
137 out string error)
138 {
139 args = null;
140 error = "";
141 var url = (argsTail ?? "").Trim();
142 if (url.Length == 0)
143 {
144 args = null;
145 return true;
146 }
147
148 var json = JsonSerializer.Serialize(new { url });
149 args = JsonArgsToDictionary(json);
150 return true;
151 }
152
153 internal static bool TryParseLineRangeTail(string? argsTail, out int startLine, out int endLine, out string error)
154 {
155 startLine = 0;
156 endLine = 0;
157 error = "";
158
159 var tail = (argsTail ?? "").Trim();
160 if (tail.Length == 0)
161 {
162 error = "Укажи номера строк (1-based): одну, две через пробел или start:end.";
163 return false;
164 }
165
166 var normalized = tail.Replace(':', ' ').Replace(';', ' ');
167 var parts = normalized.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
168 if (parts.Length is < 1 or > 2)
169 {
170 error = "Ожидается одна строка или диапазон: «5», «5 10» или «5:10».";
171 return false;
172 }
173
174 if (!int.TryParse(parts[0], out startLine) || startLine < 1)
175 {
176 error = $"Некорректный номер строки «{parts[0]}».";
177 return false;
178 }
179
180 if (parts.Length == 1)
181 {
182 endLine = startLine;
183 return true;
184 }
185
186 if (!int.TryParse(parts[1], out endLine) || endLine < 1)
187 {
188 error = $"Некорректный номер строки «{parts[1]}».";
189 return false;
190 }
191
192 if (endLine < startLine)
193 {
194 error = "Конец диапазона не может быть меньше начала.";
195 return false;
196 }
197
198 return true;
199 }
200
201 private static IReadOnlyDictionary<string, JsonElement> JsonArgsToDictionary(string argsJson)
202 {
203 using var doc = JsonDocument.Parse(argsJson);
204 return doc.RootElement.EnumerateObject()
205 .ToDictionary(static p => p.Name, static p => p.Value.Clone(), StringComparer.Ordinal);
206 }
207
208 private static bool Fail(string message, out IReadOnlyDictionary<string, JsonElement>? args, out string error)
209 {
210 args = null;
211 error = message;
212 return false;
213 }
214}
215
View only · write via MCP/CIDE