diff --git a/src/components/menu/style.scss b/src/components/menu/style.scss index 25737de8b..45432476a 100644 --- a/src/components/menu/style.scss +++ b/src/components/menu/style.scss @@ -85,6 +85,14 @@ } } + &__keybinding { + display: inline-block; + flex: 2 1 auto; + line-height: 1; + padding: 0 2em; + text-align: right; + } + &__separator { height: 1px; width: 100%; diff --git a/src/controller/activityBar.ts b/src/controller/activityBar.ts index ff67ed644..d00b43441 100644 --- a/src/controller/activityBar.ts +++ b/src/controller/activityBar.ts @@ -9,9 +9,6 @@ import { CONTEXT_MENU_EXPLORER, CONTEXT_MENU_SEARCH, CONTEXT_MENU_HIDE, - CONTEXT_MENU_COLOR_THEME, - CONTEXT_MENU_COMMAND_PALETTE, - CONTEXT_MENU_SETTINGS, IActivityBarItem, } from 'mo/model'; import { SelectColorThemeAction } from 'mo/monaco/selectColorThemeAction'; @@ -24,6 +21,12 @@ import { } from 'mo/services'; import { CommandQuickAccessViewAction } from 'mo/monaco/quickAccessViewAction'; import { IMonacoService, MonacoService } from 'mo/monaco/monacoService'; +import { + ACTION_QUICK_COMMAND, + ACTION_QUICK_ACCESS_SETTINGS, + ACTION_SELECT_THEME, +} from 'mo/model/keybinding'; + export interface IActivityBarController { /** * Called when activity bar item is clicked @@ -107,15 +110,15 @@ export class ActivityBarController break; } // manage button contextMenu - case CONTEXT_MENU_COMMAND_PALETTE: { + case ACTION_QUICK_COMMAND: { this.gotoQuickCommand(); break; } - case CONTEXT_MENU_SETTINGS: { + case ACTION_QUICK_ACCESS_SETTINGS: { this.settingsService.openSettingsInEditor(); break; } - case CONTEXT_MENU_COLOR_THEME: { + case ACTION_SELECT_THEME: { this.onSelectColorTheme(); break; } diff --git a/src/extensions/index.ts b/src/extensions/index.ts index c149ed1e2..0369edc6d 100644 --- a/src/extensions/index.ts +++ b/src/extensions/index.ts @@ -7,6 +7,7 @@ import { ExtendsActivityBar } from './activityBar'; import { ExtendsPanel } from './panel'; import { ExtendsExplorer } from './explorer'; import { ExtendsEditorTree } from './editorTree'; +import { ExtendsKeybinding } from './keybinding'; import { defaultColorThemeExtension } from './theme-defaults'; import { monokaiColorThemeExtension } from './theme-monokai'; @@ -28,4 +29,5 @@ export const defaultExtensions = [ monokaiColorThemeExtension, paleNightColorThemeExtension, ExtendsFolderTree, + ExtendsKeybinding, ]; diff --git a/src/extensions/keybinding/index.ts b/src/extensions/keybinding/index.ts new file mode 100644 index 000000000..298e73f41 --- /dev/null +++ b/src/extensions/keybinding/index.ts @@ -0,0 +1,15 @@ +import type { IExtensionService } from 'mo/services'; +import { IExtension } from 'mo/model'; +import { SelectLocaleAction } from 'mo/i18n/selectLocaleAction'; +import { QuickAccessSettings } from 'mo/monaco/quickAccessSettingsAction'; +import { CommandQuickAccessViewAction } from 'mo/monaco/quickAccessViewAction'; +import { SelectColorThemeAction } from 'mo/monaco/selectColorThemeAction'; + +export const ExtendsKeybinding: IExtension = { + activate(extensionCtx: IExtensionService) { + extensionCtx.registerAction(CommandQuickAccessViewAction); + extensionCtx.registerAction(SelectColorThemeAction); + extensionCtx.registerAction(QuickAccessSettings); + extensionCtx.registerAction(SelectLocaleAction); + }, +}; diff --git a/src/i18n/selectLocaleAction.ts b/src/i18n/selectLocaleAction.ts index 208aeca19..d55ca5317 100644 --- a/src/i18n/selectLocaleAction.ts +++ b/src/i18n/selectLocaleAction.ts @@ -9,9 +9,11 @@ import { Action2 } from 'mo/monaco/common'; import { localize } from './localize'; import { ILocaleService, LocaleService } from './localeService'; import { ILocale } from './localization'; +import { KeyCode, KeyMod } from 'mo/monaco'; +import { ACTION_SELECT_LOCALE } from 'mo/model/keybinding'; export class SelectLocaleAction extends Action2 { - static readonly ID = 'workbench.action.selectLocale'; + static readonly ID = ACTION_SELECT_LOCALE; static readonly LABEL = localize( 'select.locale', 'Select Display Language' @@ -29,6 +31,10 @@ export class SelectLocaleAction extends Action2 { alias: 'Select Display Language', precondition: undefined, f1: true, + keybinding: { + when: undefined, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L, + }, }); } diff --git a/src/model/keybinding.ts b/src/model/keybinding.ts index 30345ed08..261396b41 100644 --- a/src/model/keybinding.ts +++ b/src/model/keybinding.ts @@ -1 +1,69 @@ -export interface IKeybinding {} +import { KeyCode } from 'mo/monaco'; + +export const KeyCodeString: Partial<{ [key in KeyCode]: string }> = { + [KeyCode.Unknown]: '', + [KeyCode.Backspace]: '⌫', + [KeyCode.Tab]: '⇥', + [KeyCode.Enter]: '↩︎', + [KeyCode.KEY_0]: '0', + [KeyCode.KEY_1]: '1', + [KeyCode.KEY_2]: '2', + [KeyCode.KEY_3]: '3', + [KeyCode.KEY_4]: '4', + [KeyCode.KEY_5]: '5', + [KeyCode.KEY_6]: '6', + [KeyCode.KEY_7]: '7', + [KeyCode.KEY_8]: '8', + [KeyCode.KEY_9]: '9', + [KeyCode.KEY_A]: 'A', + [KeyCode.KEY_B]: 'B', + [KeyCode.KEY_C]: 'C', + [KeyCode.KEY_D]: 'D', + [KeyCode.KEY_E]: 'E', + [KeyCode.KEY_F]: 'F', + [KeyCode.KEY_G]: 'G', + [KeyCode.KEY_H]: 'H', + [KeyCode.KEY_I]: 'I', + [KeyCode.KEY_J]: 'J', + [KeyCode.KEY_K]: 'K', + [KeyCode.KEY_L]: 'L', + [KeyCode.KEY_M]: 'M', + [KeyCode.KEY_N]: 'N', + [KeyCode.KEY_O]: 'O', + [KeyCode.KEY_P]: 'P', + [KeyCode.KEY_Q]: 'Q', + [KeyCode.KEY_R]: 'R', + [KeyCode.KEY_S]: 'S', + [KeyCode.KEY_T]: 'T', + [KeyCode.KEY_U]: 'U', + [KeyCode.KEY_V]: 'V', + [KeyCode.KEY_W]: 'W', + [KeyCode.KEY_X]: 'X', + [KeyCode.KEY_Y]: 'Y', + [KeyCode.KEY_Z]: 'Z', + [KeyCode.US_SEMICOLON]: ';', + [KeyCode.US_EQUAL]: '+', + [KeyCode.US_COMMA]: ',', + [KeyCode.US_MINUS]: '-', + [KeyCode.US_DOT]: '.', + [KeyCode.US_SLASH]: '/', + [KeyCode.US_BACKTICK]: '~', + [KeyCode.US_OPEN_SQUARE_BRACKET]: '[', + [KeyCode.US_BACKSLASH]: '\\', + [KeyCode.US_CLOSE_SQUARE_BRACKET]: ']', + [KeyCode.US_QUOTE]: '"', +}; + +export interface ISimpleKeybinding { + ctrlKey: boolean; + shiftKey: boolean; + altKey: boolean; + metaKey: boolean; + keyCode: KeyCode; +} + +export const ACTION_QUICK_ACCESS_SETTINGS = + 'workbench.action.quickAccessSettings'; +export const ACTION_QUICK_COMMAND = 'workbench.action.quickCommand'; +export const ACTION_SELECT_THEME = 'workbench.action.selectTheme'; +export const ACTION_SELECT_LOCALE = 'workbench.action.selectLocale'; diff --git a/src/model/workbench/activityBar.ts b/src/model/workbench/activityBar.ts index 27cbb6076..288154e2a 100644 --- a/src/model/workbench/activityBar.ts +++ b/src/model/workbench/activityBar.ts @@ -1,6 +1,11 @@ import * as React from 'react'; import { IMenuItemProps } from 'mo/components/menu'; import { localize } from 'mo/i18n/localize'; +import { + ACTION_QUICK_ACCESS_SETTINGS, + ACTION_QUICK_COMMAND, + ACTION_SELECT_THEME, +} from '../keybinding'; /** * The activity bar event definition @@ -38,10 +43,6 @@ export interface IActivityBar { export const ACTIVITY_BAR_GLOBAL_SETTINGS = 'global.menu.settings'; export const ACTIVITY_BAR_GLOBAL_ACCOUNT = 'global.menu.account'; -export const CONTEXT_MENU_COMMAND_PALETTE = 'menu.commandPalette'; -export const CONTEXT_MENU_SETTINGS = 'menu.settings'; -export const CONTEXT_MENU_COLOR_THEME = 'menu.colorTheme'; - export const CONTEXT_MENU_MENU = 'menubar'; export const CONTEXT_MENU_EXPLORER = 'sidebar.explore.title'; export const CONTEXT_MENU_SEARCH = 'sidebar.search.title'; @@ -62,15 +63,15 @@ export function builtInActivityBar(): IActivityBar { type: 'global', contextMenu: [ { - id: CONTEXT_MENU_COMMAND_PALETTE, + id: ACTION_QUICK_COMMAND, name: localize('menu.commandPalette', 'Command Palette'), }, { - id: CONTEXT_MENU_SETTINGS, + id: ACTION_QUICK_ACCESS_SETTINGS, name: localize('menu.settings', 'Settings'), }, { - id: CONTEXT_MENU_COLOR_THEME, + id: ACTION_SELECT_THEME, name: localize('menu.colorTheme', 'Color Theme'), }, ], diff --git a/src/molecule.api.ts b/src/molecule.api.ts index f86d162bf..7b2da8c9d 100644 --- a/src/molecule.api.ts +++ b/src/molecule.api.ts @@ -42,8 +42,6 @@ import { NotificationService, IColorThemeService, ColorThemeService, - IExtensionService, - ExtensionService, ISettingsService, SettingsService, IProblemsService, @@ -101,12 +99,6 @@ export const colorTheme = container.resolve( ColorThemeService ); -/** - * Note: The extension service depends on other workbench services, - * So it need initialized be last one. - */ -export const extension = container.resolve(ExtensionService); - /** * Settings service */ diff --git a/src/monaco/common.ts b/src/monaco/common.ts index d634032f4..6b0ae750d 100644 --- a/src/monaco/common.ts +++ b/src/monaco/common.ts @@ -20,7 +20,15 @@ export enum KeybindingWeight { } export abstract class Action2 { - constructor(readonly desc: Readonly) {} + constructor( + readonly desc: Readonly<{ + /** + * Specify visible in quick access view + */ + f1: boolean; + [key: string]: any; + }> + ) {} abstract run(accessor: ServicesAccessor, ...args: any[]): any; } diff --git a/src/monaco/quickAccessSettingsAction.ts b/src/monaco/quickAccessSettingsAction.ts index fc73a807e..53e013b18 100644 --- a/src/monaco/quickAccessSettingsAction.ts +++ b/src/monaco/quickAccessSettingsAction.ts @@ -6,9 +6,10 @@ import { ISettingsService, SettingsService } from 'mo/services'; import { ServicesAccessor } from 'monaco-editor/esm/vs/platform/instantiation/common/instantiation'; import { container } from 'tsyringe'; import { Action2, KeybindingWeight } from './common'; +import { ACTION_QUICK_ACCESS_SETTINGS } from 'mo/model/keybinding'; export class QuickAccessSettings extends Action2 { - static readonly ID = 'workbench.action.quickAccessSettings'; + static readonly ID = ACTION_QUICK_ACCESS_SETTINGS; static readonly LABEL = localize( 'quickAccessSettings.label', 'Open Settings (JSON)' diff --git a/src/monaco/quickAccessViewAction.ts b/src/monaco/quickAccessViewAction.ts index c35255ed8..5fa84f276 100644 --- a/src/monaco/quickAccessViewAction.ts +++ b/src/monaco/quickAccessViewAction.ts @@ -29,6 +29,7 @@ import { EditorService, IEditorService } from 'mo/services'; import { Action2, KeybindingWeight } from './common'; import { MonacoService } from './monacoService'; import { registerQuickAccessProvider } from './quickAccessProvider'; +import { ACTION_QUICK_COMMAND } from 'mo/model/keybinding'; export class CommandQuickAccessProvider extends AbstractEditorCommandsQuickAccessProvider { static PREFIX = '>'; @@ -166,7 +167,7 @@ registerQuickAccessProvider({ }); export class CommandQuickAccessViewAction extends Action2 { - static ID = 'workbench.action.quickCommand'; + static ID = ACTION_QUICK_COMMAND; constructor() { super({ diff --git a/src/monaco/selectColorThemeAction.ts b/src/monaco/selectColorThemeAction.ts index ca8cd8957..7c3fb6fa7 100644 --- a/src/monaco/selectColorThemeAction.ts +++ b/src/monaco/selectColorThemeAction.ts @@ -11,9 +11,10 @@ import { ColorThemeService, IColorThemeService } from 'mo/services'; import { ServicesAccessor } from 'monaco-editor/esm/vs/platform/instantiation/common/instantiation'; import { container } from 'tsyringe'; import { Action2, KeybindingWeight } from './common'; +import { ACTION_SELECT_THEME } from 'mo/model/keybinding'; export class SelectColorThemeAction extends Action2 { - static readonly ID = 'workbench.action.selectTheme'; + static readonly ID = ACTION_SELECT_THEME; static readonly LABEL = localize('selectTheme.label', 'Color Theme'); private readonly colorThemeService: IColorThemeService; diff --git a/src/provider/molecule.tsx b/src/provider/molecule.tsx index 45b97b124..b8bdaf4b5 100644 --- a/src/provider/molecule.tsx +++ b/src/provider/molecule.tsx @@ -11,13 +11,9 @@ import { IExtensionService, } from 'mo/services/extensionService'; import { IMonacoService, MonacoService } from 'mo/monaco/monacoService'; -import { CommandQuickAccessViewAction } from 'mo/monaco/quickAccessViewAction'; -import { registerAction2 } from 'mo/monaco/common'; -import { QuickAccessSettings } from 'mo/monaco/quickAccessSettingsAction'; -import { SelectColorThemeAction } from 'mo/monaco/selectColorThemeAction'; import { ILocaleService, LocaleService } from 'mo/i18n/localeService'; import { ILayoutService, LayoutService } from 'mo/services'; -import { SelectLocaleAction } from 'mo/i18n/selectLocaleAction'; + export interface IMoleculeProps { extensions?: IExtension[]; locales?: ILocale[]; @@ -64,18 +60,6 @@ export class MoleculeProvider extends React.Component { this.monacoService.initWorkspace(this.container!); this.extensionService.load(defaultExtensions); this.extensionService.load(extensions); - - this.initWorkbenchActions(); - } - - /** - * TODO: move the register of actions to extensionService - */ - initWorkbenchActions() { - registerAction2(CommandQuickAccessViewAction); - registerAction2(SelectColorThemeAction); - registerAction2(QuickAccessSettings); - registerAction2(SelectLocaleAction); } public render() { diff --git a/src/services/extensionService.ts b/src/services/extensionService.ts index 4771ab8db..61bb8f1bf 100644 --- a/src/services/extensionService.ts +++ b/src/services/extensionService.ts @@ -8,6 +8,7 @@ import { ColorThemeService, IColorThemeService, } from './theme/colorThemeService'; +import { Action2, registerAction2 } from 'mo/monaco/common'; export interface IExtensionService { /** @@ -22,6 +23,15 @@ export interface IExtensionService { load(extensions: IExtension[]); loadContributes(contributes: IContribute); unload(extension: IExtension); + /** + * Register action based in Action2, + * @example + * ```ts + * const action = class Action extends Action2 {}; + * registerAction(action); + * ``` + */ + registerAction(actionClass: { new (): Action2 }): void; } @singleton() @@ -71,6 +81,10 @@ export class ExtensionService implements IExtensionService { }); } + public registerAction(actionClass: { new (): Action2 }) { + registerAction2(actionClass); + } + unload(extension: IExtension) { console.log('unload extension:', extension.name); } diff --git a/src/services/index.ts b/src/services/index.ts index c73dfeb8a..06122cdd8 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,4 +1,4 @@ -export * from './extensionService'; +export type { IExtensionService } from './extensionService'; export * from './theme/colorThemeService'; export * from './workbench'; export * from './settingsService'; diff --git a/src/services/keybinding.ts b/src/services/keybinding.ts new file mode 100644 index 000000000..c8e4f4266 --- /dev/null +++ b/src/services/keybinding.ts @@ -0,0 +1,81 @@ +import { ResolvedKeybindingItem } from 'monaco-editor/esm/vs/platform/keybinding/common/resolvedKeybindingItem'; +import { KeybindingsRegistry } from 'monaco-editor/esm/vs/platform/keybinding/common/keybindingsRegistry'; +import { Utils } from '@dtinsight/dt-utils/lib'; +import { ISimpleKeybinding, KeyCodeString } from 'mo/model/keybinding'; +export interface IKeybinding { + _isMac: boolean; + /** + * Query global keybingding + * @example + * ```ts + * const key = queryGlobalKeybinding('workbench.test'); + * // [{ctrlKey: boolean; shiftKey: false; altKey: false; metaKey: false; keyCode: 0;}] + * ``` + */ + queryGlobalKeybinding: (id: string) => ISimpleKeybinding[] | null; + /** + * Convert simple keybinding to a string + * @example + * ```ts + * const key = queryGlobalKeybinding('workbench.test'); + * // [{ctrlKey: boolean; shiftKey: false; altKey: false; metaKey: true; keyCode: 82;}] + * convertSimpleKeybindingToString(key); + * // ⌘, + * ``` + */ + convertSimpleKeybindingToString: ( + keybinding?: ISimpleKeybinding[] + ) => string; +} + +export const KeybindingHelper: IKeybinding = { + _isMac: Utils.isMacOs(), + + queryGlobalKeybinding: (id: string) => { + const defaultKeybindings: ResolvedKeybindingItem[] = KeybindingsRegistry.getDefaultKeybindings(); + const globalKeybindings = defaultKeybindings.filter((key) => !key.when); + + // 'Cause one action can occupy multiply keybinding, so there should be filter rather than find + const targetKeybinding = globalKeybindings.filter( + (i) => i.command === id + ); + + if (targetKeybinding.length) { + // Since it's sorted out by the weight when getDefaultKeybindings, the targetKeybinding is sorted by weight + // Get lower priority keybinding + const lowerPriorty = targetKeybinding[targetKeybinding.length - 1]; + // keybinding which is chord key[组合键] can get more than 1 parts + const keybindings: ISimpleKeybinding[] = + lowerPriorty.keybinding.parts; + return keybindings; + } + return null; + }, + + convertSimpleKeybindingToString: (keybinding: ISimpleKeybinding[] = []) => { + return ( + keybinding + .map((key) => { + const res: string[] = []; + if (key.altKey) { + res.push(KeybindingHelper._isMac ? '⌥' : 'Alt'); + } + if (key.ctrlKey) { + res.push(KeybindingHelper._isMac ? '⌃' : 'Ctrl'); + } + if (key.metaKey) { + res.push(KeybindingHelper._isMac ? '⌘' : 'Meta'); + } + if (key.shiftKey) { + res.push(KeybindingHelper._isMac ? '⇧' : 'Shift'); + } + if (key.keyCode) { + res.push(KeyCodeString[key.keyCode] || ''); + } + return res.join(KeybindingHelper._isMac ? '' : '+'); + }) + // Insert a space between chord key + .join(' ') + ); + }, +}; diff --git a/src/workbench/activityBar/activityBarItem.tsx b/src/workbench/activityBar/activityBarItem.tsx index c0def865d..f4360c69e 100644 --- a/src/workbench/activityBar/activityBarItem.tsx +++ b/src/workbench/activityBar/activityBarItem.tsx @@ -14,6 +14,7 @@ import { } from './base'; import { DropDown } from 'mo/components'; import { DropDownRef } from 'mo/components/dropdown'; +import { KeybindingHelper } from 'mo/services/keybinding'; export function ActivityBarItem( props: IActivityBarItem & IActivityBarController @@ -56,7 +57,24 @@ export function ActivityBarItem( {render?.() || null} ); - const overlay = ; + const overlay = ( + { + if (menu.id) { + const keybindingObj = KeybindingHelper.queryGlobalKeybinding( + menu.id + ); + if (keybindingObj) { + menu.keybinding = KeybindingHelper.convertSimpleKeybindingToString( + keybindingObj + ); + } + } + return menu; + })} + /> + ); const hasContextMenu = contextMenu.length > 0; diff --git a/src/workbench/menuBar/menuBar.tsx b/src/workbench/menuBar/menuBar.tsx index 1f68d87fa..a6f456ae7 100644 --- a/src/workbench/menuBar/menuBar.tsx +++ b/src/workbench/menuBar/menuBar.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { getBEMElement, prefixClaName } from 'mo/common/className'; -import { IMenuBar } from 'mo/model/workbench/menuBar'; +import { IMenuBar, IMenuBarItem } from 'mo/model/workbench/menuBar'; import { IMenuBarController } from 'mo/controller/menuBar'; - -import { Menu } from 'mo/components/menu'; import { DropDown, DropDownRef } from 'mo/components/dropdown'; +import { IMenuProps, Menu } from 'mo/components/menu'; import { Icon } from 'mo/components/icon'; +import { KeybindingHelper } from 'mo/services/keybinding'; const defaultClassName = prefixClaName('menuBar'); const actionClassName = getBEMElement(defaultClassName, 'action'); @@ -13,12 +13,41 @@ const actionClassName = getBEMElement(defaultClassName, 'action'); export function MenuBar(props: IMenuBar & IMenuBarController) { const { data, onClick } = props; const childRef = React.useRef(null); + + const addKeybindingForData = ( + rawData: IMenuBarItem[] = [] + ): IMenuProps[] => { + const resData: IMenuProps[] = rawData.concat(); + const stack = [...resData]; + while (stack.length) { + const head = stack.pop(); + if (head) { + if (head?.data) { + stack.push(...head.data); + } else { + const simplyKeybinding = + KeybindingHelper.queryGlobalKeybinding(head.id!) || []; + if (simplyKeybinding.length) { + head.keybinding = KeybindingHelper.convertSimpleKeybindingToString( + simplyKeybinding + ); + } + } + } + } + return resData; + }; + const handleClick = (e: React.MouseEvent, item) => { onClick?.(e, item); (childRef.current as any)!.dispose(); }; const overlay = ( - + ); return (
diff --git a/stories/extensions/test/testPane.tsx b/stories/extensions/test/testPane.tsx index b1c5b1f97..d22fcb9af 100644 --- a/stories/extensions/test/testPane.tsx +++ b/stories/extensions/test/testPane.tsx @@ -299,9 +299,6 @@ PARTITIONED BY (ds string) lifecycle 1000; AAA
-
- {}} /> -
); diff --git a/test/mo.test.tsx b/test/mo.test.tsx deleted file mode 100644 index 3bae9c474..000000000 --- a/test/mo.test.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import molecule from 'mo'; - -describe('Test Mo Entry', () => { - test('Instance the extensionService', () => { - expect(molecule.extension).not.toBeNull(); - }); -});