| 1 | using System.Text.Json; |
| 2 | using System.Text.Json.Serialization; |
| 3 | using CascadeIDE.Services; |
| 4 | |
| 5 | namespace CascadeIDE.Features.Editor.Application.Monaco; |
| 6 | |
| 7 | /// <summary>cide-editor bridge (ADR 0162 §4).</summary> |
| 8 | public static class CideEditorBridgeTypes |
| 9 | { |
| 10 | public const string SetModel = "editor/setModel"; |
| 11 | public const string ApplyEdits = "editor/applyEdits"; |
| 12 | public const string SetDecorations = "editor/setDecorations"; |
| 13 | public const string SetTheme = "editor/setTheme"; |
| 14 | public const string SetStickyScroll = "editor/setStickyScroll"; |
| 15 | public const string SetGutterGlyphs = "editor/setGutterGlyphs"; |
| 16 | public const string SetIntelligence = "editor/setIntelligence"; |
| 17 | public const string RevealRange = "editor/revealRange"; |
| 18 | public const string SetSelectionByOffset = "editor/setSelectionByOffset"; |
| 19 | public const string SetAgentReveal = "editor/setAgentReveal"; |
| 20 | public const string ClearAgentReveal = "editor/clearAgentReveal"; |
| 21 | public const string SetEpochDim = "editor/setEpochDim"; |
| 22 | |
| 23 | public const string DidChange = "editor/didChange"; |
| 24 | public const string DidChangeCursorSelection = "editor/didChangeCursorSelection"; |
| 25 | public const string DidScroll = "editor/didScroll"; |
| 26 | public const string DidGutterClick = "editor/didGutterClick"; |
| 27 | public const string Ready = "editor/ready"; |
| 28 | |
| 29 | /// <summary>Editor → host: tunnel hotkey from WebView2 (Ctrl+P etc.).</summary> |
| 30 | public const string HostShortcut = "host/shortcut"; |
| 31 | |
| 32 | /// <summary>Alt+drag selection released — drop target hit-test on host (Intercom composer).</summary> |
| 33 | public const string HostAttachDragComplete = "host/attach-drag-complete"; |
| 34 | |
| 35 | public const string RequestCompletion = "editor/requestCompletion"; |
| 36 | public const string CompletionResult = "editor/completionResult"; |
| 37 | public const string RequestHover = "editor/requestHover"; |
| 38 | public const string HoverResult = "editor/hoverResult"; |
| 39 | public const string RequestSignature = "editor/requestSignature"; |
| 40 | public const string SignatureResult = "editor/signatureResult"; |
| 41 | } |
| 42 | |
| 43 | public sealed record CideEditorSetModelMessage( |
| 44 | string Uri, |
| 45 | string LanguageId, |
| 46 | string Text, |
| 47 | int Version); |
| 48 | |
| 49 | public sealed record CideEditorApplyEdit( |
| 50 | int StartOffset, |
| 51 | int Length, |
| 52 | string Text); |
| 53 | |
| 54 | public sealed record CideEditorApplyEditsMessage( |
| 55 | IReadOnlyList<CideEditorApplyEdit> Edits, |
| 56 | int ExpectedVersion); |
| 57 | |
| 58 | /// <summary> |
| 59 | /// Decoration push DTO. Prefer <see cref="StartLine"/> for whole-line layers (diagnostics); |
| 60 | /// use <see cref="StartOffset"/> for token spans (highlights). See monaco-presentation-projection-v1.md. |
| 61 | /// </summary> |
| 62 | public sealed record CideEditorDecoration( |
| 63 | int StartOffset, |
| 64 | int Length, |
| 65 | string ClassName, |
| 66 | string? HoverMessage, |
| 67 | bool IsWholeLine = false, |
| 68 | string? GlyphMarginClassName = null, |
| 69 | int? StartLine = null, |
| 70 | int? StartColumn = null, |
| 71 | int? EndLine = null, |
| 72 | int? EndColumn = null); |
| 73 | |
| 74 | public sealed record CideEditorSetSelectionMessage(int SelectionStart, int SelectionLength); |
| 75 | |
| 76 | public sealed record CideEditorSetAgentRevealMessage( |
| 77 | int StartLine, |
| 78 | int EndLine, |
| 79 | bool Persistent, |
| 80 | int? DurationMs); |
| 81 | |
| 82 | public sealed record CideEditorSetEpochDimMessage(bool Dimmed); |
| 83 | |
| 84 | public sealed record CideEditorSetDecorationsMessage( |
| 85 | string SetId, |
| 86 | IReadOnlyList<CideEditorDecoration> Decorations, |
| 87 | int? ExpectedModelVersion = null); |
| 88 | |
| 89 | public sealed record CideEditorStickyScrollMessage(string? Label); |
| 90 | |
| 91 | public sealed record CideEditorGutterGlyph( |
| 92 | int LineOneBased, |
| 93 | string TextGlyph, |
| 94 | string? ToolTip, |
| 95 | string VisualKind); |
| 96 | |
| 97 | public sealed record CideEditorSetGutterGlyphsMessage( |
| 98 | IReadOnlyList<CideEditorGutterGlyph> Glyphs); |
| 99 | |
| 100 | public sealed record CideEditorSetIntelligenceMessage(bool Enabled); |
| 101 | |
| 102 | public sealed record CideEditorRevealRangeMessage( |
| 103 | int StartLine, |
| 104 | int EndLine, |
| 105 | int? Column, |
| 106 | bool Select = true); |
| 107 | |
| 108 | public sealed record CideEditorDefinitionLocation( |
| 109 | string FilePath, |
| 110 | int Line, |
| 111 | int Column); |
| 112 | |
| 113 | public sealed record CideEditorReferenceLocation( |
| 114 | string FilePath, |
| 115 | int Line, |
| 116 | int Column, |
| 117 | int? EndLine = null, |
| 118 | int? EndColumn = null); |
| 119 | |
| 120 | public sealed record CideEditorReferencesResultMessage( |
| 121 | int RequestId, |
| 122 | IReadOnlyList<CideEditorReferenceLocation> Locations); |
| 123 | |
| 124 | public sealed record CideEditorFormatResultMessage( |
| 125 | int RequestId, |
| 126 | string? Text); |
| 127 | |
| 128 | public sealed record CideEditorCodeActionItem( |
| 129 | string Title, |
| 130 | string Kind, |
| 131 | string? Text, |
| 132 | int? ActionIndex = null); |
| 133 | |
| 134 | public sealed record CideEditorDocumentTextChange( |
| 135 | string FilePath, |
| 136 | string Text, |
| 137 | bool IsNewFile = false, |
| 138 | string? PreviousFilePath = null); |
| 139 | |
| 140 | public sealed record CideEditorWorkspaceEditResultMessage( |
| 141 | int RequestId, |
| 142 | bool Ok, |
| 143 | string? Error, |
| 144 | IReadOnlyList<CideEditorDocumentTextChange> Changes); |
| 145 | |
| 146 | public sealed record CideEditorCodeActionResultMessage( |
| 147 | int RequestId, |
| 148 | IReadOnlyList<CideEditorCodeActionItem> Actions); |
| 149 | |
| 150 | public sealed record CideEditorDefinitionResultMessage( |
| 151 | int RequestId, |
| 152 | CideEditorDefinitionLocation? Location); |
| 153 | |
| 154 | public sealed record CideEditorSetThemeMessage(string ThemeName); |
| 155 | |
| 156 | public sealed record CideEditorCompletionItem( |
| 157 | string Label, |
| 158 | string InsertText, |
| 159 | string? Detail, |
| 160 | string? Kind = null); |
| 161 | |
| 162 | public sealed record CideEditorCompletionResultMessage( |
| 163 | int RequestId, |
| 164 | IReadOnlyList<CideEditorCompletionItem> Items); |
| 165 | |
| 166 | public sealed record CideEditorHoverResultMessage( |
| 167 | int RequestId, |
| 168 | string? Markdown); |
| 169 | |
| 170 | public sealed record CideEditorSignatureResultMessage( |
| 171 | int RequestId, |
| 172 | string? Signature); |
| 173 | |
| 174 | public sealed record CideEditorInlayHint( |
| 175 | int Line, |
| 176 | int Column, |
| 177 | string Label, |
| 178 | string Kind = "type", |
| 179 | bool AtEndOfLine = false); |
| 180 | |
| 181 | public sealed record CideEditorInlayHintsResultMessage( |
| 182 | int RequestId, |
| 183 | IReadOnlyList<CideEditorInlayHint> Hints); |
| 184 | |
| 185 | public sealed record CideEditorCodeLensItem( |
| 186 | string Id, |
| 187 | int Line, |
| 188 | int Column, |
| 189 | string Title); |
| 190 | |
| 191 | public sealed record CideEditorCodeLensResultMessage( |
| 192 | int RequestId, |
| 193 | IReadOnlyList<CideEditorCodeLensItem> Lenses); |
| 194 | |
| 195 | public sealed record CideEditorSemanticTokensLegend( |
| 196 | IReadOnlyList<string> TokenTypes, |
| 197 | IReadOnlyList<string> TokenModifiers); |
| 198 | |
| 199 | public sealed record CideEditorSemanticTokensData( |
| 200 | IReadOnlyList<uint> Data, |
| 201 | string? ResultId); |
| 202 | |
| 203 | public sealed record CideEditorSemanticTokensResultMessage( |
| 204 | int RequestId, |
| 205 | IReadOnlyList<uint> Data, |
| 206 | string? ResultId); |
| 207 | |
| 208 | public sealed record CideEditorInboundMessage( |
| 209 | string Type, |
| 210 | [property: JsonPropertyName("version")] int? Version, |
| 211 | [property: JsonPropertyName("text")] string? Text, |
| 212 | [property: JsonPropertyName("caretOffset")] int? CaretOffset, |
| 213 | [property: JsonPropertyName("selectionStart")] int? SelectionStart, |
| 214 | [property: JsonPropertyName("selectionLength")] int? SelectionLength, |
| 215 | [property: JsonPropertyName("requestId")] int? RequestId, |
| 216 | [property: JsonPropertyName("line")] int? Line, |
| 217 | [property: JsonPropertyName("column")] int? Column, |
| 218 | [property: JsonPropertyName("topLine")] int? TopLine, |
| 219 | [property: JsonPropertyName("lensId")] string? LensId, |
| 220 | [property: JsonPropertyName("filePath")] string? FilePath, |
| 221 | [property: JsonPropertyName("error")] string? Error, |
| 222 | [property: JsonPropertyName("actionIndex")] int? ActionIndex, |
| 223 | [property: JsonPropertyName("newName")] string? NewName, |
| 224 | [property: JsonPropertyName("endLine")] int? EndLine, |
| 225 | [property: JsonPropertyName("endColumn")] int? EndColumn, |
| 226 | [property: JsonPropertyName("id")] string? ShortcutId); |
| 227 | |
| 228 | public static class CideEditorBridgeJson |
| 229 | { |
| 230 | public static readonly JsonSerializerOptions Options = new() |
| 231 | { |
| 232 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, |
| 233 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, |
| 234 | }; |
| 235 | |
| 236 | public static string WrapOutbound(string type, object payload) => |
| 237 | JsonSerializer.Serialize(new { type, payload }, Options); |
| 238 | |
| 239 | public static CideEditorInboundMessage? TryParseInbound(string? body) |
| 240 | { |
| 241 | if (string.IsNullOrWhiteSpace(body)) |
| 242 | return null; |
| 243 | try |
| 244 | { |
| 245 | using var doc = JsonDocument.Parse(body); |
| 246 | var root = doc.RootElement; |
| 247 | if (!root.TryGetProperty("type", out var typeEl)) |
| 248 | return null; |
| 249 | var type = typeEl.GetString() ?? ""; |
| 250 | int? version = TryInt(root, "version"); |
| 251 | string? text = root.TryGetProperty("text", out var t) ? t.GetString() : null; |
| 252 | int? caret = TryInt(root, "caretOffset"); |
| 253 | int? selStart = TryInt(root, "selectionStart"); |
| 254 | int? selLen = TryInt(root, "selectionLength"); |
| 255 | int? requestId = TryInt(root, "requestId"); |
| 256 | int? line = TryInt(root, "line"); |
| 257 | int? column = TryInt(root, "column"); |
| 258 | int? topLine = TryInt(root, "topLine"); |
| 259 | string? lensId = root.TryGetProperty("lensId", out var lens) ? lens.GetString() : null; |
| 260 | string? filePath = root.TryGetProperty("filePath", out var fp) ? fp.GetString() : null; |
| 261 | string? error = root.TryGetProperty("error", out var err) ? err.GetString() : null; |
| 262 | int? actionIndex = TryInt(root, "actionIndex"); |
| 263 | string? newName = root.TryGetProperty("newName", out var nn) ? nn.GetString() : null; |
| 264 | int? endLine = TryInt(root, "endLine"); |
| 265 | int? endColumn = TryInt(root, "endColumn"); |
| 266 | string? shortcutId = root.TryGetProperty("id", out var idEl) ? idEl.GetString() : null; |
| 267 | return new CideEditorInboundMessage( |
| 268 | type, version, text, caret, selStart, selLen, requestId, line, column, topLine, lensId, filePath, error, |
| 269 | actionIndex, newName, endLine, endColumn, shortcutId); |
| 270 | } |
| 271 | catch |
| 272 | { |
| 273 | return null; |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | private static int? TryInt(JsonElement root, string name) => |
| 278 | root.TryGetProperty(name, out var el) && el.TryGetInt32(out var value) ? value : null; |
| 279 | } |
| 280 | |
| 281 | public static class CideEditorLanguageIds |
| 282 | { |
| 283 | public static string FromFilePath(string? filePath) => |
| 284 | EditorLanguageSupport.GetMonacoLanguageId(filePath); |
| 285 | |
| 286 | public static bool SupportsRoslynIntelligence(string? filePath) => |
| 287 | string.Equals(Path.GetExtension(filePath), ".cs", StringComparison.OrdinalIgnoreCase) |
| 288 | || string.Equals(Path.GetExtension(filePath), ".csx", StringComparison.OrdinalIgnoreCase); |
| 289 | } |
| 290 | |
| 291 | public static class MonacoEditorAssetLocator |
| 292 | { |
| 293 | public const string VirtualHostName = "cide-editor.local"; |
| 294 | |
| 295 | public static string GetCideEditorRoot() |
| 296 | { |
| 297 | var baseDir = AppContext.BaseDirectory; |
| 298 | var candidate = Path.Combine(baseDir, "Assets", "cide-editor"); |
| 299 | if (Directory.Exists(candidate)) |
| 300 | return candidate; |
| 301 | |
| 302 | var dev = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "Assets", "cide-editor")); |
| 303 | if (Directory.Exists(dev)) |
| 304 | return dev; |
| 305 | |
| 306 | return candidate; |
| 307 | } |
| 308 | |
| 309 | public static string GetIndexHtmlPath() |
| 310 | { |
| 311 | var path = Path.Combine(GetCideEditorRoot(), "index.html"); |
| 312 | return File.Exists(path) ? path : path; |
| 313 | } |
| 314 | |
| 315 | public static string GetMonacoVsPath() |
| 316 | { |
| 317 | var local = Path.Combine(GetCideEditorRoot(), "monaco", "min", "vs"); |
| 318 | return Directory.Exists(local) ? local.Replace('\\', '/') : ""; |
| 319 | } |
| 320 | |
| 321 | public static Uri GetIndexUri() => |
| 322 | new($"https://{VirtualHostName}/index.html"); |
| 323 | |
| 324 | public static Uri GetFileIndexUri() |
| 325 | { |
| 326 | var path = Path.GetFullPath(GetIndexHtmlPath()); |
| 327 | return new Uri(path); |
| 328 | } |
| 329 | } |
| 330 | |
| 331 | |