Skip to main content

Mountain/RPC/CocoonService/GenericRequest/
Dispatcher.rs

1//! Dispatcher for the generic `process_mountain_request` gRPC endpoint.
2//!
3//! Legacy JSON-over-gRPC rail used by Cocoon's
4//! `MountainGRPCClient.sendRequest(method, params)` for method names that
5//! predate the typed proto endpoints. Match arms call into Mountain's
6//! environment directly via `Service.environment.*`.
7
8use std::time::UNIX_EPOCH;
9
10use serde_json::json;
11use tonic::{Request, Response, Status};
12use url::Url;
13use CommonLibrary::{
14	Command::CommandExecutor::CommandExecutor,
15	LanguageFeature::{
16		DTO::PositionDTO::PositionDTO,
17		LanguageFeatureProviderRegistry::LanguageFeatureProviderRegistry,
18	},
19};
20use ::Vine::Generated::{GenericRequest as GenericRequestMsg, GenericResponse, RpcError};
21
22use crate::{RPC::CocoonService::CocoonServiceImpl, dev_log};
23
24pub async fn Fn(
25	Service:&CocoonServiceImpl,
26
27	request:Request<GenericRequestMsg>,
28) -> Result<Response<GenericResponse>, Status> {
29	let Req = request.into_inner();
30
31	let RequestId = Req.request_identifier;
32
33	dev_log!(
34		"cocoon",
35		"[CocoonService] generic request: method={} id={}",
36		Req.method,
37		RequestId
38	);
39
40	/// Serialise a value into the `result` bytes of a GenericResponse.
41	fn OkResponse(RequestId:u64, Value:&impl serde::Serialize) -> Response<GenericResponse> {
42		let Bytes = serde_json::to_vec(Value).unwrap_or_default();
43
44		Response::new(GenericResponse { request_identifier:RequestId, result:Bytes, error:None })
45	}
46
47	/// Build an error GenericResponse.
48	fn ErrResponse(RequestId:u64, Code:i32, Message:String) -> Response<GenericResponse> {
49		Response::new(GenericResponse {
50			request_identifier:RequestId,
51			result:Vec::new(),
52			error:Some(RpcError { code:Code, message:Message, data:Vec::new() }),
53		})
54	}
55
56	// Deserialise the generic parameter bytes as a JSON value
57	let Params:serde_json::Value = if Req.parameter.is_empty() {
58		serde_json::Value::Null
59	} else {
60		serde_json::from_slice(&Req.parameter).unwrap_or(serde_json::Value::Null)
61	};
62
63	match Req.method.as_str() {
64		// ---- File System ---- (Cocoon FileSystemService uses these paths)
65		"fs.readFile" | "file:read" => {
66			let Path = Params
67				.as_str()
68				.or_else(|| Params.get("path").and_then(|V| V.as_str()))
69				.unwrap_or("");
70
71			match tokio::fs::read(Path).await {
72				Ok(Content) => Ok(OkResponse(RequestId, &Content)),
73
74				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.readFile: {}", Error))),
75			}
76		},
77
78		"fs.writeFile" | "file:write" => {
79			let Path = Params.get("path").and_then(|V| V.as_str()).unwrap_or("");
80
81			let Content:Vec<u8> = Params
82				.get("content")
83				.and_then(|V| V.as_array())
84				.map(|A| A.iter().filter_map(|B| B.as_u64().map(|N| N as u8)).collect())
85				.unwrap_or_default();
86
87			match tokio::fs::write(Path, &Content).await {
88				Ok(()) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
89
90				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.writeFile: {}", Error))),
91			}
92		},
93
94		"fs.stat" | "file:stat" => {
95			let Path = Params
96				.as_str()
97				.or_else(|| Params.get("path").and_then(|V| V.as_str()))
98				.unwrap_or("");
99
100			match tokio::fs::metadata(Path).await {
101				Ok(Meta) => {
102					let Mtime = Meta
103						.modified()
104						.ok()
105						.and_then(|T| T.duration_since(UNIX_EPOCH).ok())
106						.map(|D| D.as_millis() as u64)
107						.unwrap_or(0);
108
109					Ok(OkResponse(
110						RequestId,
111						&json!({
112							"type": if Meta.is_dir() { 2 } else { 1 },
113							"is_file": Meta.is_file(),
114							"is_directory": Meta.is_dir(),
115							"size": Meta.len(),
116							"mtime": Mtime,
117						}),
118					))
119				},
120
121				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.stat: {}", Error))),
122			}
123		},
124
125		"fs.listDir" | "fs.readdir" | "file:readdir" => {
126			let Path = Params
127				.as_str()
128				.or_else(|| Params.get("path").and_then(|V| V.as_str()))
129				.unwrap_or("");
130
131			match tokio::fs::read_dir(Path).await {
132				Ok(mut Entries) => {
133					// Return [{name, type}] where type 1=File 2=Directory
134					let mut Items:Vec<serde_json::Value> = Vec::new();
135
136					while let Ok(Some(Entry)) = Entries.next_entry().await {
137						if let Some(Name) = Entry.file_name().to_str() {
138							let IsDir = Entry.file_type().await.map(|T| T.is_dir()).unwrap_or(false);
139
140							Items.push(json!({ "name": Name, "type": if IsDir { 2u32 } else { 1u32 } }));
141						}
142					}
143
144					Ok(OkResponse(RequestId, &Items))
145				},
146
147				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.listDir: {}", Error))),
148			}
149		},
150
151		"fs.createDir" | "file:mkdir" => {
152			let Path = Params
153				.as_str()
154				.or_else(|| Params.get("path").and_then(|V| V.as_str()))
155				.unwrap_or("");
156
157			match tokio::fs::create_dir_all(Path).await {
158				Ok(()) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
159
160				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.createDir: {}", Error))),
161			}
162		},
163
164		"fs.delete" | "file:delete" => {
165			let Path = Params
166				.as_str()
167				.or_else(|| Params.get("path").and_then(|V| V.as_str()))
168				.unwrap_or("");
169
170			let Result = if std::path::Path::new(Path).is_dir() {
171				tokio::fs::remove_dir_all(Path).await
172			} else {
173				tokio::fs::remove_file(Path).await
174			};
175
176			match Result {
177				Ok(()) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
178
179				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.delete: {}", Error))),
180			}
181		},
182
183		"fs.rename" | "file:move" => {
184			let From = Params.get("from").and_then(|V| V.as_str()).unwrap_or("");
185
186			let To = Params.get("to").and_then(|V| V.as_str()).unwrap_or("");
187
188			match tokio::fs::rename(From, To).await {
189				Ok(()) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
190
191				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("fs.rename: {}", Error))),
192			}
193		},
194
195		// ---- Commands ----
196		"commands.execute" => {
197			let CommandId = Params.get("id").and_then(|V| V.as_str()).unwrap_or("").to_string();
198
199			let Arg = Params.get("arg").cloned().unwrap_or(serde_json::Value::Null);
200
201			match Service.environment.ExecuteCommand(CommandId, Arg).await {
202				Ok(Value) => Ok(OkResponse(RequestId, &Value)),
203
204				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
205			}
206		},
207
208		// ---- Commands (Cocoon MountainGRPCClient format) ----
209		"executeCommand" => {
210			let CommandId = Params.get("commandId").and_then(|V| V.as_str()).unwrap_or("").to_string();
211
212			let Arg = Params
213				.get("arguments")
214				.and_then(|A| A.as_array())
215				.and_then(|A| A.first())
216				.cloned()
217				.unwrap_or(serde_json::Value::Null);
218
219			match Service.environment.ExecuteCommand(CommandId, Arg).await {
220				Ok(Value) => Ok(OkResponse(RequestId, &json!({ "result": Value }))),
221
222				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
223			}
224		},
225
226		"unregisterCommand" => {
227			let ExtensionId = Params.get("extensionId").and_then(|V| V.as_str()).unwrap_or("").to_string();
228
229			let CommandId = Params.get("commandId").and_then(|V| V.as_str()).unwrap_or("").to_string();
230
231			match Service.environment.UnregisterCommand(ExtensionId, CommandId).await {
232				Ok(()) => Ok(OkResponse(RequestId, &json!({ "success": true }))),
233
234				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
235			}
236		},
237
238		// ---- Window dialogs (Window.ts method names) ----
239		"UserInterface.ShowOpenDialog" => {
240			use CommonLibrary::UserInterface::{
241				DTO::OpenDialogOptionsDTO::OpenDialogOptionsDTO,
242				UserInterfaceProvider::UserInterfaceProvider,
243			};
244
245			let Title = Params
246				.get(0)
247				.and_then(|V| V.get("title"))
248				.and_then(|T| T.as_str())
249				.map(|S| S.to_string());
250
251			let Options = OpenDialogOptionsDTO {
252				Base:CommonLibrary::UserInterface::DTO::DialogOptionsDTO::DialogOptionsDTO {
253					Title,
254					..Default::default()
255				},
256				..OpenDialogOptionsDTO::default()
257			};
258
259			match Service.environment.ShowOpenDialog(Some(Options)).await {
260				Ok(Some(Paths)) => {
261					let Uris:Vec<String> = Paths.iter().map(|P| format!("file://{}", P.display())).collect();
262
263					Ok(OkResponse(RequestId, &json!(Uris)))
264				},
265
266				Ok(None) => Ok(OkResponse(RequestId, &json!(serde_json::Value::Array(vec![])))),
267
268				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
269			}
270		},
271
272		"UserInterface.ShowSaveDialog" => {
273			use CommonLibrary::UserInterface::{
274				DTO::SaveDialogOptionsDTO::SaveDialogOptionsDTO,
275				UserInterfaceProvider::UserInterfaceProvider,
276			};
277
278			let Title = Params
279				.get(0)
280				.and_then(|V| V.get("title"))
281				.and_then(|T| T.as_str())
282				.map(|S| S.to_string());
283
284			let Options = SaveDialogOptionsDTO {
285				Base:CommonLibrary::UserInterface::DTO::DialogOptionsDTO::DialogOptionsDTO {
286					Title,
287					..Default::default()
288				},
289				..SaveDialogOptionsDTO::default()
290			};
291
292			match Service.environment.ShowSaveDialog(Some(Options)).await {
293				Ok(Some(Path)) => Ok(OkResponse(RequestId, &json!(format!("file://{}", Path.display())))),
294
295				Ok(None) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
296
297				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
298			}
299		},
300
301		"UserInterface.ShowInputBox" => {
302			use CommonLibrary::UserInterface::{
303				DTO::InputBoxOptionsDTO::InputBoxOptionsDTO,
304				UserInterfaceProvider::UserInterfaceProvider,
305			};
306
307			let Opts = Params.get(0);
308
309			let Options = InputBoxOptionsDTO {
310				Prompt:Opts
311					.and_then(|V| V.get("prompt"))
312					.and_then(|P| P.as_str())
313					.map(|S| S.to_string()),
314
315				PlaceHolder:Opts
316					.and_then(|V| V.get("placeHolder"))
317					.and_then(|P| P.as_str())
318					.map(|S| S.to_string()),
319
320				IsPassword:Some(Opts.and_then(|V| V.get("password")).and_then(|B| B.as_bool()).unwrap_or(false)),
321
322				Value:Opts
323					.and_then(|V| V.get("value"))
324					.and_then(|V| V.as_str())
325					.map(|S| S.to_string()),
326
327				Title:None,
328
329				IgnoreFocusOut:None,
330			};
331
332			match Service.environment.ShowInputBox(Some(Options)).await {
333				Ok(Some(Text)) => Ok(OkResponse(RequestId, &json!(Text))),
334
335				Ok(None) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
336
337				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
338			}
339		},
340
341		// ---- Native shell operations ----
342		"openExternal" => {
343			use tauri::Emitter;
344
345			let Url = Params
346				.as_str()
347				.or_else(|| Params.get("url").and_then(|V| V.as_str()))
348				.unwrap_or("")
349				.to_string();
350
351			// Emit to Sky - Sky uses Tauri shell plugin to open the URL
352			let _ = Service
353				.environment
354				.ApplicationHandle
355				.emit("sky://native/openExternal", json!({ "url": Url }));
356
357			Ok(OkResponse(RequestId, &json!({ "success": true })))
358		},
359
360		// ---- Window (Cocoon MountainGRPCClient format) ----
361		"showTextDocument" => {
362			use tauri::Emitter;
363
364			let Uri = Params
365				.get("uri")
366				.and_then(|V| V.get("value").or(Some(V)))
367				.and_then(|V| V.as_str())
368				.unwrap_or("")
369				.to_string();
370
371			let ViewColumn = Params.get("viewColumn").and_then(|V| V.as_i64()).map(|N| N + 2);
372
373			let PreserveFocus = Params.get("preserveFocus").and_then(|V| V.as_bool()).unwrap_or(false);
374
375			let _ = Service.environment.ApplicationHandle.emit(
376				"sky://editor/openDocument",
377				json!({ "uri": Uri, "viewColumn": ViewColumn, "preserveFocus": PreserveFocus }),
378			);
379
380			Ok(OkResponse(RequestId, &json!({ "success": true })))
381		},
382
383		"showInformation" => {
384			use CommonLibrary::UserInterface::{
385				DTO::MessageSeverity::MessageSeverity,
386				UserInterfaceProvider::UserInterfaceProvider,
387			};
388
389			let Message = Params.get("message").and_then(|V| V.as_str()).unwrap_or("").to_string();
390
391			let Items:Option<serde_json::Value> = Params
392				.get("items")
393				.cloned()
394				.filter(|V| V.is_array() && !V.as_array().unwrap().is_empty());
395
396			match Service.environment.ShowMessage(MessageSeverity::Info, Message, Items).await {
397				Ok(Some(Selected)) => Ok(OkResponse(RequestId, &json!({ "selectedItem": Selected }))),
398
399				Ok(None) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
400
401				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
402			}
403		},
404
405		"showWarning" => {
406			use CommonLibrary::UserInterface::{
407				DTO::MessageSeverity::MessageSeverity,
408				UserInterfaceProvider::UserInterfaceProvider,
409			};
410
411			let Message = Params.get("message").and_then(|V| V.as_str()).unwrap_or("").to_string();
412
413			let Items:Option<serde_json::Value> = Params
414				.get("items")
415				.cloned()
416				.filter(|V| V.is_array() && !V.as_array().unwrap().is_empty());
417
418			match Service.environment.ShowMessage(MessageSeverity::Warning, Message, Items).await {
419				Ok(Some(Selected)) => Ok(OkResponse(RequestId, &json!({ "selectedItem": Selected }))),
420
421				Ok(None) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
422
423				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
424			}
425		},
426
427		"showError" => {
428			use CommonLibrary::UserInterface::{
429				DTO::MessageSeverity::MessageSeverity,
430				UserInterfaceProvider::UserInterfaceProvider,
431			};
432
433			let Message = Params.get("message").and_then(|V| V.as_str()).unwrap_or("").to_string();
434
435			let Items:Option<serde_json::Value> = Params
436				.get("items")
437				.cloned()
438				.filter(|V| V.is_array() && !V.as_array().unwrap().is_empty());
439
440			match Service.environment.ShowMessage(MessageSeverity::Error, Message, Items).await {
441				Ok(Some(Selected)) => Ok(OkResponse(RequestId, &json!({ "selectedItem": Selected }))),
442
443				Ok(None) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
444
445				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
446			}
447		},
448
449		"createStatusBarItem" => {
450			use tauri::Emitter;
451
452			let Id = Params.get("id").and_then(|V| V.as_str()).unwrap_or("").to_string();
453
454			let Text = Params.get("text").and_then(|V| V.as_str()).unwrap_or("").to_string();
455
456			let Tooltip = Params.get("tooltip").and_then(|V| V.as_str()).unwrap_or("").to_string();
457
458			// Sky's `SetOrUpdateEntry` (`SkyBridge.ts:744`) listens on
459			// `sky://statusbar/set-entry` and `sky://statusbar/update`
460			// - both route through the same upsert. There is no
461			// `sky://statusbar/create` listener; emit the canonical
462			// `set-entry` channel so the entry materialises on first
463			// register.
464			let _ = Service.environment.ApplicationHandle.emit(
465				"sky://statusbar/set-entry",
466				json!({ "id": Id, "text": Text, "tooltip": Tooltip }),
467			);
468
469			Ok(OkResponse(RequestId, &json!({ "itemId": Id })))
470		},
471
472		"setStatusBarText" => {
473			use tauri::Emitter;
474
475			let ItemId = Params.get("itemId").and_then(|V| V.as_str()).unwrap_or("").to_string();
476
477			let Text = Params.get("text").and_then(|V| V.as_str()).unwrap_or("").to_string();
478
479			let _ = Service
480				.environment
481				.ApplicationHandle
482				.emit("sky://statusbar/update", json!({ "id": ItemId, "text": Text }));
483
484			Ok(OkResponse(RequestId, &json!({ "success": true })))
485		},
486
487		"createWebviewPanel" => {
488			use tauri::Emitter;
489
490			let ViewType = Params.get("viewType").and_then(|V| V.as_str()).unwrap_or("").to_string();
491
492			let Title = Params.get("title").and_then(|V| V.as_str()).unwrap_or("").to_string();
493
494			let Handle = std::time::SystemTime::now()
495				.duration_since(std::time::UNIX_EPOCH)
496				.map(|D| D.as_millis() as u64)
497				.unwrap_or(0);
498
499			let _ = Service.environment.ApplicationHandle.emit("sky://webview/create", json!({ "handle": Handle, "viewType": ViewType, "title": Title, "viewColumn": Params.get("viewColumn"), "preserveFocus": Params.get("preserveFocus").and_then(|V| V.as_bool()).unwrap_or(false) }));
500
501			Ok(OkResponse(RequestId, &json!({ "handle": Handle })))
502		},
503
504		"setWebviewHtml" => {
505			use tauri::Emitter;
506
507			let Handle = Params.get("handle").and_then(|V| V.as_u64()).unwrap_or(0);
508
509			let Html = Params.get("html").and_then(|V| V.as_str()).unwrap_or("").to_string();
510
511			// Canonical kebab-case channel; `sky://webview/setHtml` retired.
512			let _ = Service
513				.environment
514				.ApplicationHandle
515				.emit("sky://webview/set-html", json!({ "handle": Handle, "html": Html }));
516
517			Ok(OkResponse(RequestId, &json!({ "success": true })))
518		},
519
520		// ---- Workspace (Cocoon MountainGRPCClient format) ----
521		// `findFiles` / `findTextInFiles` are called by Cocoon's
522		// `workspace.findFiles()` / `workspace.findTextInFiles()`
523		// API shims. Delegate to the real trait implementations
524		// (`WorkspaceProvider::FindFilesInWorkspace`,
525		// `SearchProvider::TextSearch`) which use `ignore::WalkBuilder`
526		// + `grep-searcher` - respecting `.gitignore`, doing parallel
527		// walks, and producing properly-constructed `Url` results.
528		// Prior inline implementations used naive dir-walks, hidden-
529		// dot skipping, and `format!("file://{}", path)` URI
530		// construction that mangled non-ASCII paths.
531		"findFiles" => {
532			use CommonLibrary::Workspace::WorkspaceProvider::WorkspaceProvider;
533
534			let Include = Params
535				.get("pattern")
536				.cloned()
537				.or_else(|| Params.get("include").cloned())
538				.unwrap_or(serde_json::Value::String("**".into()));
539
540			let Exclude = Params.get("exclude").cloned().filter(|V| !V.is_null());
541
542			let MaxResults = Params.get("maxResults").and_then(|V| V.as_u64()).map(|N| N as usize);
543
544			let UseIgnoreFiles = Params.get("useIgnoreFiles").and_then(|V| V.as_bool()).unwrap_or(true);
545
546			let FollowSymlinks = Params.get("followSymlinks").and_then(|V| V.as_bool()).unwrap_or(false);
547
548			match Service
549				.environment
550				.FindFilesInWorkspace(Include, Exclude, MaxResults, UseIgnoreFiles, FollowSymlinks)
551				.await
552			{
553				Ok(Urls) => {
554					Ok(OkResponse(
555						RequestId,
556						&json!({ "uris": Urls.into_iter().map(|U| U.to_string()).collect::<Vec<_>>() }),
557					))
558				},
559
560				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("findFiles: {}", Error))),
561			}
562		},
563
564		"findTextInFiles" => {
565			use CommonLibrary::Search::SearchProvider::SearchProvider;
566
567			// VS Code's `workspace.findTextInFiles` takes a
568			// `TextSearchQuery` in field `pattern` (or passed flat
569			// at the top level). Accept both shapes.
570			let QueryValue = if Params.get("pattern").map(|V| V.is_object()).unwrap_or(false) {
571				Params.get("pattern").cloned().unwrap_or(serde_json::Value::Null)
572			} else if Params.get("pattern").map(|V| V.is_string()).unwrap_or(false) {
573				json!({
574					"pattern": Params.get("pattern").and_then(|V| V.as_str()).unwrap_or(""),
575					"isRegExp": Params.get("isRegExp").and_then(|V| V.as_bool()).unwrap_or(false),
576					"isCaseSensitive": Params.get("isCaseSensitive").and_then(|V| V.as_bool()).unwrap_or(false),
577					"isWordMatch": Params.get("isWordMatch").and_then(|V| V.as_bool()).unwrap_or(false),
578				})
579			} else {
580				Params.clone()
581			};
582
583			let OptionsValue = Params.get("options").cloned().unwrap_or(serde_json::Value::Null);
584
585			match Service.environment.TextSearch(QueryValue, OptionsValue).await {
586				Ok(Matches) => Ok(OkResponse(RequestId, &json!({ "matches": Matches }))),
587
588				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("findTextInFiles: {}", Error))),
589			}
590		},
591
592		"openDocument" => {
593			use tauri::Emitter;
594
595			let Uri = Params
596				.get("uri")
597				.and_then(|V| V.get("value").or(Some(V)))
598				.and_then(|V| V.as_str())
599				.unwrap_or("")
600				.to_string();
601
602			let ViewColumn = Params.get("viewColumn").and_then(|V| V.as_i64());
603
604			let _ = Service
605				.environment
606				.ApplicationHandle
607				.emit("sky://editor/openDocument", json!({ "uri": Uri, "viewColumn": ViewColumn }));
608
609			Ok(OkResponse(RequestId, &json!({ "success": true })))
610		},
611
612		"saveAll" => {
613			use tauri::Emitter;
614
615			let IncludeUntitled = Params.get("includeUntitled").and_then(|V| V.as_bool()).unwrap_or(false);
616
617			let _ = Service
618				.environment
619				.ApplicationHandle
620				.emit("sky://editor/saveAll", json!({ "includeUntitled": IncludeUntitled }));
621
622			Ok(OkResponse(RequestId, &json!({ "success": true })))
623		},
624
625		"applyEdit" => {
626			use tauri::Emitter;
627
628			let Uri = Params
629				.get("uri")
630				.and_then(|V| V.get("value").or(Some(V)))
631				.and_then(|V| V.as_str())
632				.unwrap_or("")
633				.to_string();
634
635			let Edits = Params.get("edits").cloned().unwrap_or(json!([]));
636
637			let _ = Service
638				.environment
639				.ApplicationHandle
640				.emit("sky://editor/applyEdits", json!({ "uri": Uri, "edits": Edits }));
641
642			Ok(OkResponse(RequestId, &json!({ "success": true })))
643		},
644
645		// ---- Secret Storage (Cocoon MountainGRPCClient format) ----
646		"getSecret" => {
647			use CommonLibrary::Secret::SecretProvider::SecretProvider;
648
649			let ExtensionId = Params.get("extensionId").and_then(|V| V.as_str()).unwrap_or("").to_string();
650
651			let Key = Params.get("key").and_then(|V| V.as_str()).unwrap_or("").to_string();
652
653			match Service.environment.GetSecret(ExtensionId, Key).await {
654				Ok(Some(Value)) => Ok(OkResponse(RequestId, &json!({ "value": Value }))),
655
656				Ok(None) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
657
658				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
659			}
660		},
661
662		"storeSecret" => {
663			use CommonLibrary::Secret::SecretProvider::SecretProvider;
664
665			let ExtensionId = Params.get("extensionId").and_then(|V| V.as_str()).unwrap_or("").to_string();
666
667			let Key = Params.get("key").and_then(|V| V.as_str()).unwrap_or("").to_string();
668
669			let Value = Params.get("value").and_then(|V| V.as_str()).unwrap_or("").to_string();
670
671			match Service.environment.StoreSecret(ExtensionId, Key, Value).await {
672				Ok(()) => Ok(OkResponse(RequestId, &json!({ "success": true }))),
673
674				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
675			}
676		},
677
678		"deleteSecret" => {
679			use CommonLibrary::Secret::SecretProvider::SecretProvider;
680
681			let ExtensionId = Params.get("extensionId").and_then(|V| V.as_str()).unwrap_or("").to_string();
682
683			let Key = Params.get("key").and_then(|V| V.as_str()).unwrap_or("").to_string();
684
685			match Service.environment.DeleteSecret(ExtensionId, Key).await {
686				Ok(()) => Ok(OkResponse(RequestId, &json!({ "success": true }))),
687
688				Err(Error) => Ok(ErrResponse(RequestId, -32000, Error.to_string())),
689			}
690		},
691
692		// ---- FS aliases (Cocoon MountainGRPCClient uses different key names) ----
693		"readFile" => {
694			let Uri = Params
695				.get("uri")
696				.and_then(|V| V.as_str())
697				.or_else(|| Params.as_str())
698				.unwrap_or("")
699				.replace("file://", "");
700
701			match tokio::fs::read(&Uri).await {
702				Ok(Content) => Ok(OkResponse(RequestId, &Content)),
703
704				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("readFile: {}", Error))),
705			}
706		},
707
708		"writeFile" => {
709			let Uri = Params.get("uri").and_then(|V| V.as_str()).unwrap_or("").replace("file://", "");
710
711			let Content:Vec<u8> = Params
712				.get("content")
713				.and_then(|V| V.as_array())
714				.map(|A| A.iter().filter_map(|B| B.as_u64().map(|N| N as u8)).collect())
715				.unwrap_or_default();
716
717			match tokio::fs::write(&Uri, &Content).await {
718				Ok(()) => Ok(OkResponse(RequestId, &serde_json::Value::Null)),
719
720				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("writeFile: {}", Error))),
721			}
722		},
723
724		"stat" => {
725			let Uri = Params
726				.get("uri")
727				.and_then(|V| V.as_str())
728				.or_else(|| Params.as_str())
729				.unwrap_or("")
730				.replace("file://", "");
731
732			match tokio::fs::metadata(&Uri).await {
733				Ok(Meta) => {
734					let Mtime = Meta
735						.modified()
736						.ok()
737						.and_then(|T| T.duration_since(UNIX_EPOCH).ok())
738						.map(|D| D.as_millis() as u64)
739						.unwrap_or(0);
740
741					Ok(OkResponse(
742						RequestId,
743						&json!({ "type": if Meta.is_dir() { 2 } else { 1 }, "is_file": Meta.is_file(), "is_directory": Meta.is_dir(), "size": Meta.len(), "mtime": Mtime }),
744					))
745				},
746
747				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("stat: {}", Error))),
748			}
749		},
750
751		"readdir" => {
752			let Uri = Params
753				.get("uri")
754				.and_then(|V| V.as_str())
755				.or_else(|| Params.as_str())
756				.unwrap_or("")
757				.replace("file://", "");
758
759			match tokio::fs::read_dir(&Uri).await {
760				Ok(mut Entries) => {
761					let mut Names:Vec<String> = Vec::new();
762
763					while let Ok(Some(Entry)) = Entries.next_entry().await {
764						if let Some(Name) = Entry.file_name().to_str() {
765							Names.push(Name.to_string());
766						}
767					}
768
769					Ok(OkResponse(RequestId, &Names))
770				},
771
772				Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("readdir: {}", Error))),
773			}
774		},
775
776		// ---- Call Hierarchy / Type Hierarchy (T1.5 Approach A) ----
777		// These method names come from Cocoon's language provider when the
778		// user triggers F12 / Go to References / Call Hierarchy. They use
779		// the generic JSON request channel instead of typed proto methods
780		// because `PrepareCallHierarchy` was never added to Vine.proto.
781		// Params shape: `{ uri, position: { line, character } }`.
782		"$provideCallHierarchyItems" | "prepareCallHierarchy" => {
783			let URI_Raw = Params.get("uri").and_then(|V| V.as_str()).unwrap_or("");
784
785			let Line = Params
786				.get("position")
787				.and_then(|P| P.get("line"))
788				.and_then(|V| V.as_u64())
789				.unwrap_or(0);
790
791			let Char = Params
792				.get("position")
793				.and_then(|P| P.get("character"))
794				.and_then(|V| V.as_u64())
795				.unwrap_or(0);
796
797			match Url::parse(URI_Raw) {
798				Ok(DocURI) => {
799					let Pos = PositionDTO { LineNumber:Line as u32, Column:Char as u32 };
800
801					match Service.environment.PrepareCallHierarchy(DocURI, Pos).await {
802						Ok(Result) => Ok(OkResponse(RequestId, &Result)),
803
804						Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("prepareCallHierarchy: {}", Error))),
805					}
806				},
807
808				Err(_) => Ok(OkResponse(RequestId, &serde_json::Value::Array(Vec::new()))),
809			}
810		},
811
812		"$provideTypeHierarchyItems" | "prepareTypeHierarchy" => {
813			let URI_Raw = Params.get("uri").and_then(|V| V.as_str()).unwrap_or("");
814
815			let Line = Params
816				.get("position")
817				.and_then(|P| P.get("line"))
818				.and_then(|V| V.as_u64())
819				.unwrap_or(0);
820
821			let Char = Params
822				.get("position")
823				.and_then(|P| P.get("character"))
824				.and_then(|V| V.as_u64())
825				.unwrap_or(0);
826
827			match Url::parse(URI_Raw) {
828				Ok(DocURI) => {
829					let Pos = PositionDTO { LineNumber:Line as u32, Column:Char as u32 };
830
831					match Service.environment.PrepareTypeHierarchy(DocURI, Pos).await {
832						Ok(Result) => Ok(OkResponse(RequestId, &Result)),
833
834						Err(Error) => Ok(ErrResponse(RequestId, -32000, format!("prepareTypeHierarchy: {}", Error))),
835					}
836				},
837
838				Err(_) => Ok(OkResponse(RequestId, &serde_json::Value::Array(Vec::new()))),
839			}
840		},
841
842		// ---- Unknown ----
843		_ => {
844			dev_log!("cocoon", "warn: [CocoonService] Unknown generic method: {}", Req.method);
845
846			Ok(ErrResponse(RequestId, -32601, format!("Method '{}' not found", Req.method)))
847		},
848	}
849}