Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

drag&drop a VSIX file on the extensions viewlet #94152

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 34 additions & 31 deletions src/vs/workbench/contrib/extensions/browser/extensionsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1537,11 +1537,11 @@ export class ShowInstalledExtensionsAction extends Action {
super(id, label, undefined, true);
}

run(): Promise<void> {
run(refresh?: boolean): Promise<void> {
return this.viewletService.openViewlet(VIEWLET_ID, true)
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
.then(viewlet => {
viewlet.search('@installed ');
viewlet.search('@installed ', refresh);
viewlet.focus();
});
}
Expand Down Expand Up @@ -2895,37 +2895,40 @@ export class InstallVSIXAction extends Action {
super(id, label, 'extension-action install-vsix', true);
}

run(): Promise<any> {
return Promise.resolve(this.fileDialogService.showOpenDialog({
title: localize('installFromVSIX', "Install from VSIX"),
filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }],
canSelectFiles: true,
openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install"))
})).then(result => {
if (!result) {
return Promise.resolve();
async run(vsixPaths?: URI[]): Promise<void> {
if (!vsixPaths) {
vsixPaths = await this.fileDialogService.showOpenDialog({
title: localize('installFromVSIX', "Install from VSIX"),
filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }],
canSelectFiles: true,
openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install"))
});

if (!vsixPaths) {
return;
}
}

return Promise.all(result.map(vsix => this.extensionsWorkbenchService.install(vsix)))
.then(extensions => {
for (const extension of extensions) {
const requireReload = !(extension.local && this.extensionService.canAddExtension(toExtensionDescription(extension.local)));
const message = requireReload ? localize('InstallVSIXAction.successReload', "Please reload Visual Studio Code to complete installing the extension {0}.", extension.displayName || extension.name)
: localize('InstallVSIXAction.success', "Completed installing the extension {0}.", extension.displayName || extension.name);
const actions = requireReload ? [{
label: localize('InstallVSIXAction.reloadNow', "Reload Now"),
run: () => this.hostService.reload()
}] : [];
this.notificationService.prompt(
Severity.Info,
message,
actions,
{ sticky: true }
);
}
return this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run();
});
});
// Install extension(s), display notification(s), display @installed extensions
await Promise.all(vsixPaths.map(async (vsix) => await this.extensionsWorkbenchService.install(vsix)))
.then(async (extensions) => {
for (const extension of extensions) {
const requireReload = !(extension.local && this.extensionService.canAddExtension(toExtensionDescription(extension.local)));
const message = requireReload ? localize('InstallVSIXAction.successReload', "Please reload Visual Studio Code to complete installing the extension {0}.", extension.displayName || extension.name)
: localize('InstallVSIXAction.success', "Completed installing the extension {0}.", extension.displayName || extension.name);
const actions = requireReload ? [{
label: localize('InstallVSIXAction.reloadNow', "Reload Now"),
run: () => this.hostService.reload()
}] : [];
this.notificationService.prompt(
Severity.Info,
message,
actions,
{ sticky: true }
);
}
await this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run(true);
});
}
}

Expand Down
58 changes: 56 additions & 2 deletions src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { IAction, Action } from 'vs/base/common/actions';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { IViewlet } from 'vs/workbench/common/viewlet';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom';
import { append, $, addClass, toggleClass, Dimension, hide, show } from 'vs/base/browser/dom';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
Expand Down Expand Up @@ -57,6 +57,9 @@ import { ILabelService } from 'vs/platform/label/common/label';
import { MementoObject } from 'vs/workbench/common/memento';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
import { DragAndDropObserver } from 'vs/workbench/browser/dnd';
import { URI } from 'vs/base/common/uri';
import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';

const NonEmptyWorkspaceContext = new RawContextKey<boolean>('nonEmptyWorkspace', false);
const DefaultViewsContext = new RawContextKey<boolean>('defaultExtensionViews', true);
Expand Down Expand Up @@ -396,8 +399,12 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
addClass(parent, 'extensions-viewlet');
this.root = parent;

const header = append(this.root, $('.header'));
const overlay = append(this.root, $('.overlay'));
const overlayBackgroundColor = this.getColor(SIDE_BAR_DRAG_AND_DROP_BACKGROUND) ?? '';
overlay.style.backgroundColor = overlayBackgroundColor;
hide(overlay);

const header = append(this.root, $('.header'));
const placeholder = localize('searchExtensions', "Search Extensions in Marketplace");
const searchValue = this.searchViewletState['query.value'] ? this.searchViewletState['query.value'] : '';

Expand Down Expand Up @@ -431,6 +438,44 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
}
}));

// Register DragAndDrop support
this._register(new DragAndDropObserver(this.root, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Shall we have drop overlay only on extensions views excluding the search box ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on my conversation with @joaomoreno, we wanted to keep things simple and that is why we are set up the drop target/drop overlay to be the shown on the whole extensions view. If you feel strongly about this, or there is a prior pattern not to show the drop overlay over the search box, I am happy to update it.

onDragEnd: (e: DragEvent) => undefined,
onDragEnter: (e: DragEvent) => {
if (this.isSupportedDragElement(e)) {
show(overlay);
}
},
onDragLeave: (e: DragEvent) => {
if (this.isSupportedDragElement(e)) {
hide(overlay);
}
},
onDragOver: (e: DragEvent) => {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = this.isSupportedDragElement(e) ? 'copy' : 'none';
}
},
onDrop: (e: DragEvent) => {
if (this.isSupportedDragElement(e)) {
hide(overlay);

if (e.dataTransfer && e.dataTransfer.files.length > 0) {
let vsixPaths: URI[] = [];
for (let index = 0; index < e.dataTransfer.files.length; index++) {
const path = e.dataTransfer.files.item(index)!.path;
if (path.indexOf('.vsix') !== -1) {
vsixPaths.push(URI.parse(path));
}
}

// Install the extension(s)
this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL).run(vsixPaths);
}
}
},
}));

super.create(append(this.root, $('.extensions')));
}

Expand Down Expand Up @@ -617,6 +662,15 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE

this.notificationService.error(err);
}

private isSupportedDragElement(e: DragEvent): boolean {
if (e.dataTransfer) {
const typesLowerCase = e.dataTransfer.types.map(t => t.toLocaleLowerCase());
return typesLowerCase.indexOf('files') !== -1;
lszomoru marked this conversation as resolved.
Show resolved Hide resolved
}

return false;
}
}

export class StatusUpdater extends Disposable implements IWorkbenchContribution {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@
*--------------------------------------------------------------------------------------------*/

.extensions-viewlet {
position: relative;
height: 100%;
}

.extensions-viewlet > .overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
}

.extensions-viewlet > .header {
height: 41px;
box-sizing: border-box;
Expand Down