Skip to main content

Mountain/IPC/WindServiceHandlers/NativeDialog/
ShowOpenDialog.rs

1#![allow(non_snake_case)]
2//! `nativeHost:showOpenDialog` handler. Wires VS Code's
3//! `nativeHostService.showOpenDialog(options)` contract to Tauri's
4//! dialog plugin.
5//!
6//! Contract:
7//!   - `properties: ["openDirectory" | "openFile" | "multiSelections" |
8//!     "createDirectory" | "showHiddenFiles"]`
9//!   - `filters: [{ name, extensions: ["vsix", …] }, …]`
10//!   - `title`, `buttonLabel`, `defaultPath`
11//!   - returns `{ canceled: bool, filePaths: string[] }`.
12//!
13//! The "Install from VSIX…" flow relies on `filters` to narrow the picker
14//! to `.vsix` and on `openFile + multiSelections` so the user can pick
15//! multiple archives at once.
16
17use serde_json::{Value, json};
18use tauri::AppHandle;
19
20use crate::{IPC::WindServiceHandlers::NativeDialog::ParseDialogFilters::ParseDialogFilters, dev_log};
21
22pub async fn ShowOpenDialog(ApplicationHandle:AppHandle, Args:Vec<Value>) -> Result<Value, String> {
23	use tauri_plugin_dialog::DialogExt;
24
25	dev_log!("folder", "showOpenDialog: {:?}", Args);
26
27	// Electron passes `(windowId, options)`; `options` is always the last
28	// element regardless of how the renderer was invoked. Searching by
29	// shape (`first object with a 'properties' or 'filters' field`) keeps
30	// us robust against VS Code versions that pass an extra prefix arg.
31	let Options = Args.iter().rev().find(|V| V.is_object()).cloned().unwrap_or(Value::Null);
32
33	let Properties:Vec<String> = Options
34		.get("properties")
35		.and_then(Value::as_array)
36		.map(|Array| Array.iter().filter_map(|V| V.as_str().map(str::to_string)).collect())
37		.unwrap_or_default();
38
39	let IsFolder = Properties.iter().any(|P| P == "openDirectory");
40
41	let IsMultiple = Properties.iter().any(|P| P == "multiSelections");
42
43	let Title = Options
44		.get("title")
45		.and_then(Value::as_str)
46		.unwrap_or(if IsFolder { "Open Folder" } else { "Open File" })
47		.to_string();
48
49	let DefaultPath = Options.get("defaultPath").and_then(Value::as_str).map(str::to_string);
50
51	let Filters = ParseDialogFilters(&Options);
52
53	let Handle = ApplicationHandle.clone();
54
55	let FiltersForThread = Filters.clone();
56
57	let Selected = tokio::task::spawn_blocking(move || -> Vec<String> {
58		let mut Builder = Handle.dialog().file().set_title(&Title);
59		if let Some(Path) = DefaultPath.as_deref() {
60			Builder = Builder.set_directory(Path);
61		}
62		// Apply filters only for file pickers - Tauri returns an error on
63		// folder pickers if filters are set on some platforms.
64		if !IsFolder {
65			for Filter in &FiltersForThread {
66				let ExtRefs:Vec<&str> = Filter.Extensions.iter().map(String::as_str).collect();
67				Builder = Builder.add_filter(&Filter.Name, &ExtRefs);
68			}
69		}
70		if IsFolder {
71			if IsMultiple {
72				Builder
73					.blocking_pick_folders()
74					.unwrap_or_default()
75					.into_iter()
76					.map(|P| P.to_string())
77					.collect()
78			} else {
79				Builder.blocking_pick_folder().map(|P| vec![P.to_string()]).unwrap_or_default()
80			}
81		} else if IsMultiple {
82			Builder
83				.blocking_pick_files()
84				.unwrap_or_default()
85				.into_iter()
86				.map(|P| P.to_string())
87				.collect()
88		} else {
89			Builder.blocking_pick_file().map(|P| vec![P.to_string()]).unwrap_or_default()
90		}
91	})
92	.await
93	.map_err(|Error| format!("showOpenDialog join error: {}", Error))?;
94
95	if Selected.is_empty() {
96		dev_log!("folder", "showOpenDialog cancelled by user");
97
98		Ok(json!({ "canceled": true, "filePaths": [] }))
99	} else {
100		dev_log!(
101			"folder",
102			"showOpenDialog selected {} path(s) (folder={}, multi={}, filters={})",
103			Selected.len(),
104			IsFolder,
105			IsMultiple,
106			Filters.len()
107		);
108
109		Ok(json!({ "canceled": false, "filePaths": Selected }))
110	}
111}