Skip to content

Commit

Permalink
Move to file/newFile... contribution point
Browse files Browse the repository at this point in the history
Close #128136
  • Loading branch information
Jackson Kearl committed Jul 8, 2021
1 parent 816caf0 commit 9006cc8
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 303 deletions.
1 change: 1 addition & 0 deletions src/vs/platform/actions/common/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export class MenuId {
static readonly TerminalInlineTabContext = new MenuId('TerminalInlineTabContext');
static readonly WebviewContext = new MenuId('WebviewContext');
static readonly InlineCompletionsActions = new MenuId('InlineCompletionsActions');
static readonly NewFile = new MenuId('NewFile');

readonly id: number;
readonly _debugName: string;
Expand Down
6 changes: 6 additions & 0 deletions src/vs/workbench/api/common/menusExtensionPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ const apiMenus: IAPIMenu[] = [
id: MenuId.TunnelPortInline,
description: localize('view.tunnelPortInline', "The Ports view item port inline menu")
},
{
key: 'file/newFile',
id: MenuId.NewFile,
description: localize('file.newFile', "The 'New File...' quick pick, shown on welcome page and File menu. Supports property `{0}` to override the default title if needed.)", 'title'),

This comment has been minimized.

Copy link
@jrieken

jrieken Jul 13, 2021

Member

Supports property {0} to override the default title if needed.)", 'title'

@JacksonKearl 👀 can you clarify what that means?

supportsSubmenus: false,
},
{
key: 'editor/inlineCompletions/actions',
id: MenuId.InlineCompletionsActions,
Expand Down
219 changes: 219 additions & 0 deletions src/vs/workbench/contrib/welcome/common/newFile.contribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { assertIsDefined } from 'vs/base/common/types';
import { localize } from 'vs/nls';
import { Action2, IMenuService, MenuId, registerAction2, IMenu, MenuRegistry } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';

import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';


const category = localize('Create', "Create");

export const HasMultipleNewFileEntries = new RawContextKey<boolean>('hasMultipleNewFileEntries', false);

registerAction2(class extends Action2 {
constructor() {
super({
id: 'welcome.showNewFileEntries',
title: localize('welcome.newFile', "New File..."),
category,
f1: true,
keybinding: {
primary: KeyMod.Alt + KeyMod.CtrlCmd + KeyMod.WinCtrl + KeyCode.KEY_N,
weight: KeybindingWeight.WorkbenchContrib,
},
menu: {
id: MenuId.MenubarFileMenu,
when: HasMultipleNewFileEntries,
group: '1_new',
order: 3
}
});
}

run(accessor: ServicesAccessor) {
assertIsDefined(NewFileTemplatesManager.Instance).run();
}
});

type NewFileItem = { commandID: string, title: string, from: string, group: string };
class NewFileTemplatesManager extends Disposable {
static Instance: NewFileTemplatesManager | undefined;

private menu: IMenu;

private registry = new Map<string, NewFileItem>();

constructor(
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@ICommandService private readonly commandService: ICommandService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IExtensionService private readonly extensionService: IExtensionService,
@IMenuService menuService: IMenuService,
) {
super();

NewFileTemplatesManager.Instance = this;

this._register({ dispose() { if (NewFileTemplatesManager.Instance === this) { NewFileTemplatesManager.Instance = undefined; } } });
this._register(this.extensionService.onDidChangeExtensions(() => this.cacheRawContributionData()));
this.cacheRawContributionData();

this.menu = menuService.createMenu(MenuId.NewFile, contextKeyService);
this.updateContextKeys();
this._register(this.menu.onDidChange(() => { this.updateContextKeys(); }));
}

private async cacheRawContributionData() {
const allExts = await this.extensionService.getExtensions();
for (const ext of allExts) {
const contribution = ext.contributes?.menus?.['file/newFile'];
if (contribution?.length) {
contribution.forEach(menu => this.registry.set(menu.command, {
commandID: menu.command,
from: ext.displayName ?? ext.name,
title: (menu as any).title,
group: menu.group?.toLowerCase() ?? '',
}));
}
}
}


private allEntries(): NewFileItem[] {
const items: NewFileItem[] = [];
for (const [group, actions] of this.menu.getActions()) {
for (const action of actions) {
const registered = this.registry.get(action.id);
if (registered) {
if (!registered.title) { registered.title = action.label; }
items.push(registered);
}
else {
items.push({ commandID: action.id, from: localize('Built-In', "Built-In"), title: action.label, group: group });
}
}
}
return items;
}

private updateContextKeys() {
HasMultipleNewFileEntries.bindTo(this.contextKeyService).set(this.allEntries().length > 1);
}

run() {
const entries = this.allEntries();
if (entries.length === 0) {
throw Error('Unexpected empty new items list');
}
else if (entries.length === 1) {
this.commandService.executeCommand(entries[0].commandID);
}
else {
this.selectNewEntry(entries);
}
}

private async selectNewEntry(entries: NewFileItem[]) {
const disposables = new DisposableStore();
const qp = this.quickInputService.createQuickPick();
qp.title = localize('createNew', "Create New...");
qp.matchOnDetail = true;
qp.matchOnDescription = true;

const sortCategories = (a: string, b: string): number => {
const categoryPriority: Record<string, number> = { 'file': 1, 'notebook': 2 };
if (categoryPriority[a] && categoryPriority[b]) { return categoryPriority[b] - categoryPriority[a]; }
if (categoryPriority[a]) { return 1; }
if (categoryPriority[b]) { return -1; }
return a.localeCompare(b);
};

const displayCategory: Record<string, string> = {
'file': localize('file', "File"),
'notebook': localize('notebook', "Notebook"),
};

const refreshQp = (entries: NewFileItem[]) => {
const items: (((IQuickPickItem & NewFileItem) | IQuickPickSeparator))[] = [];
let lastSeparator: string | undefined;
entries
.sort((a, b) => -sortCategories(a.group, b.group))
.forEach((entry) => {
const command = entry.commandID;
const keybinding = this.keybindingService.lookupKeybinding(command || '', this.contextKeyService);
if (lastSeparator !== entry.group) {
items.push({
type: 'separator',
label: displayCategory[entry.group] ?? entry.group
});
lastSeparator = entry.group;
}
items.push({
...entry,
label: entry.title,
type: 'item',
keybinding,
buttons: command ? [
{
iconClass: 'codicon codicon-gear',
tooltip: localize('change keybinding', "Configure Keybinding")
}
] : [],
detail: '',
description: entry.from,
});
});
qp.items = items;
};
refreshQp(entries);

disposables.add(this.menu.onDidChange(() => refreshQp(this.allEntries())));

disposables.add(qp.onDidAccept(async e => {
const selected = qp.selectedItems[0] as (IQuickPickItem & NewFileItem);
if (selected) { await this.commandService.executeCommand(selected.commandID); }
qp.hide();
}));

disposables.add(qp.onDidHide(() => {
qp.dispose();
disposables.dispose();
}));

disposables.add(qp.onDidTriggerItemButton(e => {
qp.hide();
this.commandService.executeCommand('workbench.action.openGlobalKeybindings', (e.item as any).action.runCommand);
}));

qp.show();
}

}

Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(NewFileTemplatesManager, LifecyclePhase.Restored);

MenuRegistry.appendMenuItem(MenuId.NewFile, {
group: 'File',
command: {
id: 'workbench.action.files.newUntitledFile',
title: localize('miNewFile2', "Text File")
},
order: 1
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati
import { ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey';
import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { KeyCode } from 'vs/base/common/keyCodes';
import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { HasMultipleNewFileEntries, IGettingStartedNewMenuEntryDescriptorCategory, IGettingStartedService } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService';
import { IGettingStartedService } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService';
import { GettingStartedInput } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput';
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
Expand Down Expand Up @@ -187,80 +187,6 @@ registerAction2(class extends Action2 {
}
});

registerAction2(class extends Action2 {
constructor() {
super({
id: 'welcome.showNewFileEntries',
title: localize('welcome.newFile', "New File..."),
category,
f1: true,
keybinding: {
primary: KeyMod.Alt + KeyMod.CtrlCmd + KeyMod.WinCtrl + KeyCode.KEY_N,
weight: KeybindingWeight.WorkbenchContrib,
},
menu: {
id: MenuId.MenubarFileMenu,
when: HasMultipleNewFileEntries,
group: '1_new',
order: 3
}
});
}

run(accessor: ServicesAccessor) {
const gettingStartedService = accessor.get(IGettingStartedService);
gettingStartedService.selectNewEntry([
IGettingStartedNewMenuEntryDescriptorCategory.file,
IGettingStartedNewMenuEntryDescriptorCategory.notebook]);
}
});

registerAction2(class extends Action2 {
constructor() {
super({
id: 'welcome.showNewFolderEntries',
title: localize('welcome.newFolder', "New Folder..."),
category,
// f1: true,
keybinding: {
primary: KeyMod.Alt + KeyMod.CtrlCmd + KeyMod.WinCtrl + KeyCode.KEY_F,
weight: KeybindingWeight.WorkbenchContrib,
},
// menu: {
// id: MenuId.MenubarFileMenu,
// group: '1_new',
// order: 5
// }
});
}

run(accessor: ServicesAccessor) {
const gettingStartedService = accessor.get(IGettingStartedService);
gettingStartedService.selectNewEntry([IGettingStartedNewMenuEntryDescriptorCategory.folder]);
}
});

registerAction2(class extends Action2 {
constructor() {
super({
id: 'welcome.showNewEntries',
title: localize('welcome.new', "New..."),
category,
f1: true,
});
}

run(accessor: ServicesAccessor, args?: ('file' | 'folder' | 'notebook')[]) {
const gettingStartedService = accessor.get(IGettingStartedService);
const filters: IGettingStartedNewMenuEntryDescriptorCategory[] = [];
(args ?? []).forEach(arg => {
if (IGettingStartedNewMenuEntryDescriptorCategory[arg]) { filters.push(IGettingStartedNewMenuEntryDescriptorCategory[arg]); }
});

gettingStartedService.selectNewEntry(filters);
}
});

registerAction2(class extends Action2 {
constructor() {
super({
Expand Down Expand Up @@ -341,12 +267,6 @@ configurationRegistry.registerConfiguration({
type: 'boolean',
default: true,
description: localize('workbench.welcomePage.walkthroughs.openOnInstall', "When enabled, an extension's walkthrough will open upon install the extension. Walkthroughs are the items contributed the the 'Getting Started' section of the welcome page")
},
'workbench.welcome.experimental.startEntries': {
scope: ConfigurationScope.APPLICATION,
type: 'boolean',
default: false,
description: localize('workbench.welcome.experimental.startEntries', "Experimental. When enabled, extensions can use proposed API to contribute items to the New=>File... menu and welcome page item.")
}
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -196,39 +196,9 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo
}
});

export const startEntriesExtensionPoint = ExtensionsRegistry.registerExtensionPoint<IStartEntry[]>({
ExtensionsRegistry.registerExtensionPoint<IStartEntry[]>({
extensionPoint: 'startEntries',
jsonSchema: {
description: localize('startEntries', "Contribute commands to the \"Welcome: Start...\" pickers and \"File => New X...\" menu entries."),
type: 'array',
items: {
type: 'object',
required: ['title', 'command', 'category'],
additionalProperties: false,
defaultSnippets: [{ body: { 'title': '$1', 'command': '$3' } }],
properties: {
title: {
type: 'string',
description: localize('startEntries.title', "Title of item.")
},
command: {
type: 'string',
description: localize('startEntries.command', "Command to run.")
},
category: {
type: 'string',
description: localize('startEntries.category', "Category of the new entry."),
enum: ['file', 'folder', 'notebook', 'other'],
},
description: {
type: 'string',
description: localize('startEntries.description', "Description of item. We recommend leaving this blank unless the action is significantly nuanced in a way the title can not capture.")
},
when: {
type: 'string',
description: localize('startEntries.when', "Context key expression to control the visibility of this item.")
},
}
}
deprecationMessage: localize('removed', "Removed, use the menus => file/newFile contribution point instead"),
}
});
Loading

0 comments on commit 9006cc8

Please sign in to comment.