Skip to main content

Mountain/RPC/CocoonService/GenericRequest/
Dispatcher.rs

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