Forge
csharpdeeb25a2
1using System.Text.Json;
2using System.Text.Json.Nodes;
3using CascadeIDE.Features.Editor.Application.Monaco;
4using CascadeIDE.Services.Lsp;
5
6#nullable enable
7
8namespace CascadeIDE.Features.Lsp.DataAcquisition;
9
10/// <summary>LSP textDocument/* intelligence (ADR 0163 M8).</summary>
11public sealed partial class CSharpLspDiagnosticsHost
12{
13 public async Task<IReadOnlyList<CideEditorCompletionItem>> RequestCompletionAsync(
14 string filePath,
15 string text,
16 int line1,
17 int col1,
18 CancellationToken ct)
19 {
20 if (!IsActive || !IsCSharpPath(filePath) || _session is null || line1 < 1 || col1 < 1)
21 return [];
22
23 await SyncFullTextForRequestAsync(filePath, text, ct).ConfigureAwait(false);
24 var uri = LspFileUri.PathToFileUri(CanonicalFilePath.Normalize(filePath));
25 var id = _session.AllocateRequestId();
26 var msg = new JsonObject
27 {
28 ["jsonrpc"] = "2.0",
29 ["id"] = id,
30 ["method"] = "textDocument/completion",
31 ["params"] = new JsonObject
32 {
33 ["textDocument"] = new JsonObject { ["uri"] = uri },
34 ["position"] = new JsonObject { ["line"] = line1 - 1, ["character"] = col1 - 1 },
35 },
36 };
37
38 try
39 {
40 using var doc = await _session.SendRequestAsync(msg, id, TimeSpan.FromSeconds(3), ct).ConfigureAwait(false);
41 return ParseCompletionResponse(doc);
42 }
43 catch
44 {
45 return [];
46 }
47 }
48
49 public async Task<string?> RequestSignatureHelpAsync(
50 string filePath,
51 string text,
52 int line1,
53 int col1,
54 CancellationToken ct)
55 {
56 if (!IsActive || !IsCSharpPath(filePath) || _session is null || line1 < 1 || col1 < 1)
57 return null;
58
59 await SyncFullTextForRequestAsync(filePath, text, ct).ConfigureAwait(false);
60 var uri = LspFileUri.PathToFileUri(CanonicalFilePath.Normalize(filePath));
61 var id = _session.AllocateRequestId();
62 var msg = new JsonObject
63 {
64 ["jsonrpc"] = "2.0",
65 ["id"] = id,
66 ["method"] = "textDocument/signatureHelp",
67 ["params"] = new JsonObject
68 {
69 ["textDocument"] = new JsonObject { ["uri"] = uri },
70 ["position"] = new JsonObject { ["line"] = line1 - 1, ["character"] = col1 - 1 },
71 },
72 };
73
74 try
75 {
76 using var doc = await _session.SendRequestAsync(msg, id, TimeSpan.FromSeconds(8), ct).ConfigureAwait(false);
77 return ParseSignatureHelpResponse(doc);
78 }
79 catch
80 {
81 return null;
82 }
83 }
84
85 public async Task<CideEditorDefinitionLocation?> RequestDefinitionAsync(
86 string filePath,
87 string text,
88 int line1,
89 int col1,
90 CancellationToken ct)
91 {
92 if (!IsActive || !IsCSharpPath(filePath) || _session is null || line1 < 1 || col1 < 1)
93 return null;
94
95 await SyncFullTextForRequestAsync(filePath, text, ct).ConfigureAwait(false);
96 var uri = LspFileUri.PathToFileUri(CanonicalFilePath.Normalize(filePath));
97 var id = _session.AllocateRequestId();
98 var msg = new JsonObject
99 {
100 ["jsonrpc"] = "2.0",
101 ["id"] = id,
102 ["method"] = "textDocument/definition",
103 ["params"] = new JsonObject
104 {
105 ["textDocument"] = new JsonObject { ["uri"] = uri },
106 ["position"] = new JsonObject { ["line"] = line1 - 1, ["character"] = col1 - 1 },
107 },
108 };
109
110 try
111 {
112 using var doc = await _session.SendRequestAsync(msg, id, TimeSpan.FromSeconds(8), ct).ConfigureAwait(false);
113 return ParseDefinitionResponse(doc);
114 }
115 catch
116 {
117 return null;
118 }
119 }
120
121 public async Task<IReadOnlyList<CideEditorReferenceLocation>> RequestReferencesAsync(
122 string filePath,
123 string text,
124 int line1,
125 int col1,
126 CancellationToken ct)
127 {
128 if (!IsActive || !IsCSharpPath(filePath) || _session is null || line1 < 1 || col1 < 1)
129 return [];
130
131 await SyncFullTextForRequestAsync(filePath, text, ct).ConfigureAwait(false);
132 var uri = LspFileUri.PathToFileUri(CanonicalFilePath.Normalize(filePath));
133 var id = _session.AllocateRequestId();
134 var msg = new JsonObject
135 {
136 ["jsonrpc"] = "2.0",
137 ["id"] = id,
138 ["method"] = "textDocument/references",
139 ["params"] = new JsonObject
140 {
141 ["textDocument"] = new JsonObject { ["uri"] = uri },
142 ["position"] = new JsonObject { ["line"] = line1 - 1, ["character"] = col1 - 1 },
143 ["context"] = new JsonObject { ["includeDeclaration"] = true },
144 },
145 };
146
147 try
148 {
149 using var doc = await _session.SendRequestAsync(msg, id, TimeSpan.FromSeconds(8), ct).ConfigureAwait(false);
150 return ParseReferencesResponse(doc);
151 }
152 catch
153 {
154 return [];
155 }
156 }
157
158 private static bool IsCSharpPath(string filePath) =>
159 filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)
160 || filePath.EndsWith(".csx", StringComparison.OrdinalIgnoreCase);
161
162 private static IReadOnlyList<CideEditorCompletionItem> ParseCompletionResponse(JsonDocument? doc)
163 {
164 if (doc is null)
165 return [];
166 var root = doc.RootElement;
167 if (root.TryGetProperty("error", out _))
168 return [];
169 if (!root.TryGetProperty("result", out var result) || result.ValueKind == JsonValueKind.Null)
170 return [];
171
172 JsonElement itemsEl;
173 if (result.ValueKind == JsonValueKind.Array)
174 itemsEl = result;
175 else if (result.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Array)
176 itemsEl = items;
177 else
178 return [];
179
180 var list = new List<CideEditorCompletionItem>();
181 foreach (var item in itemsEl.EnumerateArray())
182 {
183 var label = item.TryGetProperty("label", out var l)
184 ? l.ValueKind == JsonValueKind.String ? l.GetString() ?? "" : l.GetRawText()
185 : "";
186 if (string.IsNullOrEmpty(label))
187 continue;
188 var insert = item.TryGetProperty("insertText", out var ins) && ins.ValueKind == JsonValueKind.String
189 ? ins.GetString() ?? label
190 : label;
191 var detail = item.TryGetProperty("detail", out var d) && d.ValueKind == JsonValueKind.String
192 ? d.GetString()
193 : null;
194 int? lspKind = item.TryGetProperty("kind", out var k) && k.TryGetInt32(out var kindVal)
195 ? kindVal
196 : null;
197 list.Add(new CideEditorCompletionItem(
198 label,
199 insert,
200 detail,
201 CideEditorCompletionKindMapper.FromLspKind(lspKind)));
202 }
203
204 return list;
205 }
206
207 private static string? ParseSignatureHelpResponse(JsonDocument? doc)
208 {
209 if (doc is null)
210 return null;
211 var root = doc.RootElement;
212 if (root.TryGetProperty("error", out _))
213 return null;
214 if (!root.TryGetProperty("result", out var result) || result.ValueKind == JsonValueKind.Null)
215 return null;
216 if (!result.TryGetProperty("signatures", out var sigs) || sigs.ValueKind != JsonValueKind.Array)
217 return null;
218
219 var idx = result.TryGetProperty("activeSignature", out var active) && active.TryGetInt32(out var ai)
220 ? ai
221 : 0;
222 var i = 0;
223 foreach (var sig in sigs.EnumerateArray())
224 {
225 if (i++ != idx)
226 continue;
227 if (sig.TryGetProperty("label", out var label))
228 {
229 return label.ValueKind == JsonValueKind.String
230 ? label.GetString()
231 : label.GetRawText();
232 }
233
234 break;
235 }
236
237 return null;
238 }
239
240 private static CideEditorDefinitionLocation? ParseDefinitionResponse(JsonDocument? doc)
241 {
242 if (doc is null)
243 return null;
244 var root = doc.RootElement;
245 if (root.TryGetProperty("error", out _))
246 return null;
247 if (!root.TryGetProperty("result", out var result) || result.ValueKind == JsonValueKind.Null)
248 return null;
249
250 if (result.ValueKind == JsonValueKind.Array)
251 {
252 foreach (var loc in result.EnumerateArray())
253 {
254 var mapped = MapLocation(loc);
255 if (mapped is not null)
256 return mapped;
257 }
258
259 return null;
260 }
261
262 return MapLocation(result);
263 }
264
265 private static IReadOnlyList<CideEditorReferenceLocation> ParseReferencesResponse(JsonDocument? doc)
266 {
267 if (doc is null)
268 return [];
269 var root = doc.RootElement;
270 if (root.TryGetProperty("error", out _))
271 return [];
272 if (!root.TryGetProperty("result", out var result) || result.ValueKind != JsonValueKind.Array)
273 return [];
274
275 var list = new List<CideEditorReferenceLocation>();
276 foreach (var loc in result.EnumerateArray())
277 {
278 var mapped = MapReferenceLocation(loc);
279 if (mapped is not null)
280 list.Add(mapped);
281 }
282
283 return list;
284 }
285
286 private static CideEditorReferenceLocation? MapReferenceLocation(JsonElement loc)
287 {
288 if (!loc.TryGetProperty("uri", out var uriEl))
289 return null;
290 var uri = uriEl.GetString();
291 if (string.IsNullOrEmpty(uri) || !LspFileUri.TryUriToPath(uri, out var path))
292 return null;
293 if (!loc.TryGetProperty("range", out var range)
294 || !range.TryGetProperty("start", out var start))
295 return null;
296 if (!TryGetPosition(start, out var line0, out var char0))
297 return null;
298
299 int? endLine = null;
300 int? endColumn = null;
301 if (range.TryGetProperty("end", out var end) && TryGetPosition(end, out var el, out var ec))
302 {
303 endLine = el + 1;
304 endColumn = ec + 1;
305 }
306
307 return new CideEditorReferenceLocation(path, line0 + 1, char0 + 1, endLine, endColumn);
308 }
309
310 private static CideEditorDefinitionLocation? MapLocation(JsonElement loc)
311 {
312 if (!loc.TryGetProperty("uri", out var uriEl))
313 return null;
314 var uri = uriEl.GetString();
315 if (string.IsNullOrEmpty(uri) || !LspFileUri.TryUriToPath(uri, out var path))
316 return null;
317 if (!loc.TryGetProperty("range", out var range)
318 || !range.TryGetProperty("start", out var start))
319 return null;
320 if (!TryGetPosition(start, out var line0, out var char0))
321 return null;
322 return new CideEditorDefinitionLocation(path, line0 + 1, char0 + 1);
323 }
324}
325
View only · write via MCP/CIDE