Forge
csharpdeeb25a2
1#nullable enable
2
3using System.Globalization;
4using System.Text.Json;
5
6namespace CascadeIDE.Models.Editor;
7
8/// <summary>Прямоугольный диапазон в документе (MCP/UI): файл + строки и колонки 1-based. Разбор из словаря JSON-аргументов команд <c>select</c> / <c>apply_edit</c>.</summary>
9public readonly struct EditorTextSpan : IEquatable<EditorTextSpan>
10{
11 public EditorDocumentPath File { get; }
12
13 public LineNumber StartLine { get; }
14
15 public ColumnNumber StartColumn { get; }
16
17 public LineNumber EndLine { get; }
18
19 public ColumnNumber EndColumn { get; }
20
21 public EditorTextSpan(
22 EditorDocumentPath file,
23 LineNumber startLine,
24 ColumnNumber startColumn,
25 LineNumber endLine,
26 ColumnNumber endColumn)
27 {
28 File = file;
29 StartLine = startLine;
30 StartColumn = startColumn;
31 EndLine = endLine;
32 EndColumn = endColumn;
33 }
34
35 /// <summary>Ожидаются ключи <c>start_line</c>, <c>start_column</c>, <c>end_line</c>, <c>end_column</c>, непустой <c>file_path</c> (каноническая нормализация).</summary>
36 public static bool TryParse(IReadOnlyDictionary<string, JsonElement>? args, out EditorTextSpan span, out string error)
37 {
38 span = default;
39 error = "";
40
41 if (args is null)
42 {
43 error = "Отсутствуют аргументы.";
44 return false;
45 }
46
47 var rawPath = McpCommandJsonArgs.String(args, "file_path");
48 if (!EditorDocumentPath.TryCreate(rawPath, out var filePath, out error))
49 return false;
50
51 if (!tryReadLineColumn(args, "start_line", "start_column", out var sl, out var sc, out error))
52 return false;
53 if (!tryReadLineColumn(args, "end_line", "end_column", out var el, out var ec, out error))
54 return false;
55
56 if (!LineRange.TryCreate(sl, el, out _))
57 {
58 error = "end_line не может быть меньше start_line.";
59 return false;
60 }
61
62 if (sl.Equals(el) && ec.Value < sc.Value)
63 {
64 error = "На одной строке end_column не может быть меньше start_column.";
65 return false;
66 }
67
68 span = new EditorTextSpan(filePath, sl, sc, el, ec);
69 return true;
70 }
71
72 public bool Equals(EditorTextSpan other) =>
73 File.Equals(other.File)
74 && StartLine.Equals(other.StartLine)
75 && StartColumn.Equals(other.StartColumn)
76 && EndLine.Equals(other.EndLine)
77 && EndColumn.Equals(other.EndColumn);
78
79 public override bool Equals(object? obj) => obj is EditorTextSpan other && Equals(other);
80
81 public override int GetHashCode() => HashCode.Combine(File, StartLine, StartColumn, EndLine, EndColumn);
82
83 public static bool operator ==(EditorTextSpan left, EditorTextSpan right) => left.Equals(right);
84
85 public static bool operator !=(EditorTextSpan left, EditorTextSpan right) => !left.Equals(right);
86
87 private static bool tryReadLineColumn(
88 IReadOnlyDictionary<string, JsonElement> args,
89 string lineKey,
90 string columnKey,
91 out LineNumber line,
92 out ColumnNumber column,
93 out string error)
94 {
95 line = default;
96 column = default;
97 error = "";
98
99 var lineRawOpt = readRequiredInt(args, lineKey);
100 var colRawOpt = readRequiredInt(args, columnKey);
101 if (lineRawOpt is null)
102 {
103 error = string.Format(CultureInfo.InvariantCulture, "Отсутствует или некорректное числовое поле «{0}».", lineKey);
104 return false;
105 }
106
107 if (colRawOpt is null)
108 {
109 error = string.Format(CultureInfo.InvariantCulture, "Отсутствует или некорректное числовое поле «{0}».", columnKey);
110 return false;
111 }
112
113 if (!LineNumber.TryCreate(lineRawOpt.Value, out line))
114 {
115 error = string.Format(CultureInfo.InvariantCulture, "«{0}» должно быть ≥ {1}; получено {2}.", lineKey, LineNumber.MinimumOneBasedInclusive, lineRawOpt.Value);
116 return false;
117 }
118
119 if (!ColumnNumber.TryCreate(colRawOpt.Value, out column))
120 {
121 error = string.Format(CultureInfo.InvariantCulture, "«{0}» должно быть ≥ {1}; получено {2}.", columnKey, ColumnNumber.MinimumOneBasedInclusive, colRawOpt.Value);
122 return false;
123 }
124
125 return true;
126 }
127
128 private static int? readRequiredInt(IReadOnlyDictionary<string, JsonElement> args, string key)
129 {
130 if (!args.TryGetValue(key, out var e) || e.ValueKind != JsonValueKind.Number || !e.TryGetInt32(out var v))
131 return null;
132 return v;
133 }
134}
135
136/// <summary>Разбор аргументов <c>get_editor_content_range</c>: 1-based инклюзивный диапазон строк.</summary>
137public static class EditorContentLineRangeMcpArgs
138{
139 /// <summary>Если ключ отсутствует, используется 1 (как прежнее поведение <see cref="McpCommandJsonArgs.Int"/>).</summary>
140 public static bool TryParse(IReadOnlyDictionary<string, JsonElement>? args, out LineRange lines, out string error)
141 {
142 lines = default;
143 error = "";
144
145 var startRaw = rawOrOne(args, "start_line");
146 var endRaw = rawOrOne(args, "end_line");
147
148 if (!LineNumber.TryCreate(startRaw, out var lnStart))
149 {
150 error = string.Format(CultureInfo.InvariantCulture, "start_line должно быть ≥ {0}; получено {1}.", LineNumber.MinimumOneBasedInclusive, startRaw);
151 return false;
152 }
153
154 if (!LineNumber.TryCreate(endRaw, out var lnEnd))
155 {
156 error = string.Format(CultureInfo.InvariantCulture, "end_line должно быть ≥ {0}; получено {1}.", LineNumber.MinimumOneBasedInclusive, endRaw);
157 return false;
158 }
159
160 if (!LineRange.TryCreate(lnStart, lnEnd, out lines))
161 {
162 error = "end_line не может быть меньше start_line.";
163 return false;
164 }
165
166 return true;
167 }
168
169 private static int rawOrOne(IReadOnlyDictionary<string, JsonElement>? args, string key)
170 {
171 if (args is null || !args.TryGetValue(key, out var e) || e.ValueKind != JsonValueKind.Number || !e.TryGetInt32(out var v))
172 return 1;
173 return v;
174 }
175}
176
177/// <summary>Разбор <c>go_to_position</c>: обязательны <c>file_path</c>, <c>line</c>, <c>end_line</c> опционально, <c>column</c>, <c>end_column</c>.</summary>
178public static class EditorGoToPositionMcpArgs
179{
180 public static bool TryParse(
181 IReadOnlyDictionary<string, JsonElement>? args,
182 out EditorDocumentPath file,
183 out LineNumber line,
184 out ColumnNumber column,
185 out LineNumber? endLine,
186 out ColumnNumber? endColumn,
187 out string error)
188 {
189 file = default;
190 line = default;
191 column = default;
192 endLine = null;
193 endColumn = null;
194 error = "";
195
196 if (args is null)
197 {
198 error = "Отсутствуют аргументы.";
199 return false;
200 }
201
202 var rawPath = McpCommandJsonArgs.String(args, "file_path");
203 if (!EditorDocumentPath.TryCreate(rawPath, out file, out error))
204 return false;
205
206 if (!args.TryGetValue("line", out var lineEl) || lineEl.ValueKind != JsonValueKind.Number || !lineEl.TryGetInt32(out var lineRaw))
207 {
208 error = "Отсутствует или некорректное поле «line».";
209 return false;
210 }
211
212 if (!args.TryGetValue("column", out var colEl) || colEl.ValueKind != JsonValueKind.Number || !colEl.TryGetInt32(out var colRaw))
213 {
214 error = "Отсутствует или некорректное поле «column».";
215 return false;
216 }
217
218 if (!LineNumber.TryCreate(lineRaw, out line))
219 {
220 error = string.Format(CultureInfo.InvariantCulture, "line должно быть ≥ {0}; получено {1}.", LineNumber.MinimumOneBasedInclusive, lineRaw);
221 return false;
222 }
223
224 if (!ColumnNumber.TryCreate(colRaw, out column))
225 {
226 error = string.Format(CultureInfo.InvariantCulture, "column должно быть ≥ {0}; получено {1}.", ColumnNumber.MinimumOneBasedInclusive, colRaw);
227 return false;
228 }
229
230 var elOpt = McpCommandJsonArgs.OptionalInt32(args, "end_line");
231 var ecOpt = McpCommandJsonArgs.OptionalInt32(args, "end_column");
232 if (elOpt.HasValue)
233 {
234 if (!LineNumber.TryCreate(elOpt.Value, out var ln))
235 {
236 error = string.Format(CultureInfo.InvariantCulture, "end_line должно быть ≥ {0}; получено {1}.", LineNumber.MinimumOneBasedInclusive, elOpt.Value);
237 return false;
238 }
239
240 endLine = ln;
241 }
242
243 if (ecOpt.HasValue)
244 {
245 if (!ColumnNumber.TryCreate(ecOpt.Value, out var cn))
246 {
247 error = string.Format(CultureInfo.InvariantCulture, "end_column должно быть ≥ {0}; получено {1}.", ColumnNumber.MinimumOneBasedInclusive, ecOpt.Value);
248 return false;
249 }
250
251 endColumn = cn;
252 }
253
254 return true;
255 }
256}
257
258/// <summary>Запрос <c>reveal_editor_range</c> (ADR 0130): строки и/или member/scope + опциональная длительность.</summary>
259public readonly struct EditorRevealRangeRequest
260{
261 public EditorDocumentPath File { get; init; }
262
263 public LineRange? Lines { get; init; }
264
265 public string? MemberKey { get; init; }
266
267 public Intercom.AttachmentSyntaxScope? SyntaxScope { get; init; }
268
269 public int? DurationMs { get; init; }
270}
271
272/// <summary>Разбор <c>reveal_editor_range</c> (ADR 0130 фаза 2).</summary>
273public static class EditorRevealRangeMcpArgs
274{
275 public static bool TryParse(
276 IReadOnlyDictionary<string, JsonElement>? args,
277 out EditorRevealRangeRequest request,
278 out string error)
279 {
280 request = default;
281 error = "";
282
283 if (args is null)
284 {
285 error = "Отсутствуют аргументы.";
286 return false;
287 }
288
289 if (!EditorDocumentPath.TryCreate(McpCommandJsonArgs.String(args, "file_path"), out var file, out var fileErr))
290 {
291 error = fileErr;
292 return false;
293 }
294
295 LineRange? lines = null;
296 if (args.ContainsKey("start_line") || args.ContainsKey("end_line"))
297 {
298 if (!EditorContentLineRangeMcpArgs.TryParse(args, out var range, out var rangeErr))
299 {
300 error = rangeErr;
301 return false;
302 }
303
304 lines = range;
305 }
306
307 var memberKey = McpCommandJsonArgs.String(args, "member_key");
308 Intercom.AttachmentSyntaxScope? syntaxScope = null;
309 if (args.TryGetValue("syntax_scope", out var scopeEl))
310 Intercom.AttachmentSyntaxScope.TryParse(scopeEl, out syntaxScope);
311
312 var durationMs = McpCommandJsonArgs.OptionalInt32(args, "duration_ms");
313
314 if (lines is null && string.IsNullOrWhiteSpace(memberKey) && syntaxScope is null)
315 {
316 error = "Нужен диапазон строк (start_line, end_line) или member_key / syntax_scope.";
317 return false;
318 }
319
320 request = new EditorRevealRangeRequest
321 {
322 File = file,
323 Lines = lines,
324 MemberKey = string.IsNullOrWhiteSpace(memberKey) ? null : memberKey.Trim(),
325 SyntaxScope = syntaxScope,
326 DurationMs = durationMs,
327 };
328 return true;
329 }
330}
331
View only · write via MCP/CIDE