Forge
csharpdeeb25a2
1#nullable enable
2
3namespace CascadeIDE.Features.MagicLink;
4
5public enum CideMagicLinkAction
6{
7 Reveal,
8 Open,
9 Markdown,
10}
11
12/// <summary>Разобранный <c>cide://</c> URI (ADR 0157).</summary>
13public sealed record CideMagicLinkRequest(
14 CideMagicLinkAction Action,
15 string? WorkspaceRoot,
16 string? File,
17 int? LineStart,
18 int? LineEnd,
19 string? BracketInner,
20 string? SolutionPath,
21 string? DocPath,
22 int? DocLine);
23
24public static class CideMagicLinkUri
25{
26 public const string Scheme = "cide";
27
28 public static bool TryParse(string? raw, out CideMagicLinkRequest request, out string error)
29 {
30 request = null!;
31 error = "";
32
33 if (string.IsNullOrWhiteSpace(raw))
34 {
35 error = "Пустой URI.";
36 return false;
37 }
38
39 var trimmed = raw.Trim();
40 if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var uri)
41 || !string.Equals(uri.Scheme, Scheme, StringComparison.OrdinalIgnoreCase))
42 {
43 error = "Ожидается абсолютный cide:// URI.";
44 return false;
45 }
46
47 var action = ParseAction(uri);
48 if (action is null)
49 {
50 error = $"Неизвестная команда «{uri.Host}» (ожидается reveal, open, md).";
51 return false;
52 }
53
54 var query = ParseQuery(uri.Query);
55 var root = NormalizeOptionalPath(Get(query, "root"));
56 var file = NormalizeRepoRelative(Get(query, "f"));
57 var bracket = Get(query, "b");
58 var lineStart = ParsePositiveInt(Get(query, "l"));
59 var lineEnd = ParsePositiveInt(Get(query, "le")) ?? lineStart;
60 var sln = NormalizeOptionalPath(Get(query, "sln"));
61 if (sln is null)
62 sln = NormalizeRepoRelative(Get(query, "sln"));
63 var doc = NormalizeRepoRelative(Get(query, "doc"));
64 var docLine = ParsePositiveInt(Get(query, "line"));
65
66 if (action == CideMagicLinkAction.Markdown)
67 {
68 docLine = ParsePositiveInt(Get(query, "l")) ?? docLine;
69 if (string.IsNullOrWhiteSpace(doc))
70 {
71 error = "md: обязателен параметр doc.";
72 return false;
73 }
74 }
75 else if (action == CideMagicLinkAction.Reveal)
76 {
77 if (string.IsNullOrWhiteSpace(file) && string.IsNullOrWhiteSpace(bracket))
78 {
79 error = "reveal: нужен f или b.";
80 return false;
81 }
82 }
83
84 request = new CideMagicLinkRequest(
85 action.Value,
86 root,
87 file,
88 lineStart,
89 lineEnd,
90 bracket,
91 sln,
92 doc,
93 docLine);
94 return true;
95 }
96
97 private static CideMagicLinkAction? ParseAction(Uri uri)
98 {
99 var host = uri.Host;
100 if (string.IsNullOrWhiteSpace(host) && uri.AbsolutePath.Length > 1)
101 host = uri.AbsolutePath.TrimStart('/').Split('/')[0];
102
103 return host.ToLowerInvariant() switch
104 {
105 "reveal" => CideMagicLinkAction.Reveal,
106 "open" => CideMagicLinkAction.Open,
107 "md" or "markdown" or "preview" => CideMagicLinkAction.Markdown,
108 _ => null,
109 };
110 }
111
112 private static Dictionary<string, string> ParseQuery(string query)
113 {
114 var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
115 if (string.IsNullOrWhiteSpace(query))
116 return result;
117
118 var q = query.StartsWith('?') ? query[1..] : query;
119 foreach (var part in q.Split('&', StringSplitOptions.RemoveEmptyEntries))
120 {
121 var eq = part.IndexOf('=');
122 if (eq < 0)
123 {
124 result[Uri.UnescapeDataString(part)] = "";
125 continue;
126 }
127
128 var key = Uri.UnescapeDataString(part[..eq]);
129 var value = Uri.UnescapeDataString(part[(eq + 1)..]);
130 result[key] = value;
131 }
132
133 return result;
134 }
135
136 private static string? Get(IReadOnlyDictionary<string, string> query, string key) =>
137 query.TryGetValue(key, out var value) ? value : null;
138
139 private static string? NormalizeOptionalPath(string? value)
140 {
141 if (string.IsNullOrWhiteSpace(value))
142 return null;
143
144 try
145 {
146 return Path.GetFullPath(value.Trim());
147 }
148 catch
149 {
150 return null;
151 }
152 }
153
154 private static string? NormalizeRepoRelative(string? value)
155 {
156 if (string.IsNullOrWhiteSpace(value))
157 return null;
158
159 return value.Trim().Replace('\\', '/');
160 }
161
162 private static int? ParsePositiveInt(string? value) =>
163 int.TryParse(value, out var n) && n > 0 ? n : null;
164}
165
View only · write via MCP/CIDE