| 1 | using System.Text.Json; |
| 2 | using RoslynMcp.ServiceLayer; |
| 3 | |
| 4 | namespace RoslynMcp.Mcp; |
| 5 | |
| 6 | /// <summary>Обработчики вызовов инструментов MCP. Один вход — имя + аргументы, выход — текст результата или исключение.</summary> |
| 7 | public static class ToolHandlers |
| 8 | { |
| 9 | public static Task<string> HandleAsync( |
| 10 | string name, |
| 11 | IReadOnlyDictionary<string, JsonElement> args, |
| 12 | CancellationToken cancellationToken) |
| 13 | { |
| 14 | return name switch |
| 15 | { |
| 16 | "roslyn_ping" => Task.FromResult(Ping()), |
| 17 | "roslyn_get_document_symbols" => Task.FromResult(GetDocumentSymbols(args, cancellationToken)), |
| 18 | "roslyn_get_symbol_at_position" => GetSymbolAtPositionAsync(args, cancellationToken), |
| 19 | "roslyn_find_usages" => FindUsagesAsync(args, cancellationToken), |
| 20 | "roslyn_go_to_definition" => GoToDefinitionAsync(args, cancellationToken), |
| 21 | "roslyn_rename" => RenameAsync(args, cancellationToken), |
| 22 | "roslyn_get_code_actions" => GetCodeActionsAsync(args, cancellationToken), |
| 23 | "roslyn_apply_code_action" => ApplyCodeActionAsync(args, cancellationToken), |
| 24 | "roslyn_get_diagnostics" => GetDiagnosticsAsync(args, cancellationToken), |
| 25 | "roslyn_get_solution_structure" => GetSolutionStructureAsync(args, cancellationToken), |
| 26 | "roslyn_get_workspace_navigation_context" => GetWorkspaceNavigationContextAsync(args, cancellationToken), |
| 27 | "roslyn_sync_namespaces" => SyncNamespacesAsync(args, cancellationToken), |
| 28 | "roslyn_sync_dependent_upon_partials" => SyncDependentUponPartialsAsync(args, cancellationToken), |
| 29 | "roslyn_resolve_breakpoint" => ResolveBreakpointAsync(args, cancellationToken), |
| 30 | "roslyn_generate_interface_from_class" => GenerateInterfaceFromClassAsync(args, cancellationToken), |
| 31 | "roslyn_generate_base_class_from_class" => GenerateBaseClassFromClassAsync(args, cancellationToken), |
| 32 | "roslyn_generate_overrides" => GenerateOverridesAsync(args, cancellationToken), |
| 33 | "roslyn_generate_constructor_from_members" => GenerateConstructorFromMembersAsync(args, cancellationToken), |
| 34 | "roslyn_generate_equals_gethashcode" => GenerateEqualsGetHashCodeAsync(args, cancellationToken), |
| 35 | "roslyn_move_members_to_partial_file" => MoveMembersToPartialFileAsync(args, cancellationToken), |
| 36 | _ => throw new ArgumentException($"Unknown tool: {name}.", nameof(name)) |
| 37 | }; |
| 38 | } |
| 39 | |
| 40 | private static string Ping() => |
| 41 | $"OK {DateTime.UtcNow:O} — RoslynMcp. Tools: roslyn_get_document_symbols, roslyn_get_symbol_at_position, …"; |
| 42 | |
| 43 | private static string GetDocumentSymbols(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 44 | { |
| 45 | if (!TryGetString(args, "file_path", out var filePath)) |
| 46 | throw new ArgumentException("file_path (string) is required."); |
| 47 | return DocumentSymbols.GetDocumentSymbols(filePath!, ct); |
| 48 | } |
| 49 | |
| 50 | private static Task<string> GetSymbolAtPositionAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 51 | { |
| 52 | if (!TryGetString(args, "file_path", out var filePath)) |
| 53 | throw new ArgumentException("file_path (string) is required."); |
| 54 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 55 | throw new ArgumentException("line (integer >= 1) is required."); |
| 56 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 57 | throw new ArgumentException("column (integer >= 1) is required."); |
| 58 | var solutionPath = args.TryGetValue("solution_or_project_path", out var solEl) && solEl.ValueKind == JsonValueKind.String |
| 59 | ? solEl.GetString() |
| 60 | : null; |
| 61 | return SymbolAtPosition.GetSymbolAtPositionAsync(filePath!, line, column, solutionPath, ct); |
| 62 | } |
| 63 | |
| 64 | private static async Task<string> FindUsagesAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 65 | { |
| 66 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 67 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 68 | if (!TryGetString(args, "file_path", out var filePath)) |
| 69 | throw new ArgumentException("file_path (string) is required."); |
| 70 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 71 | throw new ArgumentException("line (integer >= 1) is required."); |
| 72 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 73 | throw new ArgumentException("column (integer >= 1) is required."); |
| 74 | return await FindUsages.FindUsagesAsync(solutionPath!, filePath!, line, column, ct).ConfigureAwait(false); |
| 75 | } |
| 76 | |
| 77 | private static Task<string> GoToDefinitionAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 78 | { |
| 79 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 80 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 81 | if (!TryGetString(args, "file_path", out var filePath)) |
| 82 | throw new ArgumentException("file_path (string) is required."); |
| 83 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 84 | throw new ArgumentException("line (integer >= 1) is required."); |
| 85 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 86 | throw new ArgumentException("column (integer >= 1) is required."); |
| 87 | return GoToDefinition.GoToDefinitionAsync(solutionPath!, filePath!, line, column, ct); |
| 88 | } |
| 89 | |
| 90 | private static async Task<string> RenameAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 91 | { |
| 92 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 93 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 94 | if (!TryGetString(args, "file_path", out var filePath)) |
| 95 | throw new ArgumentException("file_path (string) is required."); |
| 96 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 97 | throw new ArgumentException("line (integer >= 1) is required."); |
| 98 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 99 | throw new ArgumentException("column (integer >= 1) is required."); |
| 100 | if (!TryGetString(args, "new_name", out var newName)) |
| 101 | throw new ArgumentException("new_name (string) is required."); |
| 102 | var apply = args.TryGetValue("apply", out var applyEl) && applyEl.ValueKind == JsonValueKind.True; |
| 103 | var renameInComments = args.TryGetValue("rename_in_comments", out var ricEl) && ricEl.ValueKind == JsonValueKind.True; |
| 104 | var renameInStrings = args.TryGetValue("rename_in_strings", out var risEl) && risEl.ValueKind == JsonValueKind.True; |
| 105 | var renameOverloads = args.TryGetValue("rename_overloads", out var roEl) && roEl.ValueKind == JsonValueKind.True; |
| 106 | var renameFile = args.TryGetValue("rename_file", out var rfEl) && rfEl.ValueKind == JsonValueKind.True; |
| 107 | var renamePartialTypeFiles = args.TryGetValue("rename_partial_type_files", out var rptfEl) && rptfEl.ValueKind == JsonValueKind.True; |
| 108 | return await RenameSymbol.RenameAsync(solutionPath!, filePath!, line, column, newName!, apply, renameInComments, renameInStrings, renameOverloads, renameFile, renamePartialTypeFiles, ct).ConfigureAwait(false); |
| 109 | } |
| 110 | |
| 111 | private static Task<string> GetCodeActionsAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 112 | { |
| 113 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 114 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 115 | if (!TryGetString(args, "file_path", out var filePath)) |
| 116 | throw new ArgumentException("file_path (string) is required."); |
| 117 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 118 | throw new ArgumentException("line (integer >= 1) is required."); |
| 119 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 120 | throw new ArgumentException("column (integer >= 1) is required."); |
| 121 | int? endLine = TryGetInt(args, "end_line", out var el) && el >= 1 ? el : null; |
| 122 | int? endColumn = TryGetInt(args, "end_column", out var ec) && ec >= 1 ? ec : null; |
| 123 | return CodeActions.GetCodeActionsAsync(solutionPath!, filePath!, line, column, endLine, endColumn, ct); |
| 124 | } |
| 125 | |
| 126 | private static Task<string> ApplyCodeActionAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 127 | { |
| 128 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 129 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 130 | if (!TryGetString(args, "file_path", out var filePath)) |
| 131 | throw new ArgumentException("file_path (string) is required."); |
| 132 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 133 | throw new ArgumentException("line (integer >= 1) is required."); |
| 134 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 135 | throw new ArgumentException("column (integer >= 1) is required."); |
| 136 | if (!TryGetInt(args, "action_index", out var actionIndex) || actionIndex < 0) |
| 137 | throw new ArgumentException("action_index (integer >= 0) is required."); |
| 138 | int? endLineApply = TryGetInt(args, "end_line", out var ela) && ela >= 1 ? ela : null; |
| 139 | int? endColumnApply = TryGetInt(args, "end_column", out var eca) && eca >= 1 ? eca : null; |
| 140 | TryGetString(args, "fix_all_scope", out var fixAllScope); |
| 141 | TryGetString(args, "constant_name", out var constantName); |
| 142 | var actionOptions = TryGetActionOptions(args); |
| 143 | return CodeActions.ApplyCodeActionAsync(solutionPath!, filePath!, line, column, actionIndex, endLineApply, endColumnApply, fixAllScope, constantName, actionOptions, ct); |
| 144 | } |
| 145 | |
| 146 | /// <summary>Парсит action_options из args: JSON object → Dictionary (string, int, bool, string[]).</summary> |
| 147 | private static Dictionary<string, object?>? TryGetActionOptions(IReadOnlyDictionary<string, JsonElement> args) |
| 148 | { |
| 149 | if (!args.TryGetValue("action_options", out var el) || el.ValueKind != JsonValueKind.Object) |
| 150 | return null; |
| 151 | var dict = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase); |
| 152 | foreach (var prop in el.EnumerateObject()) |
| 153 | { |
| 154 | object? val = prop.Value.ValueKind switch |
| 155 | { |
| 156 | JsonValueKind.String => prop.Value.GetString(), |
| 157 | JsonValueKind.Number => prop.Value.TryGetInt32(out var i32) ? i32 : prop.Value.TryGetInt64(out var i64) ? i64 : (object?)prop.Value.GetDouble(), |
| 158 | JsonValueKind.True => true, |
| 159 | JsonValueKind.False => false, |
| 160 | JsonValueKind.Array => GetStringArray(prop.Value), |
| 161 | _ => null |
| 162 | }; |
| 163 | if (val != null || prop.Value.ValueKind == JsonValueKind.Null || prop.Value.ValueKind == JsonValueKind.String) |
| 164 | dict[prop.Name] = val; |
| 165 | } |
| 166 | return dict.Count == 0 ? null : dict; |
| 167 | } |
| 168 | |
| 169 | private static string[]? GetStringArray(JsonElement arr) |
| 170 | { |
| 171 | var list = new List<string>(); |
| 172 | foreach (var item in arr.EnumerateArray()) |
| 173 | { |
| 174 | if (item.ValueKind == JsonValueKind.String && item.GetString() is { } s) |
| 175 | list.Add(s); |
| 176 | } |
| 177 | return list.Count == 0 ? null : list.ToArray(); |
| 178 | } |
| 179 | |
| 180 | private static Task<string> GetDiagnosticsAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 181 | { |
| 182 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 183 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 184 | TryGetString(args, "file_path", out var filePath); |
| 185 | return GetDiagnostics.GetDiagnosticsAsync(solutionPath!, filePath, ct); |
| 186 | } |
| 187 | |
| 188 | private static Task<string> GetSolutionStructureAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 189 | { |
| 190 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 191 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 192 | return GetSolutionStructure.GetStructureAsync(solutionPath!, ct); |
| 193 | } |
| 194 | |
| 195 | private static Task<string> GetWorkspaceNavigationContextAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 196 | { |
| 197 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 198 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 199 | if (!TryGetString(args, "file_path", out var filePath)) |
| 200 | throw new ArgumentException("file_path (string) is required."); |
| 201 | if (!TryGetString(args, "mode", out var mode) || string.IsNullOrWhiteSpace(mode)) |
| 202 | throw new ArgumentException("mode (string: related | subgraph) is required."); |
| 203 | int? line = TryGetInt(args, "line", out var l) && l >= 1 ? l : null; |
| 204 | int? column = TryGetInt(args, "column", out var c) && c >= 1 ? c : null; |
| 205 | var maxRelated = TryGetInt(args, "max_related", out var mr) && mr >= 1 ? mr : GetWorkspaceNavigationContext.DefaultMaxRelated; |
| 206 | var maxNodes = TryGetInt(args, "max_nodes", out var mn) && mn >= 1 ? mn : GetWorkspaceNavigationContext.DefaultMaxNodes; |
| 207 | var maxEdges = TryGetInt(args, "max_edges", out var me) && me >= 1 ? me : GetWorkspaceNavigationContext.DefaultMaxEdges; |
| 208 | IReadOnlyList<string>? includeKinds = TryGetOptionalStringList(args, "include_kinds"); |
| 209 | IReadOnlyList<string>? excludeKinds = TryGetOptionalStringList(args, "exclude_kinds"); |
| 210 | TryGetString(args, "preset", out var preset); |
| 211 | return GetWorkspaceNavigationContext.GetAsync( |
| 212 | solutionPath!, |
| 213 | filePath!, |
| 214 | mode!, |
| 215 | line, |
| 216 | column, |
| 217 | maxRelated, |
| 218 | maxNodes, |
| 219 | maxEdges, |
| 220 | includeKinds, |
| 221 | excludeKinds, |
| 222 | string.IsNullOrWhiteSpace(preset) ? null : preset, |
| 223 | ct); |
| 224 | } |
| 225 | |
| 226 | private static string[]? TryGetOptionalStringList(IReadOnlyDictionary<string, JsonElement> args, string key) |
| 227 | { |
| 228 | if (!args.TryGetValue(key, out var el) || el.ValueKind != JsonValueKind.Array) |
| 229 | return null; |
| 230 | if (GetStringArray(el) is not string[] arr || arr.Length == 0) |
| 231 | return null; |
| 232 | return arr; |
| 233 | } |
| 234 | |
| 235 | private static Task<string> SyncNamespacesAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 236 | { |
| 237 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 238 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 239 | TryGetString(args, "project_path", out var projectPath); |
| 240 | var dryRun = args.TryGetValue("dry_run", out var dryRunEl) && dryRunEl.ValueKind == JsonValueKind.True; |
| 241 | return SyncNamespaces.SyncAsync(solutionPath!, dryRun, projectPath, cancellationToken: ct); |
| 242 | } |
| 243 | |
| 244 | private static Task<string> SyncDependentUponPartialsAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 245 | { |
| 246 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 247 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 248 | TryGetString(args, "project_path", out var projectPath); |
| 249 | var dryRun = args.TryGetValue("dry_run", out var dryRunEl) && dryRunEl.ValueKind == JsonValueKind.True; |
| 250 | return SyncDependentUponPartials.SyncAsync(solutionPath!, projectPath, dryRun, cancellationToken: ct); |
| 251 | } |
| 252 | |
| 253 | private static Task<string> ResolveBreakpointAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 254 | { |
| 255 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 256 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 257 | if (!TryGetString(args, "file_path", out var filePath)) |
| 258 | throw new ArgumentException("file_path (string) is required."); |
| 259 | if (!TryGetString(args, "symbol_name", out var symbolName)) |
| 260 | throw new ArgumentException("symbol_name (string) is required."); |
| 261 | return ResolveBreakpoint.ResolveAsync(solutionPath!, filePath!, symbolName!, ct); |
| 262 | } |
| 263 | |
| 264 | private static Task<string> GenerateInterfaceFromClassAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 265 | { |
| 266 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 267 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 268 | if (!TryGetString(args, "file_path", out var filePath)) |
| 269 | throw new ArgumentException("file_path (string) is required."); |
| 270 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 271 | throw new ArgumentException("line (integer >= 1) is required."); |
| 272 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 273 | throw new ArgumentException("column (integer >= 1) is required."); |
| 274 | TryGetString(args, "interface_name", out var interfaceName); |
| 275 | TryGetString(args, "output_file_path", out var outputFilePath); |
| 276 | IReadOnlyList<string>? memberNames = null; |
| 277 | if (args.TryGetValue("member_names", out var mnEl) && mnEl.ValueKind == JsonValueKind.Array && GetStringArray(mnEl) is string[] arr && arr.Length > 0) |
| 278 | memberNames = arr; |
| 279 | return GenerateInterface.GenerateInterfaceFromClassAsync(solutionPath!, filePath!, line, column, interfaceName, outputFilePath, memberNames, ct); |
| 280 | } |
| 281 | |
| 282 | private static Task<string> GenerateBaseClassFromClassAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 283 | { |
| 284 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 285 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 286 | if (!TryGetString(args, "file_path", out var filePath)) |
| 287 | throw new ArgumentException("file_path (string) is required."); |
| 288 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 289 | throw new ArgumentException("line (integer >= 1) is required."); |
| 290 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 291 | throw new ArgumentException("column (integer >= 1) is required."); |
| 292 | TryGetString(args, "base_class_name", out var baseClassName); |
| 293 | TryGetString(args, "output_file_path", out var outputFilePath); |
| 294 | IReadOnlyList<string>? memberNames = null; |
| 295 | if (args.TryGetValue("member_names", out var mnEl) && mnEl.ValueKind == JsonValueKind.Array && GetStringArray(mnEl) is string[] arr && arr.Length > 0) |
| 296 | memberNames = arr; |
| 297 | return GenerateBaseClass.GenerateBaseClassFromClassAsync(solutionPath!, filePath!, line, column, baseClassName, outputFilePath, memberNames, ct); |
| 298 | } |
| 299 | |
| 300 | private static Task<string> GenerateOverridesAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 301 | { |
| 302 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 303 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 304 | if (!TryGetString(args, "file_path", out var filePath)) |
| 305 | throw new ArgumentException("file_path (string) is required."); |
| 306 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 307 | throw new ArgumentException("line (integer >= 1) is required."); |
| 308 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 309 | throw new ArgumentException("column (integer >= 1) is required."); |
| 310 | IReadOnlyList<string>? memberNames = null; |
| 311 | if (args.TryGetValue("member_names", out var mnEl) && mnEl.ValueKind == JsonValueKind.Array && GetStringArray(mnEl) is string[] arr && arr.Length > 0) |
| 312 | memberNames = arr; |
| 313 | var insertIntoFile = args.TryGetValue("insert_into_file", out var insEl) && insEl.ValueKind == JsonValueKind.True; |
| 314 | return GenerateOverrides.GenerateOverridesAsync(solutionPath!, filePath!, line, column, memberNames, insertIntoFile, ct); |
| 315 | } |
| 316 | |
| 317 | private static Task<string> GenerateConstructorFromMembersAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 318 | { |
| 319 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 320 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 321 | if (!TryGetString(args, "file_path", out var filePath)) |
| 322 | throw new ArgumentException("file_path (string) is required."); |
| 323 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 324 | throw new ArgumentException("line (integer >= 1) is required."); |
| 325 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 326 | throw new ArgumentException("column (integer >= 1) is required."); |
| 327 | IReadOnlyList<string>? memberNames = null; |
| 328 | if (args.TryGetValue("member_names", out var mnEl) && mnEl.ValueKind == JsonValueKind.Array && GetStringArray(mnEl) is string[] arr && arr.Length > 0) |
| 329 | memberNames = arr; |
| 330 | var insertIntoFile = args.TryGetValue("insert_into_file", out var insEl) && insEl.ValueKind == JsonValueKind.True; |
| 331 | return GenerateConstructor.GenerateConstructorFromMembersAsync(solutionPath!, filePath!, line, column, memberNames, insertIntoFile, ct); |
| 332 | } |
| 333 | |
| 334 | private static Task<string> GenerateEqualsGetHashCodeAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 335 | { |
| 336 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 337 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 338 | if (!TryGetString(args, "file_path", out var filePath)) |
| 339 | throw new ArgumentException("file_path (string) is required."); |
| 340 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 341 | throw new ArgumentException("line (integer >= 1) is required."); |
| 342 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 343 | throw new ArgumentException("column (integer >= 1) is required."); |
| 344 | IReadOnlyList<string>? memberNames = null; |
| 345 | if (args.TryGetValue("member_names", out var mnEl) && mnEl.ValueKind == JsonValueKind.Array && GetStringArray(mnEl) is string[] arr && arr.Length > 0) |
| 346 | memberNames = arr; |
| 347 | var insertIntoFile = args.TryGetValue("insert_into_file", out var insEl) && insEl.ValueKind == JsonValueKind.True; |
| 348 | return GenerateEqualsGetHashCode.GenerateEqualsGetHashCodeAsync(solutionPath!, filePath!, line, column, memberNames, insertIntoFile, ct); |
| 349 | } |
| 350 | |
| 351 | private static Task<string> MoveMembersToPartialFileAsync(IReadOnlyDictionary<string, JsonElement> args, CancellationToken ct) |
| 352 | { |
| 353 | if (!TryGetString(args, "solution_or_project_path", out var solutionPath)) |
| 354 | throw new ArgumentException("solution_or_project_path (string) is required."); |
| 355 | if (!TryGetString(args, "file_path", out var filePath)) |
| 356 | throw new ArgumentException("file_path (string) is required."); |
| 357 | if (!TryGetInt(args, "line", out var line) || line < 1) |
| 358 | throw new ArgumentException("line (integer >= 1) is required."); |
| 359 | if (!TryGetInt(args, "column", out var column) || column < 1) |
| 360 | throw new ArgumentException("column (integer >= 1) is required."); |
| 361 | if (!args.TryGetValue("member_names", out var mnEl) || mnEl.ValueKind != JsonValueKind.Array || GetStringArray(mnEl) is not string[] memberArr || memberArr.Length == 0) |
| 362 | throw new ArgumentException("member_names (non-empty array of strings) is required."); |
| 363 | if (!TryGetString(args, "output_file_path", out var outputFilePath)) |
| 364 | throw new ArgumentException("output_file_path (string) is required."); |
| 365 | var apply = args.TryGetValue("apply", out var applyEl) && applyEl.ValueKind == JsonValueKind.True; |
| 366 | var addDependentUpon = !(args.TryGetValue("add_dependent_upon", out var adEl) && adEl.ValueKind == JsonValueKind.False); |
| 367 | return MoveMembersToPartialFile.MoveAsync(solutionPath!, filePath!, line, column, memberArr, outputFilePath!, apply, addDependentUpon, ct); |
| 368 | } |
| 369 | |
| 370 | private static bool TryGetString(IReadOnlyDictionary<string, JsonElement> args, string key, out string? value) |
| 371 | { |
| 372 | value = null; |
| 373 | if (!args.TryGetValue(key, out var el) || el.ValueKind != JsonValueKind.String) |
| 374 | return false; |
| 375 | value = el.GetString(); |
| 376 | return true; |
| 377 | } |
| 378 | |
| 379 | private static bool TryGetInt(IReadOnlyDictionary<string, JsonElement> args, string key, out int value) |
| 380 | { |
| 381 | value = 0; |
| 382 | if (!args.TryGetValue(key, out var el)) |
| 383 | return false; |
| 384 | if (el.ValueKind == JsonValueKind.Number && el.TryGetInt32(out var n)) |
| 385 | { |
| 386 | value = n; |
| 387 | return true; |
| 388 | } |
| 389 | return false; |
| 390 | } |
| 391 | } |
| 392 | |