Skip to content

Commit 3bcce20

Browse files
1 parent e632bde commit 3bcce20

2 files changed

Lines changed: 103 additions & 15 deletions

File tree

Source/IPC/WindServiceHandler/NativeHost.rs

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,24 +111,74 @@ pub async fn handle_native_pick_folder(AppHandle:AppHandle, _Args:Vec<Value>) ->
111111
Ok(Value::Null)
112112
}
113113

114+
/// Electron-style filter passed through `showOpenDialog({ filters: [...] })`.
115+
/// Shape: `{ name: "VSIX Extensions", extensions: ["vsix"] }`. The tauri
116+
/// dialog plugin's `add_filter(name, &[&str])` expects the same pair.
117+
#[derive(Debug, Clone)]
118+
struct DialogFilter {
119+
Name:String,
120+
Extensions:Vec<String>,
121+
}
122+
123+
/// Parse `options.filters` into a vector of `DialogFilter`. Unknown / malformed
124+
/// entries are silently skipped rather than failing the whole dialog open -
125+
/// the user still gets the picker, just without the filter hint.
126+
fn ParseDialogFilters(Options:&Value) -> Vec<DialogFilter> {
127+
Options
128+
.get("filters")
129+
.and_then(Value::as_array)
130+
.map(|Array| {
131+
Array
132+
.iter()
133+
.filter_map(|Entry| {
134+
let Name =
135+
Entry.get("name").and_then(Value::as_str).unwrap_or("Files").to_string();
136+
let Extensions:Vec<String> = Entry
137+
.get("extensions")
138+
.and_then(Value::as_array)
139+
.map(|List| {
140+
List.iter().filter_map(|V| V.as_str().map(str::to_string)).collect()
141+
})
142+
.unwrap_or_default();
143+
if Extensions.is_empty() { None } else { Some(DialogFilter { Name, Extensions }) }
144+
})
145+
.collect()
146+
})
147+
.unwrap_or_default()
148+
}
149+
114150
/// Show open dialog with file/folder picker.
115151
///
116-
/// VS Code calls this via `nativeHostService.showOpenDialog(options)` with
117-
/// Electron-style `properties: ["openDirectory" | "openFile" |
118-
/// "multiSelections" | "createDirectory"]`. The expected return shape is
119-
/// `{ canceled: bool, filePaths: string[] }`.
152+
/// VS Code calls this via `nativeHostService.showOpenDialog(options)` and
153+
/// expects the Electron contract:
154+
///
155+
/// - `properties: ["openDirectory" | "openFile" | "multiSelections" |
156+
/// "createDirectory" | "showHiddenFiles"]`
157+
/// - `filters: [{ name, extensions: ["vsix", …] }, …]`
158+
/// - `title`, `buttonLabel`, `defaultPath`
159+
/// - returns `{ canceled: bool, filePaths: string[] }`.
120160
///
121-
/// This handler was previously a stub returning "canceled: true" - that's
122-
/// why clicking the Explorer's "Open Folder" button (which goes through
123-
/// this method, not through `pickFolderAndOpen`) silently did nothing. We
124-
/// now drive the Tauri dialog plugin directly, honouring the `properties`
125-
/// flags so folder-mode is picked when requested.
161+
/// The VSIX install flow (`Install from VSIX…`) relies on `filters` to narrow
162+
/// the picker to `.vsix` and on `openFile + multiSelections` so the user can
163+
/// pick several archives at once. Without either, the dialog either never
164+
/// opens (old stub) or opens unfiltered - both produced the "nothing happens"
165+
/// symptom in the field. This handler drives the Tauri dialog plugin
166+
/// end-to-end: every option in the VS Code contract maps to a builder call.
126167
pub async fn handle_native_show_open_dialog(AppHandle:AppHandle, Args:Vec<Value>) -> Result<Value, String> {
127168
use tauri_plugin_dialog::DialogExt;
128169

129170
dev_log!("folder", "showOpenDialog: {:?}", Args);
130171

131-
let Options = Args.first().cloned().unwrap_or(Value::Null);
172+
// Electron passes `(windowId, options)`; `options` is always the last
173+
// element regardless of how the renderer was invoked. Searching by shape
174+
// (`first object with a "properties" or "filters" field`) keeps us robust
175+
// against VS Code versions that pass an extra prefix arg.
176+
let Options = Args
177+
.iter()
178+
.rev()
179+
.find(|V| V.is_object())
180+
.cloned()
181+
.unwrap_or(Value::Null);
132182
let Properties:Vec<String> = Options
133183
.get("properties")
134184
.and_then(Value::as_array)
@@ -142,13 +192,26 @@ pub async fn handle_native_show_open_dialog(AppHandle:AppHandle, Args:Vec<Value>
142192
.unwrap_or(if IsFolder { "Open Folder" } else { "Open File" })
143193
.to_string();
144194
let DefaultPath = Options.get("defaultPath").and_then(Value::as_str).map(str::to_string);
195+
// `filters` only affects file pickers; Tauri's folder picker ignores them.
196+
// Parsing unconditionally keeps the code branchless - the unused vector
197+
// costs nothing and we avoid an extra branch in the hot path.
198+
let Filters = ParseDialogFilters(&Options);
145199

146200
let Handle = AppHandle.clone();
201+
let FiltersForThread = Filters.clone();
147202
let Selected = tokio::task::spawn_blocking(move || -> Vec<String> {
148203
let mut Builder = Handle.dialog().file().set_title(&Title);
149204
if let Some(Path) = DefaultPath.as_deref() {
150205
Builder = Builder.set_directory(Path);
151206
}
207+
// Apply filters only for file pickers - Tauri returns an error on
208+
// folder pickers if filters are set on some platforms.
209+
if !IsFolder {
210+
for Filter in &FiltersForThread {
211+
let ExtRefs:Vec<&str> = Filter.Extensions.iter().map(String::as_str).collect();
212+
Builder = Builder.add_filter(&Filter.Name, &ExtRefs);
213+
}
214+
}
152215
if IsFolder {
153216
if IsMultiple {
154217
Builder
@@ -178,7 +241,14 @@ pub async fn handle_native_show_open_dialog(AppHandle:AppHandle, Args:Vec<Value>
178241
dev_log!("folder", "showOpenDialog cancelled by user");
179242
Ok(json!({ "canceled": true, "filePaths": [] }))
180243
} else {
181-
dev_log!("folder", "showOpenDialog selected {} path(s)", Selected.len());
244+
dev_log!(
245+
"folder",
246+
"showOpenDialog selected {} path(s) (folder={}, multi={}, filters={})",
247+
Selected.len(),
248+
IsFolder,
249+
IsMultiple,
250+
Filters.len()
251+
);
182252
Ok(json!({ "canceled": false, "filePaths": Selected }))
183253
}
184254
}

Source/IPC/WindServiceHandlers/NativeHost.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,11 +266,29 @@ pub async fn handle_native_pick_folder(app_handle:AppHandle, _args:Vec<Value>) -
266266
Ok(Value::Null)
267267
}
268268

269-
/// Show open dialog with file/folder picker
269+
/// Show open dialog with file/folder picker.
270+
///
271+
/// VS Code routes `IFileDialogService.showOpenDialog` (Install-from-VSIX,
272+
/// Open File, Open Folder, Open Workspace, Choose Restore Location, etc.)
273+
/// through `nativeHost.showOpenDialog`. The handler must:
274+
///
275+
/// 1. Actually open the OS dialog via `tauri_plugin_dialog`.
276+
/// 2. Respect Electron-style `properties` flags (`openDirectory`,
277+
/// `openFile`, `multiSelections`, `createDirectory`) so folder vs file
278+
/// vs multi-select modes match the caller's intent.
279+
/// 3. Honour `filters` (the VSIX action sets `extensions: ["vsix"]` so
280+
/// the picker narrows to .vsix files).
281+
/// 4. Honour `title`, `buttonLabel`, `defaultPath`.
282+
/// 5. Return `{ canceled: bool, filePaths: string[] }`.
283+
///
284+
/// A single canonical implementation lives in the singular
285+
/// `WindServiceHandler::NativeHost` module - this shim delegates so the
286+
/// legacy dispatcher's `NativeHost::*` re-export surface doesn't drift from
287+
/// the new one. Previously this was a `canceled:true` stub, which is the
288+
/// precise reason "Install from VSIX…" did nothing: VS Code interpreted
289+
/// the stubbed cancel as "user dismissed the picker" and stopped.
270290
pub async fn handle_native_show_open_dialog(app_handle:AppHandle, args:Vec<Value>) -> Result<Value, String> {
271-
dev_log!("folder", "showOpenDialog: {:?}", args);
272-
// Return canceled for now - real dialog integration needs tauri_plugin_dialog
273-
Ok(json!({ "canceled": true, "filePaths": [] }))
291+
crate::IPC::WindServiceHandler::NativeHost::handle_native_show_open_dialog(app_handle, args).await
274292
}
275293

276294
/// Get OS properties - cross-platform (macOS, Windows, Linux)

0 commit comments

Comments
 (0)