Forge
csharpdeeb25a2
1#nullable enable
2
3using CascadeIDE.Features.Workspace.DataAcquisition;
4using CascadeIDE.Features.WorkspaceNavigation.Application;
5using CascadeIDE.Models;
6using CascadeIDE.Services;
7using CascadeIDE.Services.Intercom;
8using CascadeIDE.ViewModels;
9
10namespace CascadeIDE.Features.MagicLink;
11
12/// <summary>Выполнение <c>cide://</c> на UI-потоке (ADR 0157).</summary>
13public static class CideMagicLinkExecutor
14{
15 public static async Task<string> TryExecuteAsync(
16 MainWindowViewModel vm,
17 CideMagicLinkRequest request,
18 CancellationToken cancellationToken = default)
19 {
20 cancellationToken.ThrowIfCancellationRequested();
21
22 var workspaceRoot = ResolveWorkspaceRoot(vm, request.WorkspaceRoot);
23 if (string.IsNullOrWhiteSpace(workspaceRoot))
24 return "workspace_not_loaded: укажите root= или откройте solution в IDE.";
25
26 if (!CideMagicLinkWorkspaceGuard.TryValidateRoot(workspaceRoot, out var root, out var rootError))
27 return $"workspace_rejected: {rootError}";
28
29 return request.Action switch
30 {
31 CideMagicLinkAction.Open => await ExecuteOpenAsync(vm, root, request, cancellationToken).ConfigureAwait(false),
32 CideMagicLinkAction.Reveal => await ExecuteRevealAsync(vm, root, request, cancellationToken).ConfigureAwait(false),
33 CideMagicLinkAction.Markdown => await ExecuteMarkdownAsync(vm, root, request, cancellationToken).ConfigureAwait(false),
34 _ => "unsupported_action",
35 };
36 }
37
38 private static string? ResolveWorkspaceRoot(MainWindowViewModel vm, string? requestedRoot)
39 {
40 if (!string.IsNullOrWhiteSpace(requestedRoot)
41 && CideMagicLinkWorkspaceGuard.TryValidateRoot(requestedRoot, out var normalized, out _))
42 {
43 return normalized;
44 }
45
46 return vm.GetWorkspacePath();
47 }
48
49 private static async Task<string> ExecuteOpenAsync(
50 MainWindowViewModel vm,
51 string root,
52 CideMagicLinkRequest request,
53 CancellationToken cancellationToken)
54 {
55 var slnPath = request.SolutionPath;
56 if (string.IsNullOrWhiteSpace(slnPath))
57 {
58 slnPath = Directory.EnumerateFiles(root, "*.sln", SearchOption.TopDirectoryOnly).FirstOrDefault()
59 ?? Directory.EnumerateFiles(root, "*.slnx", SearchOption.TopDirectoryOnly).FirstOrDefault();
60 }
61 else if (!Path.IsPathRooted(slnPath))
62 {
63 if (!CideMagicLinkWorkspaceGuard.TryResolveUnderRoot(root, slnPath, out var abs, out var err))
64 return $"sln_resolve_failed: {err}";
65 slnPath = abs;
66 }
67
68 if (string.IsNullOrWhiteSpace(slnPath) || !File.Exists(slnPath))
69 return "sln_not_found";
70
71 cancellationToken.ThrowIfCancellationRequested();
72 await vm.LoadSolutionAsync(slnPath).ConfigureAwait(false);
73
74 if (!string.IsNullOrWhiteSpace(request.File) || !string.IsNullOrWhiteSpace(request.BracketInner))
75 return await ExecuteRevealAsync(vm, root, request, cancellationToken).ConfigureAwait(false);
76
77 return "OK";
78 }
79
80 private static async Task<string> ExecuteRevealAsync(
81 MainWindowViewModel vm,
82 string root,
83 CideMagicLinkRequest request,
84 CancellationToken cancellationToken)
85 {
86 cancellationToken.ThrowIfCancellationRequested();
87
88 if (!string.IsNullOrWhiteSpace(request.BracketInner))
89 {
90 if (!BracketCodeReferenceParser.TryParse(request.BracketInner, out var reference, out var parseError))
91 return $"bracket_parse_failed: {parseError}";
92
93 if (!BracketCodeReferenceParser.TryToAttachmentAnchor(
94 reference,
95 vm.CurrentFilePath,
96 root,
97 vm.Workspace.SolutionPath,
98 indexDirectoryRelative: null,
99 out var anchor,
100 out var anchorError))
101 {
102 return $"anchor_resolve_failed: {anchorError}";
103 }
104
105 return await vm.RevealIntercomAttachmentInIdeAsync(anchor, select: true, cancellationToken).ConfigureAwait(false);
106 }
107
108 if (string.IsNullOrWhiteSpace(request.File))
109 return "reveal: file missing";
110
111 if (!CideMagicLinkWorkspaceGuard.TryResolveUnderRoot(root, request.File, out var absolute, out var pathError))
112 return $"file_resolve_failed: {pathError}";
113
114 if (!File.Exists(absolute))
115 return "file_not_found";
116
117 var sln = SolutionFileLocator.TryFindSolutionForSourceFile(absolute);
118 if (SolutionFileLocator.NeedsLoadSolutionBeforeBreakpoint(sln, vm.Workspace.SolutionPath))
119 {
120 cancellationToken.ThrowIfCancellationRequested();
121 await vm.LoadSolutionAsync(sln!).ConfigureAwait(false);
122 }
123
124 var line = request.LineStart is > 0 ? request.LineStart.Value : 1;
125 var endLine = request.LineEnd is > 0 ? request.LineEnd.Value : line;
126
127 await UiScheduler.Default.InvokeAsync(() =>
128 {
129 vm.IdeMcp.GoToPosition(absolute, line, 1, endLine, null);
130 }).ConfigureAwait(false);
131
132 return "OK";
133 }
134
135 private static async Task<string> ExecuteMarkdownAsync(
136 MainWindowViewModel vm,
137 string root,
138 CideMagicLinkRequest request,
139 CancellationToken cancellationToken)
140 {
141 cancellationToken.ThrowIfCancellationRequested();
142
143 if (string.IsNullOrWhiteSpace(request.DocPath))
144 return "md: doc missing";
145
146 var sln = Directory.EnumerateFiles(root, "*.sln", SearchOption.TopDirectoryOnly).FirstOrDefault()
147 ?? Directory.EnumerateFiles(root, "*.slnx", SearchOption.TopDirectoryOnly).FirstOrDefault();
148 if (!string.IsNullOrWhiteSpace(sln)
149 && SolutionFileLocator.NeedsLoadSolutionBeforeBreakpoint(sln, vm.Workspace.SolutionPath))
150 {
151 await vm.LoadSolutionAsync(sln).ConfigureAwait(false);
152 }
153
154 var docRel = request.DocPath!;
155 var opened = false;
156 await UiScheduler.Default.InvokeAsync(() =>
157 {
158 opened = WorkspaceMarkdownPreviewOpener.TryOpenRepoDocument(
159 root,
160 docRel,
161 (title, content, source) => vm.MarkdownPreviewTool.SetContent(title, content, source, request.DocLine),
162 out _);
163 if (opened)
164 {
165 vm.ApplyMfdRegionExpanded(true);
166 vm.TryNavigateToMfdShellPage(MfdShellPage.MarkdownPreview);
167 }
168 }).ConfigureAwait(false);
169
170 return opened ? "OK" : "md_open_failed";
171 }
172}
173
View only · write via MCP/CIDE