| 1 | using System.Text.Json; |
| 2 | using CascadeIDE.Contracts; |
| 3 | |
| 4 | namespace CascadeIDE.Features.IdeMcp.Application; |
| 5 | |
| 6 | /// <summary> |
| 7 | /// Application-level orchestrator helpers for IDE MCP editor actions. |
| 8 | /// Keeps MCP payload shaping out of MainWindowViewModel. |
| 9 | /// </summary> |
| 10 | [ApplicationOrchestrator] |
| 11 | public static class IdeMcpEditorOrchestrator |
| 12 | { |
| 13 | /// <summary>Снимок открытой вкладки для <see cref="BuildGetOpenDocumentTextResponse"/> (без зависимости от ViewModels).</summary> |
| 14 | public readonly record struct OpenDocumentTabSnapshot(string FilePath, string Content, bool IsDirty); |
| 15 | |
| 16 | /// <summary> |
| 17 | /// Разрешить путь, найти вкладку по тем же правилам, что и обозреватель, вернуть JSON для MCP <c>get_open_document_text</c>. |
| 18 | /// </summary> |
| 19 | public static string BuildGetOpenDocumentTextResponse( |
| 20 | string? filePathArgument, |
| 21 | string? currentFilePath, |
| 22 | IReadOnlyList<OpenDocumentTabSnapshot> openTabs, |
| 23 | int? maxChars) |
| 24 | { |
| 25 | var target = string.IsNullOrWhiteSpace(filePathArgument) ? currentFilePath : filePathArgument.Trim(); |
| 26 | if (string.IsNullOrEmpty(target)) |
| 27 | return SerializeOpenDocumentMissingPathError(); |
| 28 | |
| 29 | foreach (var tab in openTabs) |
| 30 | { |
| 31 | if (EditorTextCoordinateUtilities.PathsReferToSameFile(tab.FilePath, target)) |
| 32 | return SerializeOpenDocumentText(tab.FilePath, tab.Content, tab.IsDirty, maxChars); |
| 33 | } |
| 34 | |
| 35 | return SerializeOpenDocumentNotOpenError(target); |
| 36 | } |
| 37 | |
| 38 | /// <summary> |
| 39 | /// Maps 1-based line/column range to a selection span in <paramref name="editorText"/>. |
| 40 | /// Returns false if either offset is invalid (e.g. out of range). |
| 41 | /// </summary> |
| 42 | public static bool TryComputeSelectionSpan( |
| 43 | string? editorText, |
| 44 | int startLine, |
| 45 | int startColumn, |
| 46 | int endLine, |
| 47 | int endColumn, |
| 48 | out int selectionStart, |
| 49 | out int selectionLength) |
| 50 | { |
| 51 | var text = editorText ?? ""; |
| 52 | var start = EditorTextCoordinateUtilities.LineColumnToOffset(text, startLine, startColumn); |
| 53 | var end = EditorTextCoordinateUtilities.LineColumnToOffset(text, endLine, endColumn); |
| 54 | if (start < 0 || end < 0) |
| 55 | { |
| 56 | selectionStart = 0; |
| 57 | selectionLength = 0; |
| 58 | return false; |
| 59 | } |
| 60 | |
| 61 | selectionStart = start; |
| 62 | selectionLength = Math.Max(0, end - start); |
| 63 | return true; |
| 64 | } |
| 65 | |
| 66 | public static string SerializeEditorState(Services.EditorStateDto dto) => |
| 67 | JsonSerializer.Serialize(dto); |
| 68 | |
| 69 | public static string SerializeEditorContentRange(string? filePath, int startLine, int endLine, string? content) => |
| 70 | JsonSerializer.Serialize(new |
| 71 | { |
| 72 | file_path = filePath, |
| 73 | start_line = startLine, |
| 74 | end_line = endLine, |
| 75 | content = content ?? "" |
| 76 | }); |
| 77 | |
| 78 | public static string SerializeOpenDocumentMissingPathError() => |
| 79 | JsonSerializer.Serialize(new { error = "no_path", message = "file_path не задан и нет текущего открытого файла." }); |
| 80 | |
| 81 | public static string SerializeOpenDocumentNotOpenError(string filePathRequested) => |
| 82 | JsonSerializer.Serialize(new |
| 83 | { |
| 84 | error = "not_open", |
| 85 | message = "Файл не среди открытых вкладок.", |
| 86 | file_path_requested = filePathRequested |
| 87 | }); |
| 88 | |
| 89 | /// <summary>Замена диапазона (1-based line/column) в <paramref name="text"/>.</summary> |
| 90 | public static bool TryReplaceTextRange( |
| 91 | string text, |
| 92 | int startLine, |
| 93 | int startColumn, |
| 94 | int endLine, |
| 95 | int endColumn, |
| 96 | string newText, |
| 97 | out string updated) |
| 98 | { |
| 99 | updated = text; |
| 100 | var start = EditorTextCoordinateUtilities.LineColumnToOffset(text, startLine, startColumn); |
| 101 | var end = EditorTextCoordinateUtilities.LineColumnToOffset(text, endLine, endColumn); |
| 102 | if (start < 0 || end < 0) |
| 103 | return false; |
| 104 | |
| 105 | updated = text[..start] + (newText ?? "") + text[end..]; |
| 106 | return true; |
| 107 | } |
| 108 | |
| 109 | public static string SerializeOpenDocumentText( |
| 110 | string? filePath, |
| 111 | string? fullText, |
| 112 | bool isDirty, |
| 113 | int? maxChars) |
| 114 | { |
| 115 | var source = fullText ?? ""; |
| 116 | var len = source.Length; |
| 117 | var truncated = false; |
| 118 | var text = source; |
| 119 | if (maxChars is > 0 && len > maxChars.Value) |
| 120 | { |
| 121 | text = source[..maxChars.Value]; |
| 122 | truncated = true; |
| 123 | } |
| 124 | |
| 125 | return JsonSerializer.Serialize(new |
| 126 | { |
| 127 | file_path = filePath, |
| 128 | length = len, |
| 129 | truncated, |
| 130 | is_dirty = isDirty, |
| 131 | text |
| 132 | }); |
| 133 | } |
| 134 | } |
| 135 | |