Fix remote mode multi-file selection via Show Local dialog (#300408)

* Initial plan

* Fix remote mode multi-file selection via Show Local dialog

Change ISimpleFileDialog.showOpenDialog to return URI[] instead of single URI.
When 'Show Local' is clicked in remote file dialog, pass through all selected
files from the native dialog instead of only taking the first result.

Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
This commit is contained in:
Copilot
2026-03-11 11:20:56 +00:00
committed by GitHub
parent 6a97d569af
commit 84a04d1761
3 changed files with 45 additions and 30 deletions

View File

@@ -229,7 +229,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
const title = nls.localize('openFileOrFolder.title', 'Open File or Folder');
const availableFileSystems = this.addFileSchemaIfNeeded(schema);
const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
const uris = await this.pickResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
const uri = uris?.[0];
if (uri) {
const stat = await this.fileService.stat(uri);
@@ -251,7 +252,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
const title = nls.localize('openFile.title', 'Open File');
const availableFileSystems = this.addFileSchemaIfNeeded(schema);
const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
const uris = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
const uri = uris?.[0];
if (uri) {
this.addFileToRecentlyOpened(uri);
@@ -271,7 +273,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
const title = nls.localize('openFolder.title', 'Open Folder');
const availableFileSystems = this.addFileSchemaIfNeeded(schema, true);
const uri = await this.pickResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
const uris = await this.pickResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
const uri = uris?.[0];
if (uri) {
return this.hostService.openWindow([{ folderUri: uri }], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority });
}
@@ -282,7 +285,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
const filters: FileFilter[] = [{ name: nls.localize('filterName.workspace', 'Workspace'), extensions: [WORKSPACE_EXTENSION] }];
const availableFileSystems = this.addFileSchemaIfNeeded(schema, true);
const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems });
const uris = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems });
const uri = uris?.[0];
if (uri) {
return this.hostService.openWindow([{ workspaceUri: uri }], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority });
}
@@ -316,16 +320,14 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
options.availableFileSystems = this.addFileSchemaIfNeeded(schema, options.canSelectFolders);
}
const uri = await this.pickResource(options);
return uri ? [uri] : undefined;
return this.pickResource(options);
}
protected getSimpleFileDialog(): ISimpleFileDialog {
return this.instantiationService.createInstance(SimpleFileDialog);
}
private pickResource(options: IOpenDialogOptions): Promise<URI | undefined> {
private pickResource(options: IOpenDialogOptions): Promise<URI[] | undefined> {
return this.getSimpleFileDialog().showOpenDialog(options);
}

View File

@@ -105,7 +105,7 @@ enum UpdateResult {
export const RemoteFileDialogContext = new RawContextKey<boolean>('remoteFileDialogVisible', false);
export interface ISimpleFileDialog extends IDisposable {
showOpenDialog(options: IOpenDialogOptions): Promise<URI | undefined>;
showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined>;
showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;
}
@@ -189,7 +189,7 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog {
return this.filePickBox.busy;
}
public async showOpenDialog(options: IOpenDialogOptions = {}): Promise<URI | undefined> {
public async showOpenDialog(options: IOpenDialogOptions = {}): Promise<URI[] | undefined> {
this.scheme = this.getScheme(options.availableFileSystems, options.defaultUri);
this.userHome = await this.getUserHome();
this.trueHome = await this.getUserHome(true);
@@ -198,7 +198,11 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog {
return Promise.resolve(undefined);
}
this.options = newOptions;
return this.pickResource();
const result = await this.pickResource();
if (Array.isArray(result)) {
return result;
}
return result ? [result] : undefined;
}
public async showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
@@ -215,8 +219,8 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog {
this.options.canSelectFiles = true;
return new Promise<URI | undefined>((resolve) => {
this.pickResource(true).then(folderUri => {
resolve(folderUri);
this.pickResource(true).then(result => {
resolve(Array.isArray(result) ? result[0] : result);
});
});
}
@@ -281,7 +285,14 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog {
: this.fileDialogService.preferredHome(this.scheme);
}
private async pickResource(isSave: boolean = false): Promise<URI | undefined> {
private normalizeUri(uri: URI): URI {
uri = resources.addTrailingPathSeparator(uri, this.separator); // Ensures that c: is c:/ since this comes from user input and can be incorrect.
// To be consistent, we should never have a trailing path separator on directories (or anything else). Will not remove from c:/.
uri = resources.removeTrailingPathSeparator(uri);
return uri;
}
private async pickResource(isSave: boolean = false): Promise<URI[] | URI | undefined> {
this.allowFolderSelection = !!this.options.canSelectFolders;
this.allowFileSelection = !!this.options.canSelectFiles;
this.separator = this.labelService.getSeparator(this.scheme, this.remoteAuthority);
@@ -302,7 +313,7 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog {
}
}
return new Promise<URI | undefined>((resolve) => {
return new Promise<URI[] | URI | undefined>((resolve) => {
this.filePickBox = this._register(this.quickInputService.createQuickPick<FileQuickPickItem>());
this.busy = true;
this.filePickBox.matchOnLabel = false;
@@ -345,13 +356,15 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog {
this.filePickBox.value = this.pathFromUri(this.currentFolder, true);
this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length];
const doResolve = (uri: URI | undefined) => {
if (uri) {
uri = resources.addTrailingPathSeparator(uri, this.separator); // Ensures that c: is c:/ since this comes from user input and can be incorrect.
// To be consistent, we should never have a trailing path separator on directories (or anything else). Will not remove from c:/.
uri = resources.removeTrailingPathSeparator(uri);
const doResolve = (uriOrUris: URI | URI[] | undefined) => {
if (uriOrUris) {
if (Array.isArray(uriOrUris)) {
uriOrUris = uriOrUris.map(uri => this.normalizeUri(uri));
} else {
uriOrUris = this.normalizeUri(uriOrUris);
}
}
resolve(uri);
resolve(uriOrUris);
this.contextKey.set(false);
this.dispose();
};
@@ -373,7 +386,7 @@ export class SimpleFileDialog extends Disposable implements ISimpleFileDialog {
});
} else {
return this.fileDialogService.showOpenDialog(this.options).then(result => {
doResolve(result ? result[0] : undefined);
doResolve(result);
});
}
}));

View File

@@ -89,10 +89,10 @@ suite('FileDialogService', function () {
test('Local - open/save workspaces availableFilesystems', async function () {
class TestSimpleFileDialog implements ISimpleFileDialog {
async showOpenDialog(options: IOpenDialogOptions): Promise<URI | undefined> {
async showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined> {
assert.strictEqual(options.availableFileSystems?.length, 1);
assert.strictEqual(options.availableFileSystems[0], Schemas.file);
return testFile;
return [testFile];
}
async showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
assert.strictEqual(options.availableFileSystems?.length, 1);
@@ -111,10 +111,10 @@ suite('FileDialogService', function () {
test('Virtual - open/save workspaces availableFilesystems', async function () {
class TestSimpleFileDialog {
async showOpenDialog(options: IOpenDialogOptions): Promise<URI | undefined> {
async showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined> {
assert.strictEqual(options.availableFileSystems?.length, 1);
assert.strictEqual(options.availableFileSystems[0], Schemas.file);
return testFile;
return [testFile];
}
async showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
assert.strictEqual(options.availableFileSystems?.length, 1);
@@ -137,11 +137,11 @@ suite('FileDialogService', function () {
test('Remote - open/save workspaces availableFilesystems', async function () {
class TestSimpleFileDialog implements ISimpleFileDialog {
async showOpenDialog(options: IOpenDialogOptions): Promise<URI | undefined> {
async showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined> {
assert.strictEqual(options.availableFileSystems?.length, 2);
assert.strictEqual(options.availableFileSystems[0], Schemas.vscodeRemote);
assert.strictEqual(options.availableFileSystems[1], Schemas.file);
return testFile;
return [testFile];
}
async showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
assert.strictEqual(options.availableFileSystems?.length, 2);
@@ -170,8 +170,8 @@ suite('FileDialogService', function () {
test('Remote - filters default files/folders to RA (#195938)', async function () {
class TestSimpleFileDialog implements ISimpleFileDialog {
async showOpenDialog(): Promise<URI | undefined> {
return testFile;
async showOpenDialog(): Promise<URI[] | undefined> {
return [testFile];
}
async showSaveDialog(): Promise<URI | undefined> {
return testFile;