diff --git a/packages/engine-core/src/common/dom.ts b/packages/engine-core/src/common/dom.ts new file mode 100644 index 000000000..91017d54e --- /dev/null +++ b/packages/engine-core/src/common/dom.ts @@ -0,0 +1,118 @@ +import { type IDisposable, isWebKit } from '@alilc/lowcode-shared'; + +export const DOMEventType = { + // Mouse + CLICK: 'click', + AUXCLICK: 'auxclick', + DBLCLICK: 'dblclick', + MOUSE_UP: 'mouseup', + MOUSE_DOWN: 'mousedown', + MOUSE_OVER: 'mouseover', + MOUSE_MOVE: 'mousemove', + MOUSE_OUT: 'mouseout', + MOUSE_ENTER: 'mouseenter', + MOUSE_LEAVE: 'mouseleave', + MOUSE_WHEEL: 'wheel', + POINTER_UP: 'pointerup', + POINTER_DOWN: 'pointerdown', + POINTER_MOVE: 'pointermove', + POINTER_LEAVE: 'pointerleave', + CONTEXT_MENU: 'contextmenu', + WHEEL: 'wheel', + // Keyboard + KEY_DOWN: 'keydown', + KEY_PRESS: 'keypress', + KEY_UP: 'keyup', + // HTML Document + LOAD: 'load', + BEFORE_UNLOAD: 'beforeunload', + UNLOAD: 'unload', + PAGE_SHOW: 'pageshow', + PAGE_HIDE: 'pagehide', + PASTE: 'paste', + ABORT: 'abort', + ERROR: 'error', + RESIZE: 'resize', + SCROLL: 'scroll', + FULLSCREEN_CHANGE: 'fullscreenchange', + WK_FULLSCREEN_CHANGE: 'webkitfullscreenchange', + // Form + SELECT: 'select', + CHANGE: 'change', + SUBMIT: 'submit', + RESET: 'reset', + FOCUS: 'focus', + FOCUS_IN: 'focusin', + FOCUS_OUT: 'focusout', + BLUR: 'blur', + INPUT: 'input', + // Local Storage + STORAGE: 'storage', + // Drag + DRAG_START: 'dragstart', + DRAG: 'drag', + DRAG_ENTER: 'dragenter', + DRAG_LEAVE: 'dragleave', + DRAG_OVER: 'dragover', + DROP: 'drop', + DRAG_END: 'dragend', + // Animation + ANIMATION_START: isWebKit ? 'webkitAnimationStart' : 'animationstart', + ANIMATION_END: isWebKit ? 'webkitAnimationEnd' : 'animationend', + ANIMATION_ITERATION: isWebKit ? 'webkitAnimationIteration' : 'animationiteration', +} as const; + +class DomListener implements IDisposable { + private _handler: (e: any) => void; + private _node: EventTarget; + private readonly _type: string; + private readonly _options: boolean | AddEventListenerOptions; + + constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) { + this._node = node; + this._type = type; + this._handler = handler; + this._options = options || false; + this._node.addEventListener(this._type, this._handler, this._options); + } + + dispose(): void { + if (!this._handler) { + // Already disposed + return; + } + + this._node.removeEventListener(this._type, this._handler, this._options); + + // Prevent leakers from holding on to the dom or handler func + this._node = null!; + this._handler = null!; + } +} + +export function addDisposableListener( + node: EventTarget, + type: K, + handler: (event: GlobalEventHandlersEventMap[K]) => void, + useCapture?: boolean, +): IDisposable; +export function addDisposableListener( + node: EventTarget, + type: string, + handler: (event: any) => void, + useCapture?: boolean, +): IDisposable; +export function addDisposableListener( + node: EventTarget, + type: string, + handler: (event: any) => void, + options: AddEventListenerOptions, +): IDisposable; +export function addDisposableListener( + node: EventTarget, + type: string, + handler: (event: any) => void, + useCaptureOrOptions?: boolean | AddEventListenerOptions, +): IDisposable { + return new DomListener(node, type, handler, useCaptureOrOptions); +} diff --git a/packages/engine-core/src/common/index.ts b/packages/engine-core/src/common/index.ts index 292fa1e36..cb89d3880 100644 --- a/packages/engine-core/src/common/index.ts +++ b/packages/engine-core/src/common/index.ts @@ -3,8 +3,10 @@ import * as Schemas from './schemas'; export { Schemas }; export * from './charCode'; +export * from './dom'; export * from './glob'; export * from './keyCodes'; +export * from './keyCodeUtils'; export * from './path'; export * from './strings'; export * from './ternarySearchTree'; diff --git a/packages/engine-core/src/common/keyCodeUtils.ts b/packages/engine-core/src/common/keyCodeUtils.ts new file mode 100644 index 000000000..543ebef99 --- /dev/null +++ b/packages/engine-core/src/common/keyCodeUtils.ts @@ -0,0 +1,242 @@ +import { KeyCode } from './keyCodes'; + +class KeyCodeStrMap { + public _keyCodeToStr: string[]; + public _strToKeyCode: { [str: string]: KeyCode }; + + constructor() { + this._keyCodeToStr = []; + this._strToKeyCode = Object.create(null); + } + + define(keyCode: KeyCode, str: string): void { + this._keyCodeToStr[keyCode] = str; + this._strToKeyCode[str.toLowerCase()] = keyCode; + } + + keyCodeToStr(keyCode: KeyCode): string { + return this._keyCodeToStr[keyCode]; + } + + strToKeyCode(str: string): KeyCode { + return this._strToKeyCode[str.toLowerCase()] || KeyCode.Unknown; + } +} + +const uiMap = new KeyCodeStrMap(); +const userSettingsUSMap = new KeyCodeStrMap(); +const userSettingsGeneralMap = new KeyCodeStrMap(); + +export const KeyCodeUtils = { + toString(keyCode: KeyCode): string { + return uiMap.keyCodeToStr(keyCode); + }, + fromString(key: string): KeyCode { + return uiMap.strToKeyCode(key); + }, + toUserSettingsUS(keyCode: KeyCode): string { + return userSettingsUSMap.keyCodeToStr(keyCode); + }, + toUserSettingsGeneral(keyCode: KeyCode): string { + return userSettingsGeneralMap.keyCodeToStr(keyCode); + }, + fromUserSettings(key: string): KeyCode { + return userSettingsUSMap.strToKeyCode(key) || userSettingsGeneralMap.strToKeyCode(key); + }, +}; + +export const EVENT_CODE_TO_KEY_CODE_MAP: { [keyCode: string]: KeyCode } = {}; + +(function () { + // See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx + // see https://www.toptal.com/developers/keycode/table + + const empty = ''; + type IMappingEntry = [string, KeyCode, string, string, string]; + + const mappings: IMappingEntry[] = [ + // scanCode, keyCode, keyCodeStr, usUserSettingsLabel, generalUserSettingsLabel + ['Unidentified', KeyCode.Unknown, 'unknown', empty, empty], + ['KeyA', KeyCode.KeyA, 'A', empty, empty], + ['KeyB', KeyCode.KeyB, 'B', empty, empty], + ['KeyC', KeyCode.KeyC, 'C', empty, empty], + ['KeyD', KeyCode.KeyD, 'D', empty, empty], + ['KeyE', KeyCode.KeyE, 'E', empty, empty], + ['KeyF', KeyCode.KeyF, 'F', empty, empty], + ['KeyG', KeyCode.KeyG, 'G', empty, empty], + ['KeyH', KeyCode.KeyH, 'H', empty, empty], + ['KeyI', KeyCode.KeyI, 'I', empty, empty], + ['KeyJ', KeyCode.KeyJ, 'J', empty, empty], + ['KeyK', KeyCode.KeyK, 'K', empty, empty], + ['KeyL', KeyCode.KeyL, 'L', empty, empty], + ['KeyM', KeyCode.KeyM, 'M', empty, empty], + ['KeyN', KeyCode.KeyN, 'N', empty, empty], + ['KeyO', KeyCode.KeyO, 'O', empty, empty], + ['KeyP', KeyCode.KeyP, 'P', empty, empty], + ['KeyQ', KeyCode.KeyQ, 'Q', empty, empty], + ['KeyR', KeyCode.KeyR, 'R', empty, empty], + ['KeyS', KeyCode.KeyS, 'S', empty, empty], + ['KeyT', KeyCode.KeyT, 'T', empty, empty], + ['KeyU', KeyCode.KeyU, 'U', empty, empty], + ['KeyV', KeyCode.KeyV, 'V', empty, empty], + ['KeyW', KeyCode.KeyW, 'W', empty, empty], + ['KeyX', KeyCode.KeyX, 'X', empty, empty], + ['KeyY', KeyCode.KeyY, 'Y', empty, empty], + ['KeyZ', KeyCode.KeyZ, 'Z', empty, empty], + ['Digit1', KeyCode.Digit1, '1', empty, empty], + ['Digit2', KeyCode.Digit2, '2', empty, empty], + ['Digit3', KeyCode.Digit3, '3', empty, empty], + ['Digit4', KeyCode.Digit4, '4', empty, empty], + ['Digit5', KeyCode.Digit5, '5', empty, empty], + ['Digit6', KeyCode.Digit6, '6', empty, empty], + ['Digit7', KeyCode.Digit7, '7', empty, empty], + ['Digit8', KeyCode.Digit8, '8', empty, empty], + ['Digit9', KeyCode.Digit9, '9', empty, empty], + ['Digit0', KeyCode.Digit0, '0', empty, empty], + ['Enter', KeyCode.Enter, 'Enter', empty, empty], + ['Escape', KeyCode.Escape, 'Escape', empty, empty], + ['Backspace', KeyCode.Backspace, 'Backspace', empty, empty], + ['Tab', KeyCode.Tab, 'Tab', empty, empty], + ['Space', KeyCode.Space, 'Space', empty, empty], + ['Minus', KeyCode.Minus, '-', '-', 'OEM_MINUS'], + ['Equal', KeyCode.Equal, '=', '=', 'OEM_PLUS'], + ['BracketLeft', KeyCode.BracketLeft, '[', '[', 'OEM_4'], + ['BracketRight', KeyCode.BracketRight, ']', ']', 'OEM_6'], + ['Backslash', KeyCode.Backslash, '\\', '\\', 'OEM_5'], + ['Semicolon', KeyCode.Semicolon, ';', ';', 'OEM_1'], + ['Quote', KeyCode.Quote, `'`, `'`, 'OEM_7'], + ['Backquote', KeyCode.Backquote, '`', '`', 'OEM_3'], + ['Comma', KeyCode.Comma, ',', ',', 'OEM_COMMA'], + ['Period', KeyCode.Period, '.', '.', 'OEM_PERIOD'], + ['Slash', KeyCode.Slash, '/', '/', 'OEM_2'], + ['CapsLock', KeyCode.CapsLock, 'CapsLock', empty, empty], + ['F1', KeyCode.F1, 'F1', empty, empty], + ['F2', KeyCode.F2, 'F2', empty, empty], + ['F3', KeyCode.F3, 'F3', empty, empty], + ['F4', KeyCode.F4, 'F4', empty, empty], + ['F5', KeyCode.F5, 'F5', empty, empty], + ['F6', KeyCode.F6, 'F6', empty, empty], + ['F7', KeyCode.F7, 'F7', empty, empty], + ['F8', KeyCode.F8, 'F8', empty, empty], + ['F9', KeyCode.F9, 'F9', empty, empty], + ['F10', KeyCode.F10, 'F10', empty, empty], + ['F11', KeyCode.F11, 'F11', empty, empty], + ['F12', KeyCode.F12, 'F12', empty, empty], + ['PrintScreen', KeyCode.Unknown, empty, empty, empty], + ['ScrollLock', KeyCode.ScrollLock, 'ScrollLock', empty, empty], + ['Pause', KeyCode.PauseBreak, 'PauseBreak', empty, empty], + ['Insert', KeyCode.Insert, 'Insert', empty, empty], + ['Home', KeyCode.Home, 'Home', empty, empty], + ['PageUp', KeyCode.PageUp, 'PageUp', empty, empty], + ['Delete', KeyCode.Delete, 'Delete', empty, empty], + ['End', KeyCode.End, 'End', empty, empty], + ['PageDown', KeyCode.PageDown, 'PageDown', empty, empty], + ['ArrowRight', KeyCode.RightArrow, 'RightArrow', 'Right', empty], + ['ArrowLeft', KeyCode.LeftArrow, 'LeftArrow', 'Left', empty], + ['ArrowDown', KeyCode.DownArrow, 'DownArrow', 'Down', empty], + ['ArrowUp', KeyCode.UpArrow, 'UpArrow', 'Up', empty], + ['NumLock', KeyCode.NumLock, 'NumLock', empty, empty], + ['NumpadDivide', KeyCode.NumpadDivide, 'NumPad_Divide', empty, empty], + ['NumpadMultiply', KeyCode.NumpadMultiply, 'NumPad_Multiply', empty, empty], + ['NumpadSubtract', KeyCode.NumpadSubtract, 'NumPad_Subtract', empty, empty], + ['NumpadAdd', KeyCode.NumpadAdd, 'NumPad_Add', empty, empty], + ['NumpadEnter', KeyCode.Enter, empty, empty, empty], + ['Numpad1', KeyCode.Numpad1, 'NumPad1', empty, empty], + ['Numpad2', KeyCode.Numpad2, 'NumPad2', empty, empty], + ['Numpad3', KeyCode.Numpad3, 'NumPad3', empty, empty], + ['Numpad4', KeyCode.Numpad4, 'NumPad4', empty, empty], + ['Numpad5', KeyCode.Numpad5, 'NumPad5', empty, empty], + ['Numpad6', KeyCode.Numpad6, 'NumPad6', empty, empty], + ['Numpad7', KeyCode.Numpad7, 'NumPad7', empty, empty], + ['Numpad8', KeyCode.Numpad8, 'NumPad8', empty, empty], + ['Numpad9', KeyCode.Numpad9, 'NumPad9', empty, empty], + ['Numpad0', KeyCode.Numpad0, 'NumPad0', empty, empty], + ['NumpadDecimal', KeyCode.NumpadDecimal, 'NumPad_Decimal', empty, empty], + ['IntlBackslash', KeyCode.IntlBackslash, 'OEM_102', empty, empty], + ['ContextMenu', KeyCode.ContextMenu, 'ContextMenu', empty, empty], + ['Power', KeyCode.Unknown, empty, empty, empty], + ['NumpadEqual', KeyCode.Unknown, empty, empty, empty], + ['F13', KeyCode.F13, 'F13', empty, empty], + ['F14', KeyCode.F14, 'F14', empty, empty], + ['F15', KeyCode.F15, 'F15', empty, empty], + ['F16', KeyCode.F16, 'F16', empty, empty], + ['F17', KeyCode.F17, 'F17', empty, empty], + ['F18', KeyCode.F18, 'F18', empty, empty], + ['F19', KeyCode.F19, 'F19', empty, empty], + ['F20', KeyCode.F20, 'F20', empty, empty], + ['F21', KeyCode.F21, 'F21', empty, empty], + ['F22', KeyCode.F22, 'F22', empty, empty], + ['F23', KeyCode.F23, 'F23', empty, empty], + ['F24', KeyCode.F24, 'F24', empty, empty], + ['AudioVolumeMute', KeyCode.AudioVolumeMute, 'AudioVolumeMute', empty, empty], + ['AudioVolumeUp', KeyCode.AudioVolumeUp, 'AudioVolumeUp', empty, empty], + ['AudioVolumeDown', KeyCode.AudioVolumeDown, 'AudioVolumeDown', empty, empty], + ['NumpadComma', KeyCode.NUMPAD_SEPARATOR, 'NumPad_Separator', empty, empty], + ['IntlRo', KeyCode.ABNT_C1, 'ABNT_C1', empty, empty], + ['NumpadClear', KeyCode.Clear, 'Clear', empty, empty], + [empty, KeyCode.Ctrl, 'Ctrl', empty, empty], + [empty, KeyCode.Shift, 'Shift', empty, empty], + [empty, KeyCode.Alt, 'Alt', empty, empty], + [empty, KeyCode.Meta, 'Meta', empty, empty], + ['ControlLeft', KeyCode.Ctrl, empty, empty, empty], + ['ShiftLeft', KeyCode.Shift, empty, empty, empty], + ['AltLeft', KeyCode.Alt, empty, empty, empty], + ['MetaLeft', KeyCode.Meta, empty, empty, empty], + ['ControlRight', KeyCode.Ctrl, empty, empty, empty], + ['ShiftRight', KeyCode.Shift, empty, empty, empty], + ['AltRight', KeyCode.Alt, empty, empty, empty], + ['MetaRight', KeyCode.Meta, empty, empty, empty], + ['MediaTrackNext', KeyCode.MediaTrackNext, 'MediaTrackNext', empty, empty], + ['MediaTrackPrevious', KeyCode.MediaTrackPrevious, 'MediaTrackPrevious', empty, empty], + ['MediaStop', KeyCode.MediaStop, 'MediaStop', empty, empty], + ['MediaPlayPause', KeyCode.MediaPlayPause, 'MediaPlayPause', empty, empty], + ['MediaSelect', KeyCode.LaunchMediaPlayer, 'LaunchMediaPlayer', empty, empty], + ['LaunchMail', KeyCode.LaunchMail, 'LaunchMail', empty, empty], + ['LaunchApp2', KeyCode.LaunchApp2, 'LaunchApp2', empty, empty], + ['LaunchScreenSaver', KeyCode.Unknown, empty, empty, empty], + ['BrowserSearch', KeyCode.BrowserSearch, 'BrowserSearch', empty, empty], + ['BrowserHome', KeyCode.BrowserHome, 'BrowserHome', empty, empty], + ['BrowserBack', KeyCode.BrowserBack, 'BrowserBack', empty, empty], + ['BrowserForward', KeyCode.BrowserForward, 'BrowserForward', empty, empty], + + // See https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html + // If an Input Method Editor is processing key input and the event is keydown, return 229. + [empty, KeyCode.KEY_IN_COMPOSITION, 'KeyInComposition', empty, empty], + [empty, KeyCode.ABNT_C2, 'ABNT_C2', empty, empty], + [empty, KeyCode.OEM_8, 'OEM_8', empty, empty], + ]; + + const seenKeyCode: boolean[] = []; + + for (const mapping of mappings) { + const [scanCode, keyCode, keyCodeStr, usUserSettingsLabel, generalUserSettingsLabel] = mapping; + + if (!seenKeyCode[keyCode]) { + seenKeyCode[keyCode] = true; + if (!keyCodeStr) { + throw new Error(`String representation missing for key code ${keyCode} around scan code ${scanCode}`); + } + uiMap.define(keyCode, keyCodeStr); + userSettingsUSMap.define(keyCode, usUserSettingsLabel || keyCodeStr); + userSettingsGeneralMap.define(keyCode, generalUserSettingsLabel || usUserSettingsLabel || keyCodeStr); + } + + if (scanCode) { + EVENT_CODE_TO_KEY_CODE_MAP[scanCode] = keyCode; + } + } + + console.log( + '%c [ IMMUTABLE_KEY_CODE_TO_CODE ]-500', + 'font-size:13px; background:pink; color:#bf2c9f;', + uiMap, + userSettingsUSMap, + userSettingsGeneralMap, + EVENT_CODE_TO_KEY_CODE_MAP, + ); +})(); + +export function KeyChord(firstPart: number, secondPart: number): number { + const chordPart = ((secondPart & 0x0000ffff) << 16) >>> 0; + return (firstPart | chordPart) >>> 0; +} diff --git a/packages/engine-core/src/common/keyCodes.ts b/packages/engine-core/src/common/keyCodes.ts index 0e678761a..ec6332633 100644 --- a/packages/engine-core/src/common/keyCodes.ts +++ b/packages/engine-core/src/common/keyCodes.ts @@ -1,8 +1,3 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - /** * Virtual Key Codes, the value does not hold any inherent meaning. * Inspired somewhat from https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx @@ -224,1050 +219,3 @@ export const enum KeyCode { */ MAX_VALUE, } - -/** - * keyboardEvent.code - */ -export const enum ScanCode { - DependsOnKbLayout = -1, - None, - Hyper, - Super, - Fn, - FnLock, - Suspend, - Resume, - Turbo, - Sleep, - WakeUp, - KeyA, - KeyB, - KeyC, - KeyD, - KeyE, - KeyF, - KeyG, - KeyH, - KeyI, - KeyJ, - KeyK, - KeyL, - KeyM, - KeyN, - KeyO, - KeyP, - KeyQ, - KeyR, - KeyS, - KeyT, - KeyU, - KeyV, - KeyW, - KeyX, - KeyY, - KeyZ, - Digit1, - Digit2, - Digit3, - Digit4, - Digit5, - Digit6, - Digit7, - Digit8, - Digit9, - Digit0, - Enter, - Escape, - Backspace, - Tab, - Space, - Minus, - Equal, - BracketLeft, - BracketRight, - Backslash, - IntlHash, - Semicolon, - Quote, - Backquote, - Comma, - Period, - Slash, - CapsLock, - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - PrintScreen, - ScrollLock, - Pause, - Insert, - Home, - PageUp, - Delete, - End, - PageDown, - ArrowRight, - ArrowLeft, - ArrowDown, - ArrowUp, - NumLock, - NumpadDivide, - NumpadMultiply, - NumpadSubtract, - NumpadAdd, - NumpadEnter, - Numpad1, - Numpad2, - Numpad3, - Numpad4, - Numpad5, - Numpad6, - Numpad7, - Numpad8, - Numpad9, - Numpad0, - NumpadDecimal, - IntlBackslash, - ContextMenu, - Power, - NumpadEqual, - F13, - F14, - F15, - F16, - F17, - F18, - F19, - F20, - F21, - F22, - F23, - F24, - Open, - Help, - Select, - Again, - Undo, - Cut, - Copy, - Paste, - Find, - AudioVolumeMute, - AudioVolumeUp, - AudioVolumeDown, - NumpadComma, - IntlRo, - KanaMode, - IntlYen, - Convert, - NonConvert, - Lang1, - Lang2, - Lang3, - Lang4, - Lang5, - Abort, - Props, - NumpadParenLeft, - NumpadParenRight, - NumpadBackspace, - NumpadMemoryStore, - NumpadMemoryRecall, - NumpadMemoryClear, - NumpadMemoryAdd, - NumpadMemorySubtract, - NumpadClear, - NumpadClearEntry, - ControlLeft, - ShiftLeft, - AltLeft, - MetaLeft, - ControlRight, - ShiftRight, - AltRight, - MetaRight, - BrightnessUp, - BrightnessDown, - MediaPlay, - MediaRecord, - MediaFastForward, - MediaRewind, - MediaTrackNext, - MediaTrackPrevious, - MediaStop, - Eject, - MediaPlayPause, - MediaSelect, - LaunchMail, - LaunchApp2, - LaunchApp1, - SelectTask, - LaunchScreenSaver, - BrowserSearch, - BrowserHome, - BrowserBack, - BrowserForward, - BrowserStop, - BrowserRefresh, - BrowserFavorites, - ZoomToggle, - MailReply, - MailForward, - MailSend, - - MAX_VALUE, -} - -class KeyCodeStrMap { - public _keyCodeToStr: string[]; - public _strToKeyCode: { [str: string]: KeyCode }; - - constructor() { - this._keyCodeToStr = []; - this._strToKeyCode = Object.create(null); - } - - define(keyCode: KeyCode, str: string): void { - this._keyCodeToStr[keyCode] = str; - this._strToKeyCode[str.toLowerCase()] = keyCode; - } - - keyCodeToStr(keyCode: KeyCode): string { - return this._keyCodeToStr[keyCode]; - } - - strToKeyCode(str: string): KeyCode { - return this._strToKeyCode[str.toLowerCase()] || KeyCode.Unknown; - } -} - -const uiMap = new KeyCodeStrMap(); -const userSettingsUSMap = new KeyCodeStrMap(); -const userSettingsGeneralMap = new KeyCodeStrMap(); -export const EVENT_KEY_CODE_MAP: { [keyCode: number]: KeyCode } = new Array(230); -export const NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE: { [nativeKeyCode: string]: KeyCode } = {}; -const scanCodeIntToStr: string[] = []; -const scanCodeStrToInt: { [code: string]: number } = Object.create(null); -const scanCodeLowerCaseStrToInt: { [code: string]: number } = Object.create(null); - -export const ScanCodeUtils = { - lowerCaseToEnum: (scanCode: string) => scanCodeLowerCaseStrToInt[scanCode] || ScanCode.None, - toEnum: (scanCode: string) => scanCodeStrToInt[scanCode] || ScanCode.None, - toString: (scanCode: ScanCode) => scanCodeIntToStr[scanCode] || 'None', -}; - -/** - * -1 if a ScanCode => KeyCode mapping depends on kb layout. - */ -export const IMMUTABLE_CODE_TO_KEY_CODE: KeyCode[] = []; - -/** - * -1 if a KeyCode => ScanCode mapping depends on kb layout. - */ -export const IMMUTABLE_KEY_CODE_TO_CODE: ScanCode[] = []; - -for (let i = 0; i <= ScanCode.MAX_VALUE; i++) { - IMMUTABLE_CODE_TO_KEY_CODE[i] = KeyCode.DependsOnKbLayout; -} - -for (let i = 0; i <= KeyCode.MAX_VALUE; i++) { - IMMUTABLE_KEY_CODE_TO_CODE[i] = ScanCode.DependsOnKbLayout; -} - -(function () { - // See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx - // See https://github.com/microsoft/node-native-keymap/blob/88c0b0e5/deps/chromium/keyboard_codes_win.h - - const empty = ''; - type IMappingEntry = [0 | 1, ScanCode, string, KeyCode, string, number, string, string, string]; - const mappings: IMappingEntry[] = [ - // immutable, scanCode, scanCodeStr, keyCode, keyCodeStr, eventKeyCode, vkey, usUserSettingsLabel, generalUserSettingsLabel - [1, ScanCode.None, 'None', KeyCode.Unknown, 'unknown', 0, 'VK_UNKNOWN', empty, empty], - [1, ScanCode.Hyper, 'Hyper', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Super, 'Super', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Fn, 'Fn', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.FnLock, 'FnLock', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Suspend, 'Suspend', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Resume, 'Resume', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Turbo, 'Turbo', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Sleep, 'Sleep', KeyCode.Unknown, empty, 0, 'VK_SLEEP', empty, empty], - [1, ScanCode.WakeUp, 'WakeUp', KeyCode.Unknown, empty, 0, empty, empty, empty], - [0, ScanCode.KeyA, 'KeyA', KeyCode.KeyA, 'A', 65, 'VK_A', empty, empty], - [0, ScanCode.KeyB, 'KeyB', KeyCode.KeyB, 'B', 66, 'VK_B', empty, empty], - [0, ScanCode.KeyC, 'KeyC', KeyCode.KeyC, 'C', 67, 'VK_C', empty, empty], - [0, ScanCode.KeyD, 'KeyD', KeyCode.KeyD, 'D', 68, 'VK_D', empty, empty], - [0, ScanCode.KeyE, 'KeyE', KeyCode.KeyE, 'E', 69, 'VK_E', empty, empty], - [0, ScanCode.KeyF, 'KeyF', KeyCode.KeyF, 'F', 70, 'VK_F', empty, empty], - [0, ScanCode.KeyG, 'KeyG', KeyCode.KeyG, 'G', 71, 'VK_G', empty, empty], - [0, ScanCode.KeyH, 'KeyH', KeyCode.KeyH, 'H', 72, 'VK_H', empty, empty], - [0, ScanCode.KeyI, 'KeyI', KeyCode.KeyI, 'I', 73, 'VK_I', empty, empty], - [0, ScanCode.KeyJ, 'KeyJ', KeyCode.KeyJ, 'J', 74, 'VK_J', empty, empty], - [0, ScanCode.KeyK, 'KeyK', KeyCode.KeyK, 'K', 75, 'VK_K', empty, empty], - [0, ScanCode.KeyL, 'KeyL', KeyCode.KeyL, 'L', 76, 'VK_L', empty, empty], - [0, ScanCode.KeyM, 'KeyM', KeyCode.KeyM, 'M', 77, 'VK_M', empty, empty], - [0, ScanCode.KeyN, 'KeyN', KeyCode.KeyN, 'N', 78, 'VK_N', empty, empty], - [0, ScanCode.KeyO, 'KeyO', KeyCode.KeyO, 'O', 79, 'VK_O', empty, empty], - [0, ScanCode.KeyP, 'KeyP', KeyCode.KeyP, 'P', 80, 'VK_P', empty, empty], - [0, ScanCode.KeyQ, 'KeyQ', KeyCode.KeyQ, 'Q', 81, 'VK_Q', empty, empty], - [0, ScanCode.KeyR, 'KeyR', KeyCode.KeyR, 'R', 82, 'VK_R', empty, empty], - [0, ScanCode.KeyS, 'KeyS', KeyCode.KeyS, 'S', 83, 'VK_S', empty, empty], - [0, ScanCode.KeyT, 'KeyT', KeyCode.KeyT, 'T', 84, 'VK_T', empty, empty], - [0, ScanCode.KeyU, 'KeyU', KeyCode.KeyU, 'U', 85, 'VK_U', empty, empty], - [0, ScanCode.KeyV, 'KeyV', KeyCode.KeyV, 'V', 86, 'VK_V', empty, empty], - [0, ScanCode.KeyW, 'KeyW', KeyCode.KeyW, 'W', 87, 'VK_W', empty, empty], - [0, ScanCode.KeyX, 'KeyX', KeyCode.KeyX, 'X', 88, 'VK_X', empty, empty], - [0, ScanCode.KeyY, 'KeyY', KeyCode.KeyY, 'Y', 89, 'VK_Y', empty, empty], - [0, ScanCode.KeyZ, 'KeyZ', KeyCode.KeyZ, 'Z', 90, 'VK_Z', empty, empty], - [0, ScanCode.Digit1, 'Digit1', KeyCode.Digit1, '1', 49, 'VK_1', empty, empty], - [0, ScanCode.Digit2, 'Digit2', KeyCode.Digit2, '2', 50, 'VK_2', empty, empty], - [0, ScanCode.Digit3, 'Digit3', KeyCode.Digit3, '3', 51, 'VK_3', empty, empty], - [0, ScanCode.Digit4, 'Digit4', KeyCode.Digit4, '4', 52, 'VK_4', empty, empty], - [0, ScanCode.Digit5, 'Digit5', KeyCode.Digit5, '5', 53, 'VK_5', empty, empty], - [0, ScanCode.Digit6, 'Digit6', KeyCode.Digit6, '6', 54, 'VK_6', empty, empty], - [0, ScanCode.Digit7, 'Digit7', KeyCode.Digit7, '7', 55, 'VK_7', empty, empty], - [0, ScanCode.Digit8, 'Digit8', KeyCode.Digit8, '8', 56, 'VK_8', empty, empty], - [0, ScanCode.Digit9, 'Digit9', KeyCode.Digit9, '9', 57, 'VK_9', empty, empty], - [0, ScanCode.Digit0, 'Digit0', KeyCode.Digit0, '0', 48, 'VK_0', empty, empty], - [1, ScanCode.Enter, 'Enter', KeyCode.Enter, 'Enter', 13, 'VK_RETURN', empty, empty], - [1, ScanCode.Escape, 'Escape', KeyCode.Escape, 'Escape', 27, 'VK_ESCAPE', empty, empty], - [ - 1, - ScanCode.Backspace, - 'Backspace', - KeyCode.Backspace, - 'Backspace', - 8, - 'VK_BACK', - empty, - empty, - ], - [1, ScanCode.Tab, 'Tab', KeyCode.Tab, 'Tab', 9, 'VK_TAB', empty, empty], - [1, ScanCode.Space, 'Space', KeyCode.Space, 'Space', 32, 'VK_SPACE', empty, empty], - [0, ScanCode.Minus, 'Minus', KeyCode.Minus, '-', 189, 'VK_OEM_MINUS', '-', 'OEM_MINUS'], - [0, ScanCode.Equal, 'Equal', KeyCode.Equal, '=', 187, 'VK_OEM_PLUS', '=', 'OEM_PLUS'], - [ - 0, - ScanCode.BracketLeft, - 'BracketLeft', - KeyCode.BracketLeft, - '[', - 219, - 'VK_OEM_4', - '[', - 'OEM_4', - ], - [ - 0, - ScanCode.BracketRight, - 'BracketRight', - KeyCode.BracketRight, - ']', - 221, - 'VK_OEM_6', - ']', - 'OEM_6', - ], - [0, ScanCode.Backslash, 'Backslash', KeyCode.Backslash, '\\', 220, 'VK_OEM_5', '\\', 'OEM_5'], - [0, ScanCode.IntlHash, 'IntlHash', KeyCode.Unknown, empty, 0, empty, empty, empty], // has been dropped from the w3c spec - [0, ScanCode.Semicolon, 'Semicolon', KeyCode.Semicolon, ';', 186, 'VK_OEM_1', ';', 'OEM_1'], - [0, ScanCode.Quote, 'Quote', KeyCode.Quote, "'", 222, 'VK_OEM_7', "'", 'OEM_7'], - [0, ScanCode.Backquote, 'Backquote', KeyCode.Backquote, '`', 192, 'VK_OEM_3', '`', 'OEM_3'], - [0, ScanCode.Comma, 'Comma', KeyCode.Comma, ',', 188, 'VK_OEM_COMMA', ',', 'OEM_COMMA'], - [0, ScanCode.Period, 'Period', KeyCode.Period, '.', 190, 'VK_OEM_PERIOD', '.', 'OEM_PERIOD'], - [0, ScanCode.Slash, 'Slash', KeyCode.Slash, '/', 191, 'VK_OEM_2', '/', 'OEM_2'], - [ - 1, - ScanCode.CapsLock, - 'CapsLock', - KeyCode.CapsLock, - 'CapsLock', - 20, - 'VK_CAPITAL', - empty, - empty, - ], - [1, ScanCode.F1, 'F1', KeyCode.F1, 'F1', 112, 'VK_F1', empty, empty], - [1, ScanCode.F2, 'F2', KeyCode.F2, 'F2', 113, 'VK_F2', empty, empty], - [1, ScanCode.F3, 'F3', KeyCode.F3, 'F3', 114, 'VK_F3', empty, empty], - [1, ScanCode.F4, 'F4', KeyCode.F4, 'F4', 115, 'VK_F4', empty, empty], - [1, ScanCode.F5, 'F5', KeyCode.F5, 'F5', 116, 'VK_F5', empty, empty], - [1, ScanCode.F6, 'F6', KeyCode.F6, 'F6', 117, 'VK_F6', empty, empty], - [1, ScanCode.F7, 'F7', KeyCode.F7, 'F7', 118, 'VK_F7', empty, empty], - [1, ScanCode.F8, 'F8', KeyCode.F8, 'F8', 119, 'VK_F8', empty, empty], - [1, ScanCode.F9, 'F9', KeyCode.F9, 'F9', 120, 'VK_F9', empty, empty], - [1, ScanCode.F10, 'F10', KeyCode.F10, 'F10', 121, 'VK_F10', empty, empty], - [1, ScanCode.F11, 'F11', KeyCode.F11, 'F11', 122, 'VK_F11', empty, empty], - [1, ScanCode.F12, 'F12', KeyCode.F12, 'F12', 123, 'VK_F12', empty, empty], - [1, ScanCode.PrintScreen, 'PrintScreen', KeyCode.Unknown, empty, 0, empty, empty, empty], - [ - 1, - ScanCode.ScrollLock, - 'ScrollLock', - KeyCode.ScrollLock, - 'ScrollLock', - 145, - 'VK_SCROLL', - empty, - empty, - ], - [1, ScanCode.Pause, 'Pause', KeyCode.PauseBreak, 'PauseBreak', 19, 'VK_PAUSE', empty, empty], - [1, ScanCode.Insert, 'Insert', KeyCode.Insert, 'Insert', 45, 'VK_INSERT', empty, empty], - [1, ScanCode.Home, 'Home', KeyCode.Home, 'Home', 36, 'VK_HOME', empty, empty], - [1, ScanCode.PageUp, 'PageUp', KeyCode.PageUp, 'PageUp', 33, 'VK_PRIOR', empty, empty], - [1, ScanCode.Delete, 'Delete', KeyCode.Delete, 'Delete', 46, 'VK_DELETE', empty, empty], - [1, ScanCode.End, 'End', KeyCode.End, 'End', 35, 'VK_END', empty, empty], - [1, ScanCode.PageDown, 'PageDown', KeyCode.PageDown, 'PageDown', 34, 'VK_NEXT', empty, empty], - [ - 1, - ScanCode.ArrowRight, - 'ArrowRight', - KeyCode.RightArrow, - 'RightArrow', - 39, - 'VK_RIGHT', - 'Right', - empty, - ], - [ - 1, - ScanCode.ArrowLeft, - 'ArrowLeft', - KeyCode.LeftArrow, - 'LeftArrow', - 37, - 'VK_LEFT', - 'Left', - empty, - ], - [ - 1, - ScanCode.ArrowDown, - 'ArrowDown', - KeyCode.DownArrow, - 'DownArrow', - 40, - 'VK_DOWN', - 'Down', - empty, - ], - [1, ScanCode.ArrowUp, 'ArrowUp', KeyCode.UpArrow, 'UpArrow', 38, 'VK_UP', 'Up', empty], - [1, ScanCode.NumLock, 'NumLock', KeyCode.NumLock, 'NumLock', 144, 'VK_NUMLOCK', empty, empty], - [ - 1, - ScanCode.NumpadDivide, - 'NumpadDivide', - KeyCode.NumpadDivide, - 'NumPad_Divide', - 111, - 'VK_DIVIDE', - empty, - empty, - ], - [ - 1, - ScanCode.NumpadMultiply, - 'NumpadMultiply', - KeyCode.NumpadMultiply, - 'NumPad_Multiply', - 106, - 'VK_MULTIPLY', - empty, - empty, - ], - [ - 1, - ScanCode.NumpadSubtract, - 'NumpadSubtract', - KeyCode.NumpadSubtract, - 'NumPad_Subtract', - 109, - 'VK_SUBTRACT', - empty, - empty, - ], - [ - 1, - ScanCode.NumpadAdd, - 'NumpadAdd', - KeyCode.NumpadAdd, - 'NumPad_Add', - 107, - 'VK_ADD', - empty, - empty, - ], - [1, ScanCode.NumpadEnter, 'NumpadEnter', KeyCode.Enter, empty, 0, empty, empty, empty], - [1, ScanCode.Numpad1, 'Numpad1', KeyCode.Numpad1, 'NumPad1', 97, 'VK_NUMPAD1', empty, empty], - [1, ScanCode.Numpad2, 'Numpad2', KeyCode.Numpad2, 'NumPad2', 98, 'VK_NUMPAD2', empty, empty], - [1, ScanCode.Numpad3, 'Numpad3', KeyCode.Numpad3, 'NumPad3', 99, 'VK_NUMPAD3', empty, empty], - [1, ScanCode.Numpad4, 'Numpad4', KeyCode.Numpad4, 'NumPad4', 100, 'VK_NUMPAD4', empty, empty], - [1, ScanCode.Numpad5, 'Numpad5', KeyCode.Numpad5, 'NumPad5', 101, 'VK_NUMPAD5', empty, empty], - [1, ScanCode.Numpad6, 'Numpad6', KeyCode.Numpad6, 'NumPad6', 102, 'VK_NUMPAD6', empty, empty], - [1, ScanCode.Numpad7, 'Numpad7', KeyCode.Numpad7, 'NumPad7', 103, 'VK_NUMPAD7', empty, empty], - [1, ScanCode.Numpad8, 'Numpad8', KeyCode.Numpad8, 'NumPad8', 104, 'VK_NUMPAD8', empty, empty], - [1, ScanCode.Numpad9, 'Numpad9', KeyCode.Numpad9, 'NumPad9', 105, 'VK_NUMPAD9', empty, empty], - [1, ScanCode.Numpad0, 'Numpad0', KeyCode.Numpad0, 'NumPad0', 96, 'VK_NUMPAD0', empty, empty], - [ - 1, - ScanCode.NumpadDecimal, - 'NumpadDecimal', - KeyCode.NumpadDecimal, - 'NumPad_Decimal', - 110, - 'VK_DECIMAL', - empty, - empty, - ], - [ - 0, - ScanCode.IntlBackslash, - 'IntlBackslash', - KeyCode.IntlBackslash, - 'OEM_102', - 226, - 'VK_OEM_102', - empty, - empty, - ], - [ - 1, - ScanCode.ContextMenu, - 'ContextMenu', - KeyCode.ContextMenu, - 'ContextMenu', - 93, - empty, - empty, - empty, - ], - [1, ScanCode.Power, 'Power', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.NumpadEqual, 'NumpadEqual', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.F13, 'F13', KeyCode.F13, 'F13', 124, 'VK_F13', empty, empty], - [1, ScanCode.F14, 'F14', KeyCode.F14, 'F14', 125, 'VK_F14', empty, empty], - [1, ScanCode.F15, 'F15', KeyCode.F15, 'F15', 126, 'VK_F15', empty, empty], - [1, ScanCode.F16, 'F16', KeyCode.F16, 'F16', 127, 'VK_F16', empty, empty], - [1, ScanCode.F17, 'F17', KeyCode.F17, 'F17', 128, 'VK_F17', empty, empty], - [1, ScanCode.F18, 'F18', KeyCode.F18, 'F18', 129, 'VK_F18', empty, empty], - [1, ScanCode.F19, 'F19', KeyCode.F19, 'F19', 130, 'VK_F19', empty, empty], - [1, ScanCode.F20, 'F20', KeyCode.F20, 'F20', 131, 'VK_F20', empty, empty], - [1, ScanCode.F21, 'F21', KeyCode.F21, 'F21', 132, 'VK_F21', empty, empty], - [1, ScanCode.F22, 'F22', KeyCode.F22, 'F22', 133, 'VK_F22', empty, empty], - [1, ScanCode.F23, 'F23', KeyCode.F23, 'F23', 134, 'VK_F23', empty, empty], - [1, ScanCode.F24, 'F24', KeyCode.F24, 'F24', 135, 'VK_F24', empty, empty], - [1, ScanCode.Open, 'Open', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Help, 'Help', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Select, 'Select', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Again, 'Again', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Undo, 'Undo', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Cut, 'Cut', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Copy, 'Copy', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Paste, 'Paste', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Find, 'Find', KeyCode.Unknown, empty, 0, empty, empty, empty], - [ - 1, - ScanCode.AudioVolumeMute, - 'AudioVolumeMute', - KeyCode.AudioVolumeMute, - 'AudioVolumeMute', - 173, - 'VK_VOLUME_MUTE', - empty, - empty, - ], - [ - 1, - ScanCode.AudioVolumeUp, - 'AudioVolumeUp', - KeyCode.AudioVolumeUp, - 'AudioVolumeUp', - 175, - 'VK_VOLUME_UP', - empty, - empty, - ], - [ - 1, - ScanCode.AudioVolumeDown, - 'AudioVolumeDown', - KeyCode.AudioVolumeDown, - 'AudioVolumeDown', - 174, - 'VK_VOLUME_DOWN', - empty, - empty, - ], - [ - 1, - ScanCode.NumpadComma, - 'NumpadComma', - KeyCode.NUMPAD_SEPARATOR, - 'NumPad_Separator', - 108, - 'VK_SEPARATOR', - empty, - empty, - ], - [0, ScanCode.IntlRo, 'IntlRo', KeyCode.ABNT_C1, 'ABNT_C1', 193, 'VK_ABNT_C1', empty, empty], - [1, ScanCode.KanaMode, 'KanaMode', KeyCode.Unknown, empty, 0, empty, empty, empty], - [0, ScanCode.IntlYen, 'IntlYen', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Convert, 'Convert', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.NonConvert, 'NonConvert', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Lang1, 'Lang1', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Lang2, 'Lang2', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Lang3, 'Lang3', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Lang4, 'Lang4', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Lang5, 'Lang5', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Abort, 'Abort', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.Props, 'Props', KeyCode.Unknown, empty, 0, empty, empty, empty], - [ - 1, - ScanCode.NumpadParenLeft, - 'NumpadParenLeft', - KeyCode.Unknown, - empty, - 0, - empty, - empty, - empty, - ], - [ - 1, - ScanCode.NumpadParenRight, - 'NumpadParenRight', - KeyCode.Unknown, - empty, - 0, - empty, - empty, - empty, - ], - [ - 1, - ScanCode.NumpadBackspace, - 'NumpadBackspace', - KeyCode.Unknown, - empty, - 0, - empty, - empty, - empty, - ], - [ - 1, - ScanCode.NumpadMemoryStore, - 'NumpadMemoryStore', - KeyCode.Unknown, - empty, - 0, - empty, - empty, - empty, - ], - [ - 1, - ScanCode.NumpadMemoryRecall, - 'NumpadMemoryRecall', - KeyCode.Unknown, - empty, - 0, - empty, - empty, - empty, - ], - [ - 1, - ScanCode.NumpadMemoryClear, - 'NumpadMemoryClear', - KeyCode.Unknown, - empty, - 0, - empty, - empty, - empty, - ], - [ - 1, - ScanCode.NumpadMemoryAdd, - 'NumpadMemoryAdd', - KeyCode.Unknown, - empty, - 0, - empty, - empty, - empty, - ], - [ - 1, - ScanCode.NumpadMemorySubtract, - 'NumpadMemorySubtract', - KeyCode.Unknown, - empty, - 0, - empty, - empty, - empty, - ], - [1, ScanCode.NumpadClear, 'NumpadClear', KeyCode.Clear, 'Clear', 12, 'VK_CLEAR', empty, empty], - [ - 1, - ScanCode.NumpadClearEntry, - 'NumpadClearEntry', - KeyCode.Unknown, - empty, - 0, - empty, - empty, - empty, - ], - [1, ScanCode.None, empty, KeyCode.Ctrl, 'Ctrl', 17, 'VK_CONTROL', empty, empty], - [1, ScanCode.None, empty, KeyCode.Shift, 'Shift', 16, 'VK_SHIFT', empty, empty], - [1, ScanCode.None, empty, KeyCode.Alt, 'Alt', 18, 'VK_MENU', empty, empty], - [1, ScanCode.None, empty, KeyCode.Meta, 'Meta', 91, 'VK_COMMAND', empty, empty], - [1, ScanCode.ControlLeft, 'ControlLeft', KeyCode.Ctrl, empty, 0, 'VK_LCONTROL', empty, empty], - [1, ScanCode.ShiftLeft, 'ShiftLeft', KeyCode.Shift, empty, 0, 'VK_LSHIFT', empty, empty], - [1, ScanCode.AltLeft, 'AltLeft', KeyCode.Alt, empty, 0, 'VK_LMENU', empty, empty], - [1, ScanCode.MetaLeft, 'MetaLeft', KeyCode.Meta, empty, 0, 'VK_LWIN', empty, empty], - [1, ScanCode.ControlRight, 'ControlRight', KeyCode.Ctrl, empty, 0, 'VK_RCONTROL', empty, empty], - [1, ScanCode.ShiftRight, 'ShiftRight', KeyCode.Shift, empty, 0, 'VK_RSHIFT', empty, empty], - [1, ScanCode.AltRight, 'AltRight', KeyCode.Alt, empty, 0, 'VK_RMENU', empty, empty], - [1, ScanCode.MetaRight, 'MetaRight', KeyCode.Meta, empty, 0, 'VK_RWIN', empty, empty], - [1, ScanCode.BrightnessUp, 'BrightnessUp', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.BrightnessDown, 'BrightnessDown', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.MediaPlay, 'MediaPlay', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.MediaRecord, 'MediaRecord', KeyCode.Unknown, empty, 0, empty, empty, empty], - [ - 1, - ScanCode.MediaFastForward, - 'MediaFastForward', - KeyCode.Unknown, - empty, - 0, - empty, - empty, - empty, - ], - [1, ScanCode.MediaRewind, 'MediaRewind', KeyCode.Unknown, empty, 0, empty, empty, empty], - [ - 1, - ScanCode.MediaTrackNext, - 'MediaTrackNext', - KeyCode.MediaTrackNext, - 'MediaTrackNext', - 176, - 'VK_MEDIA_NEXT_TRACK', - empty, - empty, - ], - [ - 1, - ScanCode.MediaTrackPrevious, - 'MediaTrackPrevious', - KeyCode.MediaTrackPrevious, - 'MediaTrackPrevious', - 177, - 'VK_MEDIA_PREV_TRACK', - empty, - empty, - ], - [ - 1, - ScanCode.MediaStop, - 'MediaStop', - KeyCode.MediaStop, - 'MediaStop', - 178, - 'VK_MEDIA_STOP', - empty, - empty, - ], - [1, ScanCode.Eject, 'Eject', KeyCode.Unknown, empty, 0, empty, empty, empty], - [ - 1, - ScanCode.MediaPlayPause, - 'MediaPlayPause', - KeyCode.MediaPlayPause, - 'MediaPlayPause', - 179, - 'VK_MEDIA_PLAY_PAUSE', - empty, - empty, - ], - [ - 1, - ScanCode.MediaSelect, - 'MediaSelect', - KeyCode.LaunchMediaPlayer, - 'LaunchMediaPlayer', - 181, - 'VK_MEDIA_LAUNCH_MEDIA_SELECT', - empty, - empty, - ], - [ - 1, - ScanCode.LaunchMail, - 'LaunchMail', - KeyCode.LaunchMail, - 'LaunchMail', - 180, - 'VK_MEDIA_LAUNCH_MAIL', - empty, - empty, - ], - [ - 1, - ScanCode.LaunchApp2, - 'LaunchApp2', - KeyCode.LaunchApp2, - 'LaunchApp2', - 183, - 'VK_MEDIA_LAUNCH_APP2', - empty, - empty, - ], - [ - 1, - ScanCode.LaunchApp1, - 'LaunchApp1', - KeyCode.Unknown, - empty, - 0, - 'VK_MEDIA_LAUNCH_APP1', - empty, - empty, - ], - [1, ScanCode.SelectTask, 'SelectTask', KeyCode.Unknown, empty, 0, empty, empty, empty], - [ - 1, - ScanCode.LaunchScreenSaver, - 'LaunchScreenSaver', - KeyCode.Unknown, - empty, - 0, - empty, - empty, - empty, - ], - [ - 1, - ScanCode.BrowserSearch, - 'BrowserSearch', - KeyCode.BrowserSearch, - 'BrowserSearch', - 170, - 'VK_BROWSER_SEARCH', - empty, - empty, - ], - [ - 1, - ScanCode.BrowserHome, - 'BrowserHome', - KeyCode.BrowserHome, - 'BrowserHome', - 172, - 'VK_BROWSER_HOME', - empty, - empty, - ], - [ - 1, - ScanCode.BrowserBack, - 'BrowserBack', - KeyCode.BrowserBack, - 'BrowserBack', - 166, - 'VK_BROWSER_BACK', - empty, - empty, - ], - [ - 1, - ScanCode.BrowserForward, - 'BrowserForward', - KeyCode.BrowserForward, - 'BrowserForward', - 167, - 'VK_BROWSER_FORWARD', - empty, - empty, - ], - [ - 1, - ScanCode.BrowserStop, - 'BrowserStop', - KeyCode.Unknown, - empty, - 0, - 'VK_BROWSER_STOP', - empty, - empty, - ], - [ - 1, - ScanCode.BrowserRefresh, - 'BrowserRefresh', - KeyCode.Unknown, - empty, - 0, - 'VK_BROWSER_REFRESH', - empty, - empty, - ], - [ - 1, - ScanCode.BrowserFavorites, - 'BrowserFavorites', - KeyCode.Unknown, - empty, - 0, - 'VK_BROWSER_FAVORITES', - empty, - empty, - ], - [1, ScanCode.ZoomToggle, 'ZoomToggle', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.MailReply, 'MailReply', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.MailForward, 'MailForward', KeyCode.Unknown, empty, 0, empty, empty, empty], - [1, ScanCode.MailSend, 'MailSend', KeyCode.Unknown, empty, 0, empty, empty, empty], - - // See https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html - // If an Input Method Editor is processing key input and the event is keydown, return 229. - [ - 1, - ScanCode.None, - empty, - KeyCode.KEY_IN_COMPOSITION, - 'KeyInComposition', - 229, - empty, - empty, - empty, - ], - [1, ScanCode.None, empty, KeyCode.ABNT_C2, 'ABNT_C2', 194, 'VK_ABNT_C2', empty, empty], - [1, ScanCode.None, empty, KeyCode.OEM_8, 'OEM_8', 223, 'VK_OEM_8', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_KANA', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_HANGUL', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_JUNJA', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_FINAL', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_HANJA', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_KANJI', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_CONVERT', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_NONCONVERT', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_ACCEPT', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_MODECHANGE', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_SELECT', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_PRINT', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_EXECUTE', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_SNAPSHOT', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_HELP', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_APPS', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_PROCESSKEY', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_PACKET', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_DBE_SBCSCHAR', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_DBE_DBCSCHAR', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_ATTN', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_CRSEL', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_EXSEL', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_EREOF', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_PLAY', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_ZOOM', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_NONAME', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_PA1', empty, empty], - [1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_OEM_CLEAR', empty, empty], - ]; - - const seenKeyCode: boolean[] = []; - const seenScanCode: boolean[] = []; - for (const mapping of mappings) { - const [ - immutable, - scanCode, - scanCodeStr, - keyCode, - keyCodeStr, - eventKeyCode, - vkey, - usUserSettingsLabel, - generalUserSettingsLabel, - ] = mapping; - if (!seenScanCode[scanCode]) { - seenScanCode[scanCode] = true; - scanCodeIntToStr[scanCode] = scanCodeStr; - scanCodeStrToInt[scanCodeStr] = scanCode; - scanCodeLowerCaseStrToInt[scanCodeStr.toLowerCase()] = scanCode; - if (immutable) { - IMMUTABLE_CODE_TO_KEY_CODE[scanCode] = keyCode; - if ( - keyCode !== KeyCode.Unknown && - keyCode !== KeyCode.Enter && - keyCode !== KeyCode.Ctrl && - keyCode !== KeyCode.Shift && - keyCode !== KeyCode.Alt && - keyCode !== KeyCode.Meta - ) { - IMMUTABLE_KEY_CODE_TO_CODE[keyCode] = scanCode; - } - } - } - if (!seenKeyCode[keyCode]) { - seenKeyCode[keyCode] = true; - if (!keyCodeStr) { - throw new Error( - `String representation missing for key code ${keyCode} around scan code ${scanCodeStr}`, - ); - } - uiMap.define(keyCode, keyCodeStr); - userSettingsUSMap.define(keyCode, usUserSettingsLabel || keyCodeStr); - userSettingsGeneralMap.define( - keyCode, - generalUserSettingsLabel || usUserSettingsLabel || keyCodeStr, - ); - } - if (eventKeyCode) { - EVENT_KEY_CODE_MAP[eventKeyCode] = keyCode; - } - if (vkey) { - NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE[vkey] = keyCode; - } - } - // Manually added due to the exclusion above (due to duplication with NumpadEnter) - IMMUTABLE_KEY_CODE_TO_CODE[KeyCode.Enter] = ScanCode.Enter; -})(); - -export function keyCodeToString(keyCode: KeyCode): string { - return uiMap.keyCodeToStr(keyCode); -} -export function keyCodeFromString(key: string): KeyCode { - return uiMap.strToKeyCode(key); -} - -export function keyCodeToUserSettingsUS(keyCode: KeyCode): string { - return userSettingsUSMap.keyCodeToStr(keyCode); -} -export function keyCodeToUserSettingsGeneral(keyCode: KeyCode): string { - return userSettingsGeneralMap.keyCodeToStr(keyCode); -} -export function keyCodeFromUserSettings(key: string): KeyCode { - return userSettingsUSMap.strToKeyCode(key) || userSettingsGeneralMap.strToKeyCode(key); -} - -export const enum KeyMod { - CtrlCmd = (1 << 11) >>> 0, - Shift = (1 << 10) >>> 0, - Alt = (1 << 9) >>> 0, - WinCtrl = (1 << 8) >>> 0, -} - -export function KeyChord(firstPart: number, secondPart: number): number { - const chordPart = ((secondPart & 0x0000ffff) << 16) >>> 0; - return (firstPart | chordPart) >>> 0; -} diff --git a/packages/engine-core/src/configuration/configurationRegistry.ts b/packages/engine-core/src/configuration/configurationRegistry.ts index 7570f5cb6..b81a5f01d 100644 --- a/packages/engine-core/src/configuration/configurationRegistry.ts +++ b/packages/engine-core/src/configuration/configurationRegistry.ts @@ -1,15 +1,8 @@ -import { - Events, - type StringDictionary, - type JSONSchemaType, - jsonTypes, - IJSONSchema, - types, - Disposable, -} from '@alilc/lowcode-shared'; +import { Events, type StringDictionary, jsonTypes, types, Disposable } from '@alilc/lowcode-shared'; import { isUndefined, isObject } from 'lodash-es'; import { Extensions, Registry } from '../extension/registry'; import { OVERRIDE_PROPERTY_REGEX, overrideIdentifiersFromKey } from './configuration'; +import { type IJSONSchema, type JSONSchemaType } from '../schema'; export interface IConfigurationRegistry { /** @@ -20,10 +13,7 @@ export interface IConfigurationRegistry { /** * Register multiple configurations to the registry. */ - registerConfigurations( - configurations: IConfigurationNode[], - validate?: boolean, - ): ReadonlySet; + registerConfigurations(configurations: IConfigurationNode[], validate?: boolean): ReadonlySet; /** * Deregister multiple configurations from the registry. @@ -101,7 +91,6 @@ export interface IConfigurationPropertySchema extends IJSONSchema { */ export interface IExtensionInfo { id: string; - displayName?: string; version?: string; } @@ -167,10 +156,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat this.registerConfigurations([configuration], validate); } - registerConfigurations( - configurations: IConfigurationNode[], - validate: boolean = true, - ): ReadonlySet { + registerConfigurations(configurations: IConfigurationNode[], validate: boolean = true): ReadonlySet { const properties = new Set(); this.doRegisterConfigurations(configurations, validate, properties); @@ -179,18 +165,9 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat return properties; } - private doRegisterConfigurations( - configurations: IConfigurationNode[], - validate: boolean, - bucket: Set, - ): void { + private doRegisterConfigurations(configurations: IConfigurationNode[], validate: boolean, bucket: Set): void { configurations.forEach((configuration) => { - this.validateAndRegisterProperties( - configuration, - validate, - configuration.extensionInfo, - bucket, - ); + this.validateAndRegisterProperties(configuration, validate, configuration.extensionInfo, bucket); this.registerJSONConfiguration(configuration); }); @@ -250,10 +227,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat return null; } - private updatePropertyDefaultValue( - key: string, - property: IRegisteredConfigurationPropertySchema, - ): void { + private updatePropertyDefaultValue(key: string, property: IRegisteredConfigurationPropertySchema): void { let defaultValue = undefined; let defaultSource = undefined; @@ -286,10 +260,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat this._onDidUpdateConfiguration.notify({ properties }); } - private doDeregisterConfigurations( - configurations: IConfigurationNode[], - bucket: Set, - ): void { + private doDeregisterConfigurations(configurations: IConfigurationNode[], bucket: Set): void { const deregisterConfiguration = (configuration: IConfigurationNode) => { if (configuration.properties) { for (const key of Object.keys(configuration.properties)) { @@ -313,10 +284,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat this._onDidUpdateConfiguration.notify({ properties, defaultsOverrides: true }); } - private doRegisterDefaultConfigurations( - configurationDefaults: IConfigurationDefaults[], - bucket: Set, - ) { + private doRegisterDefaultConfigurations(configurationDefaults: IConfigurationDefaults[], bucket: Set) { this.registeredConfigurationDefaults.push(...configurationDefaults); const overrideIdentifiers: string[] = []; @@ -327,9 +295,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat const configurationDefaultOverridesForKey = this.configurationDefaultsOverrides.get(key) ?? - this.configurationDefaultsOverrides - .set(key, { configurationDefaultOverrides: [] }) - .get(key)!; + this.configurationDefaultsOverrides.set(key, { configurationDefaultOverrides: [] }).get(key)!; const value = overrides[key]; configurationDefaultOverridesForKey.configurationDefaultOverrides.push({ value, source }); @@ -346,8 +312,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat continue; } - configurationDefaultOverridesForKey.configurationDefaultOverrideValue = - newDefaultOverride; + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = newDefaultOverride; this.updateDefaultOverrideProperty(key, newDefaultOverride, source); overrideIdentifiers.push(...overrideIdentifiersFromKey(key)); } @@ -364,8 +329,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat continue; } - configurationDefaultOverridesForKey.configurationDefaultOverrideValue = - newDefaultOverride; + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = newDefaultOverride; const property = this.configurationProperties[key]; if (property) { this.updatePropertyDefaultValue(key, property); @@ -453,8 +417,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat const isObjectSetting = isObject(value) && ((property !== undefined && property.type === 'object') || - (property === undefined && - (isUndefined(existingDefaultValue) || isObject(existingDefaultValue)))); + (property === undefined && (isUndefined(existingDefaultValue) || isObject(existingDefaultValue)))); // If the default value is an object, merge the objects and store the source of each keys if (isObjectSetting) { @@ -522,21 +485,16 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat // configuration override defaults - merges defaults for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) { - configurationDefaultOverrideValue = - this.mergeDefaultConfigurationsForOverrideIdentifier( - key, - configurationDefaultOverride.value, - configurationDefaultOverride.source, - configurationDefaultOverrideValue, - ); + configurationDefaultOverrideValue = this.mergeDefaultConfigurationsForOverrideIdentifier( + key, + configurationDefaultOverride.value, + configurationDefaultOverride.source, + configurationDefaultOverrideValue, + ); } - if ( - configurationDefaultOverrideValue && - !types.isEmptyObject(configurationDefaultOverrideValue.value) - ) { - configurationDefaultOverridesForKey.configurationDefaultOverrideValue = - configurationDefaultOverrideValue; + if (configurationDefaultOverrideValue && !types.isEmptyObject(configurationDefaultOverrideValue.value)) { + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = configurationDefaultOverrideValue; this.updateDefaultOverrideProperty(key, configurationDefaultOverrideValue, source); } else { this.configurationDefaultsOverrides.delete(key); @@ -547,17 +505,15 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat // configuration override defaults - merges defaults for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) { - configurationDefaultOverrideValue = - this.mergeDefaultConfigurationsForConfigurationProperty( - key, - configurationDefaultOverride.value, - configurationDefaultOverride.source, - configurationDefaultOverrideValue, - ); + configurationDefaultOverrideValue = this.mergeDefaultConfigurationsForConfigurationProperty( + key, + configurationDefaultOverride.value, + configurationDefaultOverride.source, + configurationDefaultOverrideValue, + ); } - configurationDefaultOverridesForKey.configurationDefaultOverrideValue = - configurationDefaultOverrideValue; + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = configurationDefaultOverrideValue; const property = this.configurationProperties[key]; if (property) { diff --git a/packages/engine-core/src/contentEditor/contentEditor.ts b/packages/engine-core/src/contentEditor/contentEditor.ts new file mode 100644 index 000000000..a2195ad33 --- /dev/null +++ b/packages/engine-core/src/contentEditor/contentEditor.ts @@ -0,0 +1,9 @@ +export interface IContentEditor { + load(content: string): Promise; + + export(): Promise; + + close(): void; + + send(channel: string, ...args: any[]): void; +} diff --git a/packages/engine-core/src/contentEditor/contentEditorRegistry.ts b/packages/engine-core/src/contentEditor/contentEditorRegistry.ts new file mode 100644 index 000000000..b17552078 --- /dev/null +++ b/packages/engine-core/src/contentEditor/contentEditorRegistry.ts @@ -0,0 +1,48 @@ +import { toDisposable, type IDisposable } from '@alilc/lowcode-shared'; +import { IContentEditor } from './contentEditor'; +import { Registry, Extensions } from '../extension/registry'; + +export interface IContentEditorRegistry { + registerContentEditor(contentType: string, windowContent: IContentEditor, options?: IRegisterOptions): IDisposable; + + getContentEditor(contentType: string): IContentEditor | undefined; + + getContentTypeByExt(ext: string): string | undefined; +} + +export interface IRegisterOptions { + ext?: string; +} + +class ContentEditorRegistryImpl implements IContentEditorRegistry { + private readonly _contentEditors = new Map(); + private readonly _mapExtToType = new Map(); + + registerContentEditor( + contentType: string, + contentEditor: IContentEditor, + options: IRegisterOptions = {}, + ): IDisposable { + const { ext = contentType } = options; + + this._contentEditors.set(contentType, contentEditor); + this._mapExtToType.set(ext, contentType); + + return toDisposable(() => { + this._contentEditors.delete(contentType); + this._mapExtToType.delete(contentType); + }); + } + + getContentEditor(contentType: string): IContentEditor | undefined { + return this._contentEditors.get(contentType); + } + + getContentTypeByExt(ext: string): string | undefined { + return this._mapExtToType.get(ext); + } +} + +export const ContentEditorRegistry = new ContentEditorRegistryImpl(); + +Registry.add(Extensions.ContentEditor, ContentEditorRegistry); diff --git a/packages/engine-core/src/extension/extension.ts b/packages/engine-core/src/extension/extension.ts index b22e6ec69..89de7902a 100644 --- a/packages/engine-core/src/extension/extension.ts +++ b/packages/engine-core/src/extension/extension.ts @@ -1,14 +1,13 @@ import { StringDictionary } from '@alilc/lowcode-shared'; import { IConfigurationNode } from '../configuration'; -export type ExtensionInitializer = (ctx: Context) => IExtensionInstance; +export type ExtensionStarter = (ctx: Context) => IExtensionInstance; /** * 函数声明插件 */ -export interface IFunctionExtension extends ExtensionInitializer { - id: string; - displayName?: string; +export interface IFunctionExtension extends ExtensionStarter { + id?: string; version: string; metadata?: IExtensionMetadata; } diff --git a/packages/engine-core/src/extension/extensionHost.ts b/packages/engine-core/src/extension/extensionHost.ts index 514306655..0390f4685 100644 --- a/packages/engine-core/src/extension/extensionHost.ts +++ b/packages/engine-core/src/extension/extensionHost.ts @@ -1,59 +1,59 @@ -import { ConfigurationRegistry, type IConfigurationNode } from '../configuration'; -import { type ExtensionInitializer, type IExtensionInstance } from './extension'; -import { invariant } from '@alilc/lowcode-shared'; +import { type ExtensionStarter, type IExtensionInstance } from './extension'; export type ExtensionExportsAccessor = { [key: string]: any; }; export class ExtensionHost { - private isInited = false; + private _isInited = false; - private instance: IExtensionInstance; + private _instance: IExtensionInstance; - private configurationProperties: ReadonlySet; + private _exports: ExtensionExportsAccessor | undefined; + get exports(): ExtensionExportsAccessor | undefined { + if (!this._isInited) return; - constructor( - public name: string, - initializer: ExtensionInitializer, - preferenceConfigurations: IConfigurationNode[], - ) { - this.configurationProperties = - ConfigurationRegistry.registerConfigurations(preferenceConfigurations); + if (!this._exports) { + const exports = this._instance.exports?.(); - this.instance = initializer({}); - } + if (!exports) return; - async init(): Promise { - if (this.isInited) return; + this._exports = new Proxy(Object.create(null), { + get(target, prop, receiver) { + if (Reflect.has(exports, prop)) { + return exports?.[prop as string]; + } + return Reflect.get(target, prop, receiver); + }, + }); + } - await this.instance.init(); - - this.isInited = true; + return this._exports; } - async destroy(): Promise { - if (!this.isInited) return; - - await this.instance.destroy(); + constructor( + public id: string, + starter: ExtensionStarter, + ) { + // context will be provide in + this._instance = starter({}); + } - this.isInited = false; + dispose(): void { + this.destroy(); } - toProxy(): ExtensionExportsAccessor | undefined { - invariant(this.isInited, 'Could not call toProxy before init'); + async init(): Promise { + if (this._isInited) return; - const exports = this.instance.exports?.(); + await this._instance.init(); + this._isInited = true; + } - if (!exports) return; + async destroy(): Promise { + if (!this._isInited) return; - return new Proxy(Object.create(null), { - get(target, prop, receiver) { - if (Reflect.has(exports, prop)) { - return exports?.[prop as string]; - } - return Reflect.get(target, prop, receiver); - }, - }); + await this._instance.destroy(); + this._isInited = false; } } diff --git a/packages/engine-core/src/extension/extensionManagement.ts b/packages/engine-core/src/extension/extensionManagement.ts deleted file mode 100644 index 790aab68c..000000000 --- a/packages/engine-core/src/extension/extensionManagement.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { type Reference } from '@alilc/lowcode-shared'; -import { type IFunctionExtension } from './extension'; -import { type IConfigurationNode } from '../configuration'; -import { ExtensionHost } from './extensionHost'; - -export interface IExtensionGallery { - name: string; - version: string; - reference: Reference | undefined; - dependencies: string[] | undefined; - engineVerison: string | undefined; - preferenceConfigurations: IConfigurationNode[] | undefined; -} - -export interface IExtensionRegisterOptions { - /** - * Will enable plugin registered with auto-initialization immediately - * other than plugin-manager init all plugins at certain time. - * It is helpful when plugin register is later than plugin-manager initialization. - */ - autoInit?: boolean; - /** - * allow overriding existing plugin with same name when override === true - */ - override?: boolean; -} - -export class ExtensionManagement { - private extensionGalleryMap: Map = new Map(); - private extensionHosts: Map = new Map(); - - constructor() {} - - async register( - extension: IFunctionExtension, - { autoInit = false, override = false }: IExtensionRegisterOptions = {}, - ): Promise { - if (!this.validateExtension(extension, override)) return; - - const metadata = extension.metadata ?? {}; - const host = new ExtensionHost( - extension.name, - extension, - metadata.preferenceConfigurations ?? [], - ); - - if (autoInit) { - await host.init(); - } - - this.extensionHosts.set(extension.name, host); - - const gallery: IExtensionGallery = { - name: extension.name, - version: extension.version, - reference: undefined, - dependencies: metadata.dependencies, - engineVerison: metadata.engineVerison, - preferenceConfigurations: metadata.preferenceConfigurations, - }; - - this.extensionGalleryMap.set(gallery.name, gallery); - } - - private validateExtension(extension: IFunctionExtension, override: boolean): boolean { - if (!override && this.has(extension.name)) return false; - - return true; - } - - async deregister(name: string): Promise { - if (this.has(name)) { - const host = this.extensionHosts.get(name)!; - await host.destroy(); - - this.extensionGalleryMap.delete(name); - this.extensionHosts.delete(name); - } - } - - has(name: string): boolean { - return this.extensionGalleryMap.has(name); - } - - getExtensionGallery(name: string): IExtensionGallery | undefined { - return this.extensionGalleryMap.get(name); - } - - getExtensionHost(name: string): ExtensionHost | undefined { - return this.extensionHosts.get(name); - } -} diff --git a/packages/engine-core/src/extension/extensionManager.ts b/packages/engine-core/src/extension/extensionManager.ts new file mode 100644 index 000000000..504e9fe85 --- /dev/null +++ b/packages/engine-core/src/extension/extensionManager.ts @@ -0,0 +1,160 @@ +import { CyclicDependencyError, Disposable, Graph, type Reference } from '@alilc/lowcode-shared'; +import { type IFunctionExtension } from './extension'; +import { type IConfigurationNode, type IConfigurationRegistry } from '../configuration'; +import { ExtensionHost } from './extensionHost'; +import { Registry, Extensions } from './registry'; + +export interface IExtensionGallery { + id: string; + name: string; + version: string; + reference: Reference | undefined; + dependencies: string[] | undefined; + engineVerison: string | undefined; + preferenceConfigurations: IConfigurationNode[] | undefined; + + registerOptions: IExtensionRegisterOptions; +} + +export interface IExtensionRegisterOptions { + /** + * Will enable plugin registered with auto-initialization immediately + * other than plugin-manager init all plugins at certain time. + * It is helpful when plugin register is later than plugin-manager initialization. + */ + autoInit?: boolean; + /** + * allow overriding existing plugin with same name when override === true + */ + override?: boolean; +} + +export class ExtensionManager extends Disposable { + private _extensionGalleryMap: Map = new Map(); + private _extensionHosts: Map = new Map(); + private _extensionStore: Map = new Map(); + private _extensionDependencyGraph = new Graph((name) => name); + + constructor() { + super(); + } + + async register(extension: IFunctionExtension, options: IExtensionRegisterOptions = {}): Promise { + extension.id = extension.id ?? extension.name; + + if (!this._validateExtension(extension, options.override)) return; + + this._extensionStore.set(extension.id!, extension); + + const metadata = extension.metadata ?? {}; + const gallery: IExtensionGallery = { + id: extension.id!, + name: extension.name, + version: extension.version, + reference: undefined, + dependencies: metadata.dependencies, + engineVerison: metadata.engineVerison, + preferenceConfigurations: metadata.preferenceConfigurations?.map((config) => { + return { + ...config, + extensionInfo: { + id: extension.id!, + version: extension.version, + }, + }; + }), + + registerOptions: options, + }; + + this._extensionGalleryMap.set(gallery.id, gallery); + + await this._installExtension(extension); + } + + private _validateExtension(extension: IFunctionExtension, override: boolean = false): boolean { + if (!override && this.has(extension.id!)) return false; + + return true; + } + + private async _installExtension(extension: IFunctionExtension): Promise { + const { dependencies = [] } = extension.metadata ?? {}; + + this._extensionDependencyGraph.lookupOrInsertNode(extension.id!); + + if (dependencies.length > 0) { + for (const dep of dependencies) { + this._extensionDependencyGraph.insertEdge(extension.id!, dep); + } + } + + while (true) { + const roots = this._extensionDependencyGraph.roots(); + + if (roots.length === 0 || roots.every((node) => !this.isExtensionActivated(node.data))) { + if (this._extensionDependencyGraph.isEmpty()) { + throw new CyclicDependencyError(this._extensionDependencyGraph); + } + break; + } + + for (const { data } of roots) { + const extensionFunction = this.getExtension(data); + const gallery = this.getExtensionGallery(data); + + if (extensionFunction) { + const host = new ExtensionHost(data, extensionFunction); + + if (gallery!.preferenceConfigurations) { + Registry.as(Extensions.Configuration).registerConfigurations( + gallery!.preferenceConfigurations, + ); + } + + this._addDispose(host); + this._extensionHosts.set(extension.name, host); + this._extensionDependencyGraph.removeNode(extensionFunction.id!); + + try { + if (gallery!.registerOptions.autoInit) { + await host.init(); + } + } catch (e) { + console.log(`The extension [${data}] init failed: `, e); + } + } + } + } + } + + async deregister(id: string): Promise { + if (this.has(id)) { + const host = this._extensionHosts.get(id)!; + await host.destroy(); + + this._extensionGalleryMap.delete(id); + this._extensionHosts.delete(id); + } + } + + has(id: string): boolean { + return this._extensionGalleryMap.has(id); + } + + getExtension(id: string): IFunctionExtension | undefined { + return this._extensionStore.get(id); + } + + getExtensionGallery(id: string): IExtensionGallery | undefined { + return this._extensionGalleryMap.get(id); + } + + getExtensionHost(id: string): ExtensionHost | undefined { + return this._extensionHosts.get(id); + } + + isExtensionActivated(id: string): boolean { + return this._extensionHosts.has(id); + } +} diff --git a/packages/engine-core/src/extension/extensionService.ts b/packages/engine-core/src/extension/extensionService.ts index 959f889d3..1437ae6ad 100644 --- a/packages/engine-core/src/extension/extensionService.ts +++ b/packages/engine-core/src/extension/extensionService.ts @@ -1,5 +1,5 @@ import { createDecorator } from '@alilc/lowcode-shared'; -import { ExtensionManagement, type IExtensionRegisterOptions } from './extensionManagement'; +import { ExtensionManager, type IExtensionRegisterOptions } from './extensionManager'; import { type IFunctionExtension } from './extension'; import { ExtensionHost } from './extensionHost'; @@ -11,26 +11,32 @@ export interface IExtensionService { has(name: string): boolean; getExtensionHost(name: string): ExtensionHost | undefined; + + dispose(): void; } export const IExtensionService = createDecorator('extensionService'); export class ExtensionService implements IExtensionService { - private extensionManagement = new ExtensionManagement(); + private _manager = new ExtensionManager(); + + dispose(): void { + this._manager.dispose(); + } register(extension: IFunctionExtension, options?: IExtensionRegisterOptions): Promise { - return this.extensionManagement.register(extension, options); + return this._manager.register(extension, options); } deregister(name: string): Promise { - return this.extensionManagement.deregister(name); + return this._manager.deregister(name); } has(name: string): boolean { - return this.extensionManagement.has(name); + return this._manager.has(name); } getExtensionHost(name: string): ExtensionHost | undefined { - return this.extensionManagement.getExtensionHost(name); + return this._manager.getExtensionHost(name); } } diff --git a/packages/engine-core/src/extension/index.ts b/packages/engine-core/src/extension/index.ts new file mode 100644 index 000000000..7c0814015 --- /dev/null +++ b/packages/engine-core/src/extension/index.ts @@ -0,0 +1,2 @@ +export * from './extension'; +export * from './extensionService'; diff --git a/packages/engine-core/src/extension/registry.ts b/packages/engine-core/src/extension/registry.ts index a6d7be19b..f6d2e088d 100644 --- a/packages/engine-core/src/extension/registry.ts +++ b/packages/engine-core/src/extension/registry.ts @@ -39,8 +39,10 @@ class RegistryImpl implements IRegistry { export const Registry: IRegistry = new RegistryImpl(); export const Extensions = { + JSONContribution: 'base.contributions.jsonContribution', Configuration: 'base.contributions.configuration', Command: 'base.contributions.command', Keybinding: 'base.contributions.keybinding', Widget: 'base.contributions.widget', + ContentEditor: 'base.contributions.contentEditor', }; diff --git a/packages/engine-core/src/file/file.ts b/packages/engine-core/src/file/file.ts index 2876624f4..4dc993216 100644 --- a/packages/engine-core/src/file/file.ts +++ b/packages/engine-core/src/file/file.ts @@ -199,20 +199,20 @@ export enum FsContants { } export interface IFileSystemProvider { - watch(resource: URI, opts: IWatchOptions): IFileSystemWatcher; + watch(resource: URI, opts?: IWatchOptions): IFileSystemWatcher; chmod(resource: URI, mode: number): Promise; access(resource: URI, mode?: number): Promise; stat(resource: URI): Promise; - mkdir(resource: URI, opts: IFileWriteOptions): Promise; + mkdir(resource: URI, opts?: IFileWriteOptions): Promise; readdir(resource: URI): Promise<[string, FileType][]>; - delete(resource: URI, opts: IFileDeleteOptions): Promise; + delete(resource: URI, opts?: IFileDeleteOptions): Promise; - rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise; + rename(from: URI, to: URI, opts?: IFileOverwriteOptions): Promise; // copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise; readFile(resource: URI): Promise; - writeFile(resource: URI, content: string, opts: IFileWriteOptions): Promise; + writeFile(resource: URI, content: string, opts?: IFileWriteOptions): Promise; } export enum FileSystemErrorCode { diff --git a/packages/engine-core/src/file/inMemoryFileSystemProvider.ts b/packages/engine-core/src/file/inMemoryFileSystemProvider.ts index b06e25b33..3ae76bc1f 100644 --- a/packages/engine-core/src/file/inMemoryFileSystemProvider.ts +++ b/packages/engine-core/src/file/inMemoryFileSystemProvider.ts @@ -104,17 +104,17 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider { }; } - watch(resource: URI, opts: IWatchOptions): IFileSystemWatcher { + watch(resource: URI, opts?: IWatchOptions): IFileSystemWatcher { return { resource, opts } as any as IFileSystemWatcher; } - async mkdir(resource: URI, opts: IFileWriteOptions): Promise { + async mkdir(resource: URI, opts?: IFileWriteOptions): Promise { const base = basename(resource.path); const dir = dirname(resource.path); const parent = this._lookupAsDirectory(dir, true); if (parent) { - if (!opts.overwrite && parent.entries.has(base)) { + if (!opts?.overwrite && parent.entries.has(base)) { throw FileSystemError.create('directory exists', FileSystemErrorCode.FileExists); } @@ -126,7 +126,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider { { resource, type: FileChangeType.ADDED }, ); } else { - if (!opts.recursive) { + if (!opts?.recursive) { throw FileSystemError.create('parent directory not found', FileSystemErrorCode.FileNotFound); } @@ -154,14 +154,14 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider { return file.data; } - async writeFile(resource: URI, content: string, opts: IFileWriteOptions): Promise { + async writeFile(resource: URI, content: string, opts?: IFileWriteOptions): Promise { const base = basename(resource.path); const dir = dirname(resource.path); const dirUri = resource.with({ path: dir }); let parent = this._lookupAsDirectory(dir, true); if (!parent) { - if (!opts.recursive) { + if (!opts?.recursive) { throw FileSystemError.create('file not found', FileSystemErrorCode.FileNotFound); } parent = await this._mkdirRecursive(dirUri); @@ -174,7 +174,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider { if (entry && entry.permission < FilePermission.Writable) { throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable); } - if (entry && !opts.overwrite) { + if (entry && !opts?.overwrite) { throw FileSystemError.create('file exists already', FileSystemErrorCode.FileExists); } @@ -189,7 +189,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider { this._fireSoon({ resource, type: FileChangeType.UPDATED }); } - async delete(resource: URI, opts: IFileDeleteOptions): Promise { + async delete(resource: URI, opts?: IFileDeleteOptions): Promise { const dir = dirname(resource.path); const base = basename(resource.path); const parent = this._lookupAsDirectory(dir, false); @@ -206,7 +206,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider { } if (entry instanceof Directory) { - if (opts.recursive) { + if (opts?.recursive) { parent.entries.delete(base); parent.mtime = Date.now(); } else { @@ -224,7 +224,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider { } } - async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise { + async rename(from: URI, to: URI, opts?: IFileOverwriteOptions): Promise { if (from.path === to.path) return; const entry = this._lookup(from.path, false); @@ -232,7 +232,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider { if (entry.permission < FilePermission.Writable) { throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable); } - if (!opts.overwrite) { + if (!opts?.overwrite) { throw FileSystemError.create('file exists already', FileSystemErrorCode.FileExists); } diff --git a/packages/engine-core/src/index.ts b/packages/engine-core/src/index.ts index 3f925d305..bfe60cc0a 100644 --- a/packages/engine-core/src/index.ts +++ b/packages/engine-core/src/index.ts @@ -4,6 +4,7 @@ export * from './resource'; export * from './command'; export * from './workspace'; export * from './common'; +export * from './keybinding'; // test export * from './main'; diff --git a/packages/engine-core/src/json-schema/index.ts b/packages/engine-core/src/json-schema/index.ts new file mode 100644 index 000000000..a54642c64 --- /dev/null +++ b/packages/engine-core/src/json-schema/index.ts @@ -0,0 +1,2 @@ +export * from './jsonSchema'; +export * from './jsonSchemaRegistry'; diff --git a/packages/engine-core/src/json-schema/jsonSchema.ts b/packages/engine-core/src/json-schema/jsonSchema.ts new file mode 100644 index 000000000..86848d3bc --- /dev/null +++ b/packages/engine-core/src/json-schema/jsonSchema.ts @@ -0,0 +1,272 @@ +export type JSONSchemaType = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object'; + +/** + * fork from vscode + */ +export interface IJSONSchema { + id?: string; + $id?: string; + $schema?: string; + type?: JSONSchemaType | JSONSchemaType[]; + title?: string; + default?: any; + definitions?: IJSONSchemaMap; + description?: string; + properties?: IJSONSchemaMap; + patternProperties?: IJSONSchemaMap; + additionalProperties?: boolean | IJSONSchema; + minProperties?: number; + maxProperties?: number; + dependencies?: IJSONSchemaMap | { [prop: string]: string[] }; + items?: IJSONSchema | IJSONSchema[]; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + additionalItems?: boolean | IJSONSchema; + pattern?: string; + minLength?: number; + maxLength?: number; + minimum?: number; + maximum?: number; + exclusiveMinimum?: boolean | number; + exclusiveMaximum?: boolean | number; + multipleOf?: number; + required?: string[]; + $ref?: string; + anyOf?: IJSONSchema[]; + allOf?: IJSONSchema[]; + oneOf?: IJSONSchema[]; + not?: IJSONSchema; + enum?: any[]; + format?: string; + + // schema draft 06 + const?: any; + contains?: IJSONSchema; + propertyNames?: IJSONSchema; + examples?: any[]; + + // schema draft 07 + $comment?: string; + if?: IJSONSchema; + then?: IJSONSchema; + else?: IJSONSchema; + + // schema 2019-09 + unevaluatedProperties?: boolean | IJSONSchema; + unevaluatedItems?: boolean | IJSONSchema; + minContains?: number; + maxContains?: number; + deprecated?: boolean; + dependentRequired?: { [prop: string]: string[] }; + dependentSchemas?: IJSONSchemaMap; + $defs?: { [name: string]: IJSONSchema }; + $anchor?: string; + $recursiveRef?: string; + $recursiveAnchor?: string; + $vocabulary?: any; + + // schema 2020-12 + prefixItems?: IJSONSchema[]; + $dynamicRef?: string; + $dynamicAnchor?: string; + + // internal extensions + errorMessage?: string; +} + +export interface IJSONSchemaMap { + [name: string]: IJSONSchema; +} + +export interface IJSONSchemaSnippet { + label?: string; + description?: string; + body?: any; // a object that will be JSON stringified + bodyText?: string; // an already stringified JSON object that can contain new lines (\n) and tabs (\t) +} + +/** + * Converts a basic JSON schema to a TypeScript type. + * + * TODO: only supports basic schemas. Doesn't support all JSON schema features. + */ +export type SchemaToType = T extends { type: 'string' } + ? string + : T extends { type: 'number' } + ? number + : T extends { type: 'boolean' } + ? boolean + : T extends { type: 'null' } + ? null + : T extends { type: 'object'; properties: infer P } + ? { [K in keyof P]: SchemaToType } + : T extends { type: 'array'; items: infer I } + ? Array> + : never; + +interface Equals { + schemas: IJSONSchema[]; + id?: string; +} + +export function getCompressedContent(schema: IJSONSchema): string { + let hasDups = false; + + // visit all schema nodes and collect the ones that are equal + const equalsByString = new Map(); + const nodeToEquals = new Map(); + const visitSchemas = (next: IJSONSchema) => { + if (schema === next) { + return true; + } + const val = JSON.stringify(next); + if (val.length < 30) { + // the $ref takes around 25 chars, so we don't save anything + return true; + } + const eq = equalsByString.get(val); + if (!eq) { + const newEq = { schemas: [next] }; + equalsByString.set(val, newEq); + nodeToEquals.set(next, newEq); + return true; + } + eq.schemas.push(next); + nodeToEquals.set(next, eq); + hasDups = true; + return false; + }; + traverseNodes(schema, visitSchemas); + equalsByString.clear(); + + if (!hasDups) { + return JSON.stringify(schema); + } + + let defNodeName = '$defs'; + while (Reflect.has(schema, defNodeName)) { + defNodeName += '_'; + } + + // used to collect all schemas that are later put in `$defs`. The index in the array is the id of the schema. + const definitions: IJSONSchema[] = []; + + function stringify(root: IJSONSchema): string { + return JSON.stringify(root, (_key: string, value: any) => { + if (value !== root) { + const eq = nodeToEquals.get(value); + if (eq && eq.schemas.length > 1) { + if (!eq.id) { + eq.id = `_${definitions.length}`; + definitions.push(eq.schemas[0]); + } + return { $ref: `#/${defNodeName}/${eq.id}` }; + } + } + return value; + }); + } + + // stringify the schema and replace duplicate subtrees with $ref + // this will add new items to the definitions array + const str = stringify(schema); + + // now stringify the definitions. Each invication of stringify cann add new items to the definitions array, so the length can grow while we iterate + const defStrings: string[] = []; + for (let i = 0; i < definitions.length; i++) { + defStrings.push(`"_${i}":${stringify(definitions[i])}`); + } + if (defStrings.length) { + return `${str.substring(0, str.length - 1)},"${defNodeName}":{${defStrings.join(',')}}}`; + } + return str; +} + +type IJSONSchemaRef = IJSONSchema | boolean; + +function isObject(thing: any): thing is object { + return typeof thing === 'object' && thing !== null; +} + +/* + * Traverse a JSON schema and visit each schema node + */ +function traverseNodes(root: IJSONSchema, visit: (schema: IJSONSchema) => boolean) { + if (!root || typeof root !== 'object') { + return; + } + const collectEntries = (...entries: (IJSONSchemaRef | undefined)[]) => { + for (const entry of entries) { + if (isObject(entry)) { + toWalk.push(entry); + } + } + }; + const collectMapEntries = (...maps: (IJSONSchemaMap | undefined)[]) => { + for (const map of maps) { + if (isObject(map)) { + for (const key in map) { + const entry = map[key]; + if (isObject(entry)) { + toWalk.push(entry); + } + } + } + } + }; + const collectArrayEntries = (...arrays: (IJSONSchemaRef[] | undefined)[]) => { + for (const array of arrays) { + if (Array.isArray(array)) { + for (const entry of array) { + if (isObject(entry)) { + toWalk.push(entry); + } + } + } + } + }; + const collectEntryOrArrayEntries = (items: IJSONSchemaRef[] | IJSONSchemaRef | undefined) => { + if (Array.isArray(items)) { + for (const entry of items) { + if (isObject(entry)) { + toWalk.push(entry); + } + } + } else if (isObject(items)) { + toWalk.push(items); + } + }; + + const toWalk: IJSONSchema[] = [root]; + + let next = toWalk.pop(); + while (next) { + const visitChildern = visit(next); + if (visitChildern) { + collectEntries( + next.additionalItems, + next.additionalProperties, + next.not, + next.contains, + next.propertyNames, + next.if, + next.then, + next.else, + next.unevaluatedItems, + next.unevaluatedProperties, + ); + collectMapEntries( + next.definitions, + next.$defs, + next.properties, + next.patternProperties, + next.dependencies, + next.dependentSchemas, + ); + collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.prefixItems); + collectEntryOrArrayEntries(next.items); + } + next = toWalk.pop(); + } +} diff --git a/packages/engine-core/src/json-schema/jsonSchemaRegistry.ts b/packages/engine-core/src/json-schema/jsonSchemaRegistry.ts new file mode 100644 index 000000000..034b31e3e --- /dev/null +++ b/packages/engine-core/src/json-schema/jsonSchemaRegistry.ts @@ -0,0 +1,78 @@ +import { Events } from '@alilc/lowcode-shared'; +import { Registry, Extensions } from '../extension/registry'; +import { getCompressedContent, type IJSONSchema } from './jsonSchema'; + +export interface ISchemaContributions { + schemas: { [id: string]: IJSONSchema }; +} + +export interface IJSONContributionRegistry { + readonly onDidChangeSchema: Events.Event; + + /** + * Register a schema to the registry. + */ + registerSchema(uri: string, unresolvedSchemaContent: IJSONSchema): void; + + /** + * Notifies all listeners that the content of the given schema has changed. + * @param uri The id of the schema + */ + notifySchemaChanged(uri: string): void; + + /** + * Get all schemas + */ + getSchemaContributions(): ISchemaContributions; + + /** + * Gets the (compressed) content of the schema with the given schema ID (if any) + * @param uri The id of the schema + */ + getSchemaContent(uri: string): string | undefined; + + /** + * Returns true if there's a schema that matches the given schema ID + * @param uri The id of the schema + */ + hasSchemaContent(uri: string): boolean; +} + +class JSONContributionRegistryImpl implements IJSONContributionRegistry { + private _onDidChangeSchema = new Events.Emitter(); + onDidChangeSchema = this._onDidChangeSchema.event; + + private schemasById: { [id: string]: IJSONSchema }; + + constructor() { + this.schemasById = {}; + } + + registerSchema(uri: string, unresolvedSchemaContent: IJSONSchema): void { + this.schemasById[uri] = unresolvedSchemaContent; + this._onDidChangeSchema.notify(uri); + } + + notifySchemaChanged(uri: string): void { + this._onDidChangeSchema.notify(uri); + } + + getSchemaContributions(): ISchemaContributions { + return { + schemas: this.schemasById, + }; + } + + getSchemaContent(uri: string): string | undefined { + const schema = this.schemasById[uri]; + return schema ? getCompressedContent(schema) : undefined; + } + + hasSchemaContent(uri: string): boolean { + return !!this.schemasById[uri]; + } +} + +export const JSONContributionRegistry = new JSONContributionRegistryImpl(); + +Registry.add(Extensions.JSONContribution, JSONContributionRegistry); diff --git a/packages/engine-core/src/keybinding/index.ts b/packages/engine-core/src/keybinding/index.ts new file mode 100644 index 000000000..68e672b14 --- /dev/null +++ b/packages/engine-core/src/keybinding/index.ts @@ -0,0 +1,2 @@ +export * from './keybinding'; +export * from './keybindingService'; diff --git a/packages/engine-core/src/keybinding/keybinding.ts b/packages/engine-core/src/keybinding/keybinding.ts index e69de29bb..a8bdb82ec 100644 --- a/packages/engine-core/src/keybinding/keybinding.ts +++ b/packages/engine-core/src/keybinding/keybinding.ts @@ -0,0 +1,63 @@ +import { Events, createDecorator } from '@alilc/lowcode-shared'; +import { Keybinding, ResolvedKeybinding } from './keybindings'; +import { ResolvedKeybindingItem } from './keybindingResolver'; +import { type IKeyboardEvent } from './keybindingEvent'; + +export interface IUserFriendlyKeybinding { + key: string; + command: string; + args?: any; +} + +export interface IKeybindingService { + readonly inChordMode: boolean; + + onDidUpdateKeybindings: Events.Event; + + /** + * Returns none, one or many (depending on keyboard layout)! + */ + resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[]; + + resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding; + + resolveUserBinding(userBinding: string): ResolvedKeybinding[]; + + /** + * Resolve and dispatch `keyboardEvent` and invoke the command. + */ + dispatchEvent(e: IKeyboardEvent, target: any): boolean; + + /** + * Resolve and dispatch `keyboardEvent`, but do not invoke the command or change inner state. + */ + // softDispatch(keyboardEvent: IKeyboardEvent, target: any): ResolutionResult; + + /** + * Enable hold mode for this command. This is only possible if the command is current being dispatched, meaning + * we are after its keydown and before is keyup event. + * + * @returns A promise that resolves when hold stops, returns undefined if hold mode could not be enabled. + */ + enableKeybindingHoldMode(commandId: string): Promise | undefined; + + dispatchByUserSettingsLabel(userSettingsLabel: string, target: any): void; + + /** + * Look up keybindings for a command. + * Use `lookupKeybinding` if you are interested in the preferred keybinding. + */ + lookupKeybindings(commandId: string): ResolvedKeybinding[]; + + /** + * Look up the preferred (last defined) keybinding for a command. + * @returns The preferred keybinding or null if the command is not bound. + */ + // lookupKeybinding(commandId: string, context?: any): ResolvedKeybinding | undefined; + + getDefaultKeybindings(): readonly ResolvedKeybindingItem[]; + + getKeybindings(): readonly ResolvedKeybindingItem[]; +} + +export const IKeybindingService = createDecorator('keybindingService'); diff --git a/packages/engine-core/src/keybinding/keybindingEvent.ts b/packages/engine-core/src/keybinding/keybindingEvent.ts new file mode 100644 index 000000000..376c719b6 --- /dev/null +++ b/packages/engine-core/src/keybinding/keybindingEvent.ts @@ -0,0 +1,145 @@ +import { isMacintosh, isLinux, isFirefox } from '@alilc/lowcode-shared'; +import { EVENT_CODE_TO_KEY_CODE_MAP, KeyCode } from '../common'; +import { KeyCodeChord, BinaryKeybindingsMask } from './keybindings'; + +const ctrlKeyMod = isMacintosh ? BinaryKeybindingsMask.WinCtrl : BinaryKeybindingsMask.CtrlCmd; +const altKeyMod = BinaryKeybindingsMask.Alt; +const shiftKeyMod = BinaryKeybindingsMask.Shift; +const metaKeyMod = isMacintosh ? BinaryKeybindingsMask.CtrlCmd : BinaryKeybindingsMask.WinCtrl; + +export interface IKeyboardEvent { + readonly ctrlKey: boolean; + readonly shiftKey: boolean; + readonly altKey: boolean; + readonly metaKey: boolean; + readonly keyCode: KeyCode; + readonly code: string; + + preventDefault(): void; + stopPropagation(): void; + + toKeyCodeChord(): KeyCodeChord; + equals(keybinding: number): boolean; +} + +export class StandardKeyboardEvent implements IKeyboardEvent { + public readonly navtiveEvent: KeyboardEvent; + public readonly target: HTMLElement; + + public readonly ctrlKey: boolean; + public readonly shiftKey: boolean; + public readonly altKey: boolean; + public readonly metaKey: boolean; + public readonly keyCode: KeyCode; + public readonly code: string; + + private _asKeybinding: number; + private _asKeyCodeChord: KeyCodeChord; + + constructor(source: KeyboardEvent) { + const e = source; + + this.navtiveEvent = e; + this.target = e.target; + + this.ctrlKey = e.ctrlKey; + this.shiftKey = e.shiftKey; + this.altKey = e.altKey; + this.metaKey = e.metaKey; + this.keyCode = extractKeyCode(e); + this.code = e.code; + + this.ctrlKey = this.ctrlKey || this.keyCode === KeyCode.Ctrl; + this.altKey = this.altKey || this.keyCode === KeyCode.Alt; + this.shiftKey = this.shiftKey || this.keyCode === KeyCode.Shift; + this.metaKey = this.metaKey || this.keyCode === KeyCode.Meta; + + this._asKeybinding = this._computeKeybinding(); + this._asKeyCodeChord = this._computeKeyCodeChord(); + } + + preventDefault(): void { + if (this.navtiveEvent && this.navtiveEvent.preventDefault) { + this.navtiveEvent.preventDefault(); + } + } + + stopPropagation(): void { + if (this.navtiveEvent && this.navtiveEvent.stopPropagation) { + this.navtiveEvent.stopPropagation(); + } + } + + toKeyCodeChord(): KeyCodeChord { + return this._asKeyCodeChord; + } + + equals(other: number): boolean { + return this._asKeybinding === other; + } + + private _computeKeybinding(): number { + let key = KeyCode.Unknown; + if ( + this.keyCode !== KeyCode.Ctrl && + this.keyCode !== KeyCode.Shift && + this.keyCode !== KeyCode.Alt && + this.keyCode !== KeyCode.Meta + ) { + key = this.keyCode; + } + + let result = 0; + if (this.ctrlKey) { + result |= ctrlKeyMod; + } + if (this.altKey) { + result |= altKeyMod; + } + if (this.shiftKey) { + result |= shiftKeyMod; + } + if (this.metaKey) { + result |= metaKeyMod; + } + result |= key; + + return result; + } + + private _computeKeyCodeChord(): KeyCodeChord { + let key = KeyCode.Unknown; + if ( + this.keyCode !== KeyCode.Ctrl && + this.keyCode !== KeyCode.Shift && + this.keyCode !== KeyCode.Alt && + this.keyCode !== KeyCode.Meta + ) { + key = this.keyCode; + } + return new KeyCodeChord(this.ctrlKey, this.shiftKey, this.altKey, this.metaKey, key); + } +} + +function extractKeyCode(e: KeyboardEvent): KeyCode { + const code = e.code; + + // browser quirks + if (isFirefox) { + switch (code) { + case 'Backquote': + if (isLinux) { + return KeyCode.IntlBackslash; + } + break; + case 'OSLeft': + if (isMacintosh) { + return KeyCode.Meta; + } + break; + } + } + + // cross browser keycodes: + return EVENT_CODE_TO_KEY_CODE_MAP[code] || KeyCode.Unknown; +} diff --git a/packages/engine-core/src/keybinding/keybindingParser.ts b/packages/engine-core/src/keybinding/keybindingParser.ts index 6232b0f32..0f745d095 100644 --- a/packages/engine-core/src/keybinding/keybindingParser.ts +++ b/packages/engine-core/src/keybinding/keybindingParser.ts @@ -1,5 +1,5 @@ -import { KeyCodeUtils, ScanCodeUtils } from '@alilc/lowcode-shared'; -import { KeyCodeChord, ScanCodeChord, Keybinding, Chord } from './keybindings'; +import { KeyCodeUtils } from '../common'; +import { KeyCodeChord, Keybinding } from './keybindings'; export class KeybindingParser { private static _readModifiers(input: string) { @@ -67,17 +67,8 @@ export class KeybindingParser { }; } - private static parseChord(input: string): [Chord, string] { + private static parseChord(input: string): [KeyCodeChord, string] { const mods = this._readModifiers(input); - const scanCodeMatch = mods.key.match(/^\[([^\]]+)\]$/); - if (scanCodeMatch) { - const strScanCode = scanCodeMatch[1]; - const scanCode = ScanCodeUtils.lowerCaseToEnum(strScanCode); - return [ - new ScanCodeChord(mods.ctrl, mods.shift, mods.alt, mods.meta, scanCode), - mods.remains, - ]; - } const keyCode = KeyCodeUtils.fromUserSettings(mods.key); return [new KeyCodeChord(mods.ctrl, mods.shift, mods.alt, mods.meta, keyCode), mods.remains]; } @@ -87,8 +78,8 @@ export class KeybindingParser { return null; } - const chords: Chord[] = []; - let chord: Chord; + const chords: KeyCodeChord[] = []; + let chord: KeyCodeChord; while (input.length > 0) { [chord, input] = this.parseChord(input); diff --git a/packages/engine-core/src/keybinding/keybindingRegistry.ts b/packages/engine-core/src/keybinding/keybindingRegistry.ts index 471e1188a..52a0fff53 100644 --- a/packages/engine-core/src/keybinding/keybindingRegistry.ts +++ b/packages/engine-core/src/keybinding/keybindingRegistry.ts @@ -1,18 +1,16 @@ -import { OperatingSystem, OS } from '@alilc/lowcode-shared'; +import { + combinedDisposable, + DisposableStore, + IDisposable, + LinkedList, + OperatingSystem, + OS, + toDisposable, +} from '@alilc/lowcode-shared'; import { ICommandHandler, ICommandMetadata, CommandsRegistry } from '../command'; -import { Keybinding } from './keybindings'; +import { decodeKeybinding, Keybinding } from './keybindings'; import { Extensions, Registry } from '../extension/registry'; -export interface IKeybindingItem { - keybinding: Keybinding | null; - command: string | null; - commandArgs?: any; - weight1: number; - weight2: number; - extensionId: string | null; - isBuiltinExtension: boolean; -} - export interface IKeybindings { primary?: number; secondary?: number[]; @@ -46,8 +44,16 @@ export interface IExtensionKeybindingRule { id: string; args?: any; weight: number; - extensionId?: string; - isBuiltinExtension?: boolean; + extensionId: string; +} + +export interface IKeybindingItem { + keybinding: Keybinding | null; + command: string | null; + commandArgs?: any; + weight1: number; + weight2: number; + extensionId: string | null; } export const enum KeybindingWeight { @@ -59,9 +65,12 @@ export const enum KeybindingWeight { } export interface IKeybindingsRegistry { - registerKeybindingRule(rule: IKeybindingRule): void; + registerKeybindingRule(rule: IKeybindingRule): IDisposable; + setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void; - registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void; + + registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): IDisposable; + getDefaultKeybindings(): IKeybindingItem[]; } @@ -90,18 +99,110 @@ export class KeybindingsRegistryImpl implements IKeybindingsRegistry { return kb; } - registerKeybindingRule(rule: IKeybindingRule): void { + private _coreKeybindings: LinkedList; + private _extensionKeybindings: IKeybindingItem[]; + private _cachedMergedKeybindings: IKeybindingItem[] | null; + + constructor() { + this._coreKeybindings = new LinkedList(); + this._extensionKeybindings = []; + this._cachedMergedKeybindings = null; + } + + registerKeybindingRule(rule: IKeybindingRule): IDisposable { const actualKb = KeybindingsRegistryImpl.bindToCurrentPlatform(rule); + + const result = new DisposableStore(); + + if (actualKb && actualKb.primary) { + const kk = decodeKeybinding(actualKb.primary, OS); + if (kk) { + result.add(this._registerDefaultKeybinding(kk, rule.id, rule.args, rule.weight, 0)); + } + } + + if (actualKb && Array.isArray(actualKb.secondary)) { + for (let i = 0, len = actualKb.secondary.length; i < len; i++) { + const k = actualKb.secondary[i]; + const kk = decodeKeybinding(k, OS); + if (kk) { + result.add(this._registerDefaultKeybinding(kk, rule.id, rule.args, rule.weight, -i - 1)); + } + } + } + + return result; + } + + private _registerDefaultKeybinding( + keybinding: Keybinding, + commandId: string, + commandArgs: any, + weight1: number, + weight2: number, + ): IDisposable { + const remove = this._coreKeybindings.push({ + keybinding: keybinding, + command: commandId, + commandArgs: commandArgs, + weight1: weight1, + weight2: weight2, + extensionId: null, + }); + this._cachedMergedKeybindings = null; + + return toDisposable(() => { + remove(); + this._cachedMergedKeybindings = null; + }); + } + + registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): IDisposable { + return combinedDisposable(this.registerKeybindingRule(desc), CommandsRegistry.registerCommand(desc)); } - registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void { - this.registerKeybindingRule(desc); - CommandsRegistry.registerCommand(desc); + setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void { + const result: IKeybindingItem[] = []; + + for (const rule of rules) { + if (rule.keybinding) { + result.push({ + keybinding: rule.keybinding, + command: rule.id, + commandArgs: rule.args, + weight1: rule.weight, + weight2: 0, + extensionId: rule.extensionId || null, + }); + } + } + + this._extensionKeybindings = result; + this._cachedMergedKeybindings = null; } - setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void {} + getDefaultKeybindings(): IKeybindingItem[] { + if (!this._cachedMergedKeybindings) { + this._cachedMergedKeybindings = Array.from(this._coreKeybindings).concat(this._extensionKeybindings); + this._cachedMergedKeybindings.sort(sorter); + } + return this._cachedMergedKeybindings.slice(0); + } +} - getDefaultKeybindings(): IKeybindingItem[] {} +function sorter(a: IKeybindingItem, b: IKeybindingItem): number { + if (a.weight1 !== b.weight1) { + return a.weight1 - b.weight1; + } + if (a.command && b.command) { + if (a.command < b.command) { + return -1; + } + if (a.command > b.command) { + return 1; + } + } + return a.weight2 - b.weight2; } export const KeybindingsRegistry = new KeybindingsRegistryImpl(); diff --git a/packages/engine-core/src/keybinding/keybindingResolver.ts b/packages/engine-core/src/keybinding/keybindingResolver.ts index 9e276aa83..35ff8a26e 100644 --- a/packages/engine-core/src/keybinding/keybindingResolver.ts +++ b/packages/engine-core/src/keybinding/keybindingResolver.ts @@ -1,68 +1,33 @@ -export const enum ResultKind { - /** No keybinding found this sequence of chords */ - NoMatchingKb, - - /** There're several keybindings that have the given sequence of chords as a prefix */ - MoreChordsNeeded, - - /** A single keybinding found to be dispatched/invoked */ - KbFound, -} - -export type ResolutionResult = - | { kind: ResultKind.NoMatchingKb } - | { kind: ResultKind.MoreChordsNeeded } - | { kind: ResultKind.KbFound; commandId: string | null; commandArgs: any; isBubble: boolean }; - -// util definitions to make working with the above types easier within this module: - -export const NoMatchingKb: ResolutionResult = { kind: ResultKind.NoMatchingKb }; -const MoreChordsNeeded: ResolutionResult = { kind: ResultKind.MoreChordsNeeded }; -function KbFound(commandId: string | null, commandArgs: any, isBubble: boolean): ResolutionResult { - return { kind: ResultKind.KbFound, commandId, commandArgs, isBubble }; -} - -//#endregion +import { CharCode } from '../common'; +import { ResolvedKeybinding } from './keybindings'; export class ResolvedKeybindingItem { - _resolvedKeybindingItemBrand: void = undefined; - public readonly resolvedKeybinding: ResolvedKeybinding | undefined; public readonly chords: string[]; public readonly bubble: boolean; public readonly command: string | null; public readonly commandArgs: any; - public readonly when: ContextKeyExpression | undefined; public readonly isDefault: boolean; public readonly extensionId: string | null; - public readonly isBuiltinExtension: boolean; constructor( resolvedKeybinding: ResolvedKeybinding | undefined, command: string | null, commandArgs: any, - when: ContextKeyExpression | undefined, isDefault: boolean, extensionId: string | null, - isBuiltinExtension: boolean, ) { this.resolvedKeybinding = resolvedKeybinding; - this.chords = resolvedKeybinding - ? toEmptyArrayIfContainsNull(resolvedKeybinding.getDispatchChords()) - : []; + this.chords = resolvedKeybinding ? toEmptyArrayIfContainsNull(resolvedKeybinding.getDispatchChords()) : []; if (resolvedKeybinding && this.chords.length === 0) { // handle possible single modifier chord keybindings - this.chords = toEmptyArrayIfContainsNull( - resolvedKeybinding.getSingleModifierDispatchChords(), - ); + this.chords = toEmptyArrayIfContainsNull(resolvedKeybinding.getSingleModifierDispatchChords()); } this.bubble = command ? command.charCodeAt(0) === CharCode.Caret : false; this.command = this.bubble ? command!.substr(1) : command; this.commandArgs = commandArgs; - this.when = when; this.isDefault = isDefault; this.extensionId = extensionId; - this.isBuiltinExtension = isBuiltinExtension; } } @@ -77,3 +42,329 @@ export function toEmptyArrayIfContainsNull(arr: (T | null)[]): T[] { } return result; } + +export const enum ResultKind { + /** No keybinding found this sequence of chords */ + NoMatchingKb, + + /** There're several keybindings that have the given sequence of chords as a prefix */ + MoreChordsNeeded, + + /** A single keybinding found to be dispatched/invoked */ + KbFound, +} + +export type ResolutionResult = + | { kind: ResultKind.NoMatchingKb } + | { kind: ResultKind.MoreChordsNeeded } + | { kind: ResultKind.KbFound; commandId: string | null; commandArgs: any; isBubble: boolean }; + +// util definitions to make working with the above types easier within this module: + +export const NoMatchingKb: ResolutionResult = { kind: ResultKind.NoMatchingKb }; +const MoreChordsNeeded: ResolutionResult = { kind: ResultKind.MoreChordsNeeded }; +function KbFound(commandId: string | null, commandArgs: any, isBubble: boolean): ResolutionResult { + return { kind: ResultKind.KbFound, commandId, commandArgs, isBubble }; +} + +/** + * Stores mappings from keybindings to commands and from commands to keybindings. + * Given a sequence of chords, `resolve`s which keybinding it matches + */ +export class KeybindingResolver { + private readonly _log: (str: string) => void; + private readonly _defaultKeybindings: ResolvedKeybindingItem[]; + private readonly _keybindings: ResolvedKeybindingItem[]; + private readonly _defaultBoundCommands: Map; + private readonly _map: Map; + private readonly _lookupMap: Map; + + constructor( + /** built-in and extension-provided keybindings */ + defaultKeybindings: ResolvedKeybindingItem[], + /** user's keybindings */ + overrides: ResolvedKeybindingItem[], + log: (str: string) => void, + ) { + this._log = log; + this._defaultKeybindings = defaultKeybindings; + + this._defaultBoundCommands = new Map(); + for (const defaultKeybinding of defaultKeybindings) { + const command = defaultKeybinding.command; + if (command && command.charAt(0) !== '-') { + this._defaultBoundCommands.set(command, true); + } + } + + this._map = new Map(); + this._lookupMap = new Map(); + + this._keybindings = KeybindingResolver.handleRemovals( + ([] as ResolvedKeybindingItem[]).concat(defaultKeybindings).concat(overrides), + ); + for (let i = 0, len = this._keybindings.length; i < len; i++) { + const k = this._keybindings[i]; + if (k.chords.length === 0) { + // unbound + continue; + } + + this._addKeyPress(k.chords[0], k); + } + } + + private static _isTargetedForRemoval(defaultKb: ResolvedKeybindingItem, keypress: string[] | null): boolean { + if (keypress) { + for (let i = 0; i < keypress.length; i++) { + if (keypress[i] !== defaultKb.chords[i]) { + return false; + } + } + } + + return true; + } + + /** + * Looks for rules containing "-commandId" and removes them. + */ + public static handleRemovals(rules: ResolvedKeybindingItem[]): ResolvedKeybindingItem[] { + // Do a first pass and construct a hash-map for removals + const removals = new Map(); + for (let i = 0, len = rules.length; i < len; i++) { + const rule = rules[i]; + if (rule.command && rule.command.charAt(0) === '-') { + const command = rule.command.substring(1); + if (!removals.has(command)) { + removals.set(command, [rule]); + } else { + removals.get(command)!.push(rule); + } + } + } + + if (removals.size === 0) { + // There are no removals + return rules; + } + + // Do a second pass and keep only non-removed keybindings + const result: ResolvedKeybindingItem[] = []; + for (let i = 0, len = rules.length; i < len; i++) { + const rule = rules[i]; + + if (!rule.command || rule.command.length === 0) { + result.push(rule); + continue; + } + if (rule.command.charAt(0) === '-') { + continue; + } + const commandRemovals = removals.get(rule.command); + if (!commandRemovals || !rule.isDefault) { + result.push(rule); + continue; + } + let isRemoved = false; + for (const commandRemoval of commandRemovals) { + if (this._isTargetedForRemoval(rule, commandRemoval.chords)) { + isRemoved = true; + break; + } + } + if (!isRemoved) { + result.push(rule); + continue; + } + } + return result; + } + + private _addKeyPress(keypress: string, item: ResolvedKeybindingItem): void { + const conflicts = this._map.get(keypress); + + if (typeof conflicts === 'undefined') { + // There is no conflict so far + this._map.set(keypress, [item]); + this._addToLookupMap(item); + return; + } + + for (let i = conflicts.length - 1; i >= 0; i--) { + const conflict = conflicts[i]; + + if (conflict.command === item.command) { + continue; + } + + // Test if the shorter keybinding is a prefix of the longer one. + // If the shorter keybinding is a prefix, it effectively will shadow the longer one and is considered a conflict. + let isShorterKbPrefix = true; + for (let i = 1; i < conflict.chords.length && i < item.chords.length; i++) { + if (conflict.chords[i] !== item.chords[i]) { + // The ith step does not conflict + isShorterKbPrefix = false; + break; + } + } + if (!isShorterKbPrefix) { + continue; + } + } + + conflicts.push(item); + this._addToLookupMap(item); + } + + private _addToLookupMap(item: ResolvedKeybindingItem): void { + if (!item.command) { + return; + } + + let arr = this._lookupMap.get(item.command); + if (typeof arr === 'undefined') { + arr = [item]; + this._lookupMap.set(item.command, arr); + } else { + arr.push(item); + } + } + + private _removeFromLookupMap(item: ResolvedKeybindingItem): void { + if (!item.command) { + return; + } + const arr = this._lookupMap.get(item.command); + if (typeof arr === 'undefined') { + return; + } + for (let i = 0, len = arr.length; i < len; i++) { + if (arr[i] === item) { + arr.splice(i, 1); + return; + } + } + } + + public getDefaultBoundCommands(): Map { + return this._defaultBoundCommands; + } + + public getDefaultKeybindings(): readonly ResolvedKeybindingItem[] { + return this._defaultKeybindings; + } + + public getKeybindings(): readonly ResolvedKeybindingItem[] { + return this._keybindings; + } + + public lookupKeybindings(commandId: string): ResolvedKeybindingItem[] { + const items = this._lookupMap.get(commandId); + if (typeof items === 'undefined' || items.length === 0) { + return []; + } + + // Reverse to get the most specific item first + const result: ResolvedKeybindingItem[] = []; + let resultLen = 0; + for (let i = items.length - 1; i >= 0; i--) { + result[resultLen++] = items[i]; + } + return result; + } + + public lookupPrimaryKeybinding(commandId: string): ResolvedKeybindingItem | null { + const items = this._lookupMap.get(commandId); + if (typeof items === 'undefined' || items.length === 0) { + return null; + } + if (items.length === 1) { + return items[0]; + } + + return items[items.length - 1]; + } + + /** + * Looks up a keybinding trigged as a result of pressing a sequence of chords - `[...currentChords, keypress]` + * + * Example: resolving 3 chords pressed sequentially - `cmd+k cmd+p cmd+i`: + * `currentChords = [ 'cmd+k' , 'cmd+p' ]` and `keypress = `cmd+i` - last pressed chord + */ + public resolve(currentChords: string[], keypress: string): ResolutionResult { + const pressedChords = [...currentChords, keypress]; + + this._log(`| Resolving ${pressedChords}`); + + const kbCandidates = this._map.get(pressedChords[0]); + if (kbCandidates === undefined) { + // No bindings with such 0-th chord + this._log(`\\ No keybinding entries.`); + return NoMatchingKb; + } + + let lookupMap: ResolvedKeybindingItem[] | null = null; + + if (pressedChords.length < 2) { + lookupMap = kbCandidates; + } else { + // Fetch all chord bindings for `currentChords` + lookupMap = []; + for (let i = 0, len = kbCandidates.length; i < len; i++) { + const candidate = kbCandidates[i]; + + if (pressedChords.length > candidate.chords.length) { + // # of pressed chords can't be less than # of chords in a keybinding to invoke + continue; + } + + let prefixMatches = true; + for (let i = 1; i < pressedChords.length; i++) { + if (candidate.chords[i] !== pressedChords[i]) { + prefixMatches = false; + break; + } + } + if (prefixMatches) { + lookupMap.push(candidate); + } + } + } + + // check there's a keybinding with a matching when clause + const result = this._findCommand(lookupMap); + if (!result) { + this._log(`\\ From ${lookupMap.length} keybinding entries, no when clauses matched the context.`); + return NoMatchingKb; + } + + // check we got all chords necessary to be sure a particular keybinding needs to be invoked + if (pressedChords.length < result.chords.length) { + // The chord sequence is not complete + this._log( + `\\ From ${lookupMap.length} keybinding entries, awaiting ${result.chords.length - pressedChords.length} more chord(s), source: ${printSourceExplanation(result)}.`, + ); + return MoreChordsNeeded; + } + + this._log( + `\\ From ${lookupMap.length} keybinding entries, matched ${result.command}, source: ${printSourceExplanation(result)}.`, + ); + + return KbFound(result.command, result.commandArgs, result.bubble); + } + + private _findCommand(matches: ResolvedKeybindingItem[]): ResolvedKeybindingItem | null { + for (let i = matches.length - 1; i >= 0; i--) { + const k = matches[i]; + return k; + } + + return null; + } +} + +function printSourceExplanation(kb: ResolvedKeybindingItem): string { + return kb.extensionId ? `user extension ${kb.extensionId}` : kb.isDefault ? `built-in` : `user`; +} diff --git a/packages/engine-core/src/keybinding/keybindingService.ts b/packages/engine-core/src/keybinding/keybindingService.ts index a6018873f..82f0028fb 100644 --- a/packages/engine-core/src/keybinding/keybindingService.ts +++ b/packages/engine-core/src/keybinding/keybindingService.ts @@ -1,100 +1,468 @@ -import { createDecorator, type IJSONSchema, KeyCode, type Event } from '@alilc/lowcode-shared'; -import { Keybinding, ResolvedKeybinding } from './keybindings'; -import { ResolutionResult, ResolvedKeybindingItem } from './keybindingResolver'; - -export interface IUserFriendlyKeybinding { - key: string; - command: string; - args?: any; - when?: string; -} - -export interface IKeyboardEvent { - readonly _standardKeyboardEventBrand: true; +import { + DeferredPromise, + Disposable, + DisposableStore, + Events, + IDisposable, + illegalState, + IntervalTimer, + OperatingSystem, + OS, + TimeoutTimer, +} from '@alilc/lowcode-shared'; +import { IKeybindingService } from './keybinding'; +import { + BinaryKeybindingsMask, + Keybinding, + KeyCodeChord, + ResolvedChord, + ResolvedKeybinding, + SingleModifierChord, +} from './keybindings'; +import { type IKeyboardEvent, StandardKeyboardEvent } from './keybindingEvent'; +import { addDisposableListener, DOMEventType, KeyCode } from '../common'; +import { IKeyboardMapper, USLayoutKeyboardMapper } from './keyboardMapper'; +import { KeybindingParser } from './keybindingParser'; +import { KeybindingResolver, ResolvedKeybindingItem, ResultKind } from './keybindingResolver'; +import { IKeybindingItem, KeybindingsRegistry } from './keybindingRegistry'; +import { ICommandService } from '../command'; - readonly ctrlKey: boolean; - readonly shiftKey: boolean; - readonly altKey: boolean; - readonly metaKey: boolean; - readonly altGraphKey: boolean; - readonly keyCode: KeyCode; - readonly code: string; +interface CurrentChord { + keypress: string; + label: string | null; } -export interface KeybindingsSchemaContribution { - readonly onDidChange?: Event; +export class KeybindingService extends Disposable implements IKeybindingService { + private _onDidUpdateKeybindings = this._addDispose(new Events.Emitter()); + onDidUpdateKeybindings = this._onDidUpdateKeybindings.event; - getSchemaAdditions(): IJSONSchema[]; -} + private _keyboardMapper: IKeyboardMapper = new USLayoutKeyboardMapper(); -export interface IKeybindingService { - readonly _serviceBrand: undefined; + private _currentlyDispatchingCommandId: string | null = null; + private _isCompostion = false; + private _ignoreSingleModifiers: KeybindingModifierSet = KeybindingModifierSet.EMPTY; + private _currentSingleModifier: SingleModifierChord | null = null; + private _cachedResolver: KeybindingResolver | null = null; - readonly inChordMode: boolean; - - onDidUpdateKeybindings: Event; + private _keybindingHoldMode: DeferredPromise | null = null; + private _currentSingleModifierClearTimeout: TimeoutTimer = new TimeoutTimer(); + private _currentChordChecker: IntervalTimer = new IntervalTimer(); /** - * Returns none, one or many (depending on keyboard layout)! + * recently recorded keypresses that can trigger a keybinding; + * + * example: say, there's "cmd+k cmd+i" keybinding; + * the user pressed "cmd+k" (before they press "cmd+i") + * "cmd+k" would be stored in this array, when on pressing "cmd+i", the service + * would invoke the command bound by the keybinding */ - resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[]; + private _currentChords: CurrentChord[] = []; - resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding; + get inChordMode(): boolean { + return this._currentChords.length > 0; + } - resolveUserBinding(userBinding: string): ResolvedKeybinding[]; + constructor(@ICommandService private readonly commandService: ICommandService) { + super(); - /** - * Resolve and dispatch `keyboardEvent` and invoke the command. - */ - dispatchEvent(e: IKeyboardEvent, target: any): boolean; + this._addDispose(this._registerKeyboardEvent()); + } - /** - * Resolve and dispatch `keyboardEvent`, but do not invoke the command or change inner state. - */ - softDispatch(keyboardEvent: IKeyboardEvent, target: any): ResolutionResult; + private _getResolver(): KeybindingResolver { + if (!this._cachedResolver) { + const defaults = this._resolveKeybindingItems(KeybindingsRegistry.getDefaultKeybindings(), true); + // const overrides = this._resolveUserKeybindingItems(this.userKeybindings.keybindings, false); + this._cachedResolver = new KeybindingResolver(defaults, [], console.log); + } + return this._cachedResolver; + } - /** - * Enable hold mode for this command. This is only possible if the command is current being dispatched, meaning - * we are after its keydown and before is keyup event. - * - * @returns A promise that resolves when hold stops, returns undefined if hold mode could not be enabled. - */ - enableKeybindingHoldMode(commandId: string): Promise | undefined; + private _resolveKeybindingItems(items: IKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] { + const result: ResolvedKeybindingItem[] = []; - dispatchByUserSettingsLabel(userSettingsLabel: string, target: any): void; + let resultLen = 0; + for (const item of items) { + const keybinding = item.keybinding; + if (!keybinding) { + // This might be a removal keybinding item in user settings => accept it + result[resultLen++] = new ResolvedKeybindingItem( + undefined, + item.command, + item.commandArgs, + isDefault, + item.extensionId, + ); + } else { + if (this._assertBrowserConflicts(keybinding)) { + continue; + } - /** - * Look up keybindings for a command. - * Use `lookupKeybinding` if you are interested in the preferred keybinding. - */ - lookupKeybindings(commandId: string): ResolvedKeybinding[]; + const resolvedKeybindings = this._keyboardMapper.resolveKeybinding(keybinding); + for (let i = resolvedKeybindings.length - 1; i >= 0; i--) { + const resolvedKeybinding = resolvedKeybindings[i]; + result[resultLen++] = new ResolvedKeybindingItem( + resolvedKeybinding, + item.command, + item.commandArgs, + isDefault, + item.extensionId, + ); + } + } + } - /** - * Look up the preferred (last defined) keybinding for a command. - * @returns The preferred keybinding or null if the command is not bound. - */ - lookupKeybinding(commandId: string, context?: any): ResolvedKeybinding | undefined; + return result; + } - getDefaultKeybindingsContent(): string; + private _assertBrowserConflicts(keybinding: Keybinding): boolean { + for (const chord of keybinding.chords) { + if (!chord.metaKey && !chord.altKey && !chord.ctrlKey && !chord.shiftKey) { + continue; + } - getDefaultKeybindings(): readonly ResolvedKeybindingItem[]; + const modifiersMask = BinaryKeybindingsMask.CtrlCmd | BinaryKeybindingsMask.Alt | BinaryKeybindingsMask.Shift; - getKeybindings(): readonly ResolvedKeybindingItem[]; + let partModifiersMask = 0; + if (chord.metaKey) { + partModifiersMask |= BinaryKeybindingsMask.CtrlCmd; + } - customKeybindingsCount(): number; + if (chord.shiftKey) { + partModifiersMask |= BinaryKeybindingsMask.Shift; + } - /** - * Will the given key event produce a character that's rendered on screen, e.g. in a - * text box. *Note* that the results of this function can be incorrect. - */ - mightProducePrintableCharacter(event: IKeyboardEvent): boolean; + if (chord.altKey) { + partModifiersMask |= BinaryKeybindingsMask.Alt; + } + + if (chord.ctrlKey && OS === OperatingSystem.Macintosh) { + partModifiersMask |= BinaryKeybindingsMask.WinCtrl; + } + + if ((partModifiersMask & modifiersMask) === (BinaryKeybindingsMask.CtrlCmd | BinaryKeybindingsMask.Alt)) { + if (chord.keyCode === KeyCode.LeftArrow || chord.keyCode === KeyCode.RightArrow) { + // console.warn('Ctrl/Cmd+Arrow keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId); + return true; + } + } + + if ((partModifiersMask & modifiersMask) === BinaryKeybindingsMask.CtrlCmd) { + if (chord instanceof KeyCodeChord && chord.keyCode >= KeyCode.Digit0 && chord.keyCode <= KeyCode.Digit9) { + // console.warn('Ctrl/Cmd+Num keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId); + return true; + } + } + } + + return false; + } + + private _registerKeyboardEvent(): IDisposable { + const disposables = new DisposableStore(); + + disposables.add( + addDisposableListener(document, DOMEventType.KEY_DOWN, (e) => { + if (this._keybindingHoldMode) { + return; + } + this._isCompostion = e.isComposing; + const keyEvent = new StandardKeyboardEvent(e); + const shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target); + if (shouldPreventDefault) { + keyEvent.preventDefault(); + } + this._isCompostion = false; + }), + ); + + disposables.add( + addDisposableListener(document, DOMEventType.KEY_UP, (e) => { + this._resetKeybindingHoldMode(); + this._isCompostion = e.isComposing; + const keyEvent = new StandardKeyboardEvent(e); + const shouldPreventDefault = this._singleModifierDispatch(keyEvent, keyEvent.target); + if (shouldPreventDefault) { + keyEvent.preventDefault(); + } + this._isCompostion = false; + }), + ); + + return disposables; + } + + private _dispatch(e: IKeyboardEvent, target: HTMLElement): boolean { + return this._doDispatch(this.resolveKeyboardEvent(e), target, false); + } + + private _doDispatch(userKeypress: ResolvedKeybinding, target: HTMLElement, isSingleModiferChord = false): boolean { + let shouldPreventDefault = false; + + if (userKeypress.hasMultipleChords()) { + // warn - because user can press a single chord at a time + console.warn('Unexpected keyboard event mapped to multiple chords'); + return false; + } + + let userPressedChord: string | null = null; + let currentChords: string[] | null = null; + + if (isSingleModiferChord) { + // The keybinding is the second keypress of a single modifier chord, e.g. "shift shift". + // A single modifier can only occur when the same modifier is pressed in short sequence, + // hence we disregard `_currentChord` and use the same modifier instead. + const [dispatchKeyname] = userKeypress.getSingleModifierDispatchChords(); + userPressedChord = dispatchKeyname; + currentChords = dispatchKeyname ? [dispatchKeyname] : []; // TODO@ulugbekna: in the `else` case we assign an empty array - make sure `resolve` can handle an empty array well + } else { + [userPressedChord] = userKeypress.getDispatchChords(); + currentChords = this._currentChords.map(({ keypress }) => keypress); + } + + if (userPressedChord === null) { + console.log(`\\ Keyboard event cannot be dispatched in keydown phase.`); + // cannot be dispatched, probably only modifier keys + return shouldPreventDefault; + } + + const keypressLabel = userKeypress.getLabel(); + const resolveResult = this._getResolver().resolve(currentChords, userPressedChord); + + switch (resolveResult.kind) { + case ResultKind.NoMatchingKb: { + if (this.inChordMode) { + const currentChordsLabel = this._currentChords.map(({ label }) => label).join(', '); + console.log(`+ Leaving multi-chord mode: Nothing bound to "${currentChordsLabel}, ${keypressLabel}".`); - registerSchemaContribution(contribution: KeybindingsSchemaContribution): void; + this._leaveChordMode(); - toggleLogging(): boolean; + shouldPreventDefault = true; + } + return shouldPreventDefault; + } - _dumpDebugInfo(): string; - _dumpDebugInfoJSON(): string; + case ResultKind.MoreChordsNeeded: { + shouldPreventDefault = true; + this._expectAnotherChord(userPressedChord, keypressLabel); + console.log( + this._currentChords.length === 1 ? `+ Entering multi-chord mode...` : `+ Continuing multi-chord mode...`, + ); + return shouldPreventDefault; + } + + case ResultKind.KbFound: { + if (resolveResult.commandId === null || resolveResult.commandId === '') { + if (this.inChordMode) { + const currentChordsLabel = this._currentChords.map(({ label }) => label).join(', '); + console.log(`+ Leaving chord mode: Nothing bound to "${currentChordsLabel}, ${keypressLabel}".`); + this._leaveChordMode(); + shouldPreventDefault = true; + } + } else { + if (this.inChordMode) { + this._leaveChordMode(); + } + + if (!resolveResult.isBubble) { + shouldPreventDefault = true; + } + + console.log(`+ Invoking command ${resolveResult.commandId}.`); + this._currentlyDispatchingCommandId = resolveResult.commandId; + try { + this.commandService + .executeCommand( + resolveResult.commandId, + typeof resolveResult.commandArgs === 'undefined' ? undefined : resolveResult.commandArgs, + ) + .then(undefined, (err) => console.warn(err)); + } finally { + this._currentlyDispatchingCommandId = null; + } + } + + return shouldPreventDefault; + } + } + } + + private _leaveChordMode(): void { + this._currentChordChecker.cancel(); + this._currentChords = []; + } + + private _expectAnotherChord(firstChord: string, keypressLabel: string | null): void { + this._currentChords.push({ keypress: firstChord, label: keypressLabel }); + + switch (this._currentChords.length) { + case 0: + throw illegalState('impossible'); + case 1: + // TODO@ulugbekna: revise this message and the one below (at least, fix terminology) + // this._currentChordStatusMessage = this._notificationService.status( + // nls.localize('first.chord', '({0}) was pressed. Waiting for second key of chord...', keypressLabel), + // ); + break; + default: { + // const fullKeypressLabel = this._currentChords.map(({ label }) => label).join(', '); + // this._currentChordStatusMessage = this._notificationService.status( + // nls.localize('next.chord', '({0}) was pressed. Waiting for next key of chord...', fullKeypressLabel), + // ); + } + } + + this._scheduleLeaveChordMode(); + } + + private _scheduleLeaveChordMode(): void { + const chordLastInteractedTime = Date.now(); + this._currentChordChecker.cancelAndSet(() => { + if (Date.now() - chordLastInteractedTime > 5000) { + // 5 seconds elapsed => leave chord mode + this._leaveChordMode(); + } + }, 500); + } + + private _singleModifierDispatch(e: IKeyboardEvent, target: HTMLElement): boolean { + const keybinding = this.resolveKeyboardEvent(e); + const [singleModifier] = keybinding.getSingleModifierDispatchChords(); + + if (singleModifier) { + if (this._ignoreSingleModifiers.has(singleModifier)) { + console.log(`+ Ignoring single modifier ${singleModifier} due to it being pressed together with other keys.`); + this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY; + this._currentSingleModifierClearTimeout.cancel(); + this._currentSingleModifier = null; + return false; + } + + this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY; + + if (this._currentSingleModifier === null) { + // we have a valid `singleModifier`, store it for the next keyup, but clear it in 300ms + console.log(`+ Storing single modifier for possible chord ${singleModifier}.`); + this._currentSingleModifier = singleModifier; + this._currentSingleModifierClearTimeout.cancelAndSet(() => { + console.log(`+ Clearing single modifier due to 300ms elapsed.`); + this._currentSingleModifier = null; + }, 300); + return false; + } + + if (singleModifier === this._currentSingleModifier) { + // bingo! + console.log(`/ Dispatching single modifier chord ${singleModifier} ${singleModifier}`); + this._currentSingleModifierClearTimeout.cancel(); + this._currentSingleModifier = null; + return this._doDispatch(keybinding, target, /*isSingleModiferChord*/ true); + } + + console.log( + `+ Clearing single modifier due to modifier mismatch: ${this._currentSingleModifier} ${singleModifier}`, + ); + this._currentSingleModifierClearTimeout.cancel(); + this._currentSingleModifier = null; + return false; + } + + // When pressing a modifier and holding it pressed with any other modifier or key combination, + // the pressed modifiers should no longer be considered for single modifier dispatch. + const [firstChord] = keybinding.getChords(); + this._ignoreSingleModifiers = new KeybindingModifierSet(firstChord); + + if (this._currentSingleModifier !== null) { + console.log(`+ Clearing single modifier due to other key up.`); + } + this._currentSingleModifierClearTimeout.cancel(); + this._currentSingleModifier = null; + return false; + } + + enableKeybindingHoldMode(commandId: string): Promise | undefined { + if (this._currentlyDispatchingCommandId !== commandId) { + return undefined; + } + + this._keybindingHoldMode = new DeferredPromise(); + + return this._keybindingHoldMode.p; + } + + private _resetKeybindingHoldMode(): void { + if (this._keybindingHoldMode) { + this._keybindingHoldMode?.complete(); + this._keybindingHoldMode = null; + } + } + + resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[] { + return this._keyboardMapper.resolveKeybinding(keybinding); + } + + resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding { + return this._keyboardMapper.resolveKeyboardEvent(keyboardEvent); + } + + resolveUserBinding(userBinding: string): ResolvedKeybinding[] { + const keybinding = KeybindingParser.parseKeybinding(userBinding); + return keybinding ? this._keyboardMapper.resolveKeybinding(keybinding) : []; + } + + dispatchEvent(e: IKeyboardEvent, target: HTMLElement): boolean { + return this._dispatch(e, target); + } + + dispatchByUserSettingsLabel(userSettingsLabel: string, target: HTMLElement): void { + const keybindings = this.resolveUserBinding(userSettingsLabel); + if (keybindings.length === 0) { + console.log(`\\ Could not resolve - ${userSettingsLabel}`); + } else { + this._doDispatch(keybindings[0], target, /*isSingleModiferChord*/ false); + } + } + + lookupKeybindings(commandId: string): ResolvedKeybinding[] { + return this._getResolver() + .lookupKeybindings(commandId) + .map((item) => item.resolvedKeybinding) + .filter(Boolean) as ResolvedKeybinding[]; + } + + getDefaultKeybindings(): readonly ResolvedKeybindingItem[] { + return this._getResolver().getDefaultKeybindings(); + } + + getKeybindings(): readonly ResolvedKeybindingItem[] { + return this._getResolver().getKeybindings(); + } } -export const IKeybindingService = createDecorator('keybindingService'); +class KeybindingModifierSet { + public static EMPTY = new KeybindingModifierSet(null); + + private readonly _ctrlKey: boolean; + private readonly _shiftKey: boolean; + private readonly _altKey: boolean; + private readonly _metaKey: boolean; + + constructor(source: ResolvedChord | null) { + this._ctrlKey = source ? source.ctrlKey : false; + this._shiftKey = source ? source.shiftKey : false; + this._altKey = source ? source.altKey : false; + this._metaKey = source ? source.metaKey : false; + } + + has(modifier: SingleModifierChord) { + switch (modifier) { + case 'ctrl': + return this._ctrlKey; + case 'shift': + return this._shiftKey; + case 'alt': + return this._altKey; + case 'meta': + return this._metaKey; + } + } +} diff --git a/packages/engine-core/src/keybinding/keybindings.ts b/packages/engine-core/src/keybinding/keybindings.ts index 6c521f87d..821f26074 100644 --- a/packages/engine-core/src/keybinding/keybindings.ts +++ b/packages/engine-core/src/keybinding/keybindings.ts @@ -1,5 +1,6 @@ import { illegalArgument, OperatingSystem } from '@alilc/lowcode-shared'; -import { KeyCode, ScanCode } from '../common/keyCodes'; +import { KeyCode } from '../common/keyCodes'; +import { AriaLabelProvider, UILabelProvider, UserSettingsLabelProvider } from './keybingdingLabels'; /** * Binary encoding strategy: @@ -14,7 +15,7 @@ import { KeyCode, ScanCode } from '../common/keyCodes'; * K = bits 0-7 = key code * ``` */ -const enum BinaryKeybindingsMask { +export const enum BinaryKeybindingsMask { CtrlCmd = (1 << 11) >>> 0, Shift = (1 << 10) >>> 0, Alt = (1 << 9) >>> 0, @@ -22,28 +23,27 @@ const enum BinaryKeybindingsMask { KeyCode = 0x000000ff, } -export function decodeKeybinding( - keybinding: number | number[], - OS: OperatingSystem, -): Keybinding | null { +export function decodeKeybinding(keybinding: number | number[], OS: OperatingSystem): Keybinding | null { if (typeof keybinding === 'number') { if (keybinding === 0) { return null; } + const firstChord = (keybinding & 0x0000ffff) >>> 0; const secondChord = (keybinding & 0xffff0000) >>> 16; + if (secondChord !== 0) { - return new Keybinding([ - createSimpleKeybinding(firstChord, OS), - createSimpleKeybinding(secondChord, OS), - ]); + return new Keybinding([createSimpleKeybinding(firstChord, OS), createSimpleKeybinding(secondChord, OS)]); } + return new Keybinding([createSimpleKeybinding(firstChord, OS)]); } else { const chords = []; + for (let i = 0; i < keybinding.length; i++) { chords.push(createSimpleKeybinding(keybinding[i], OS)); } + return new Keybinding(chords); } } @@ -81,7 +81,7 @@ export class KeyCodeChord implements Modifiers { public readonly keyCode: KeyCode, ) {} - equals(other: Chord): boolean { + equals(other: KeyCodeChord): boolean { return ( other instanceof KeyCodeChord && this.ctrlKey === other.ctrlKey && @@ -127,64 +127,13 @@ export class KeyCodeChord implements Modifiers { } } -/** - * Represents a chord which uses the `code` field of keyboard events. - * A chord is a combination of keys pressed simultaneously. - */ -export class ScanCodeChord implements Modifiers { - constructor( - public readonly ctrlKey: boolean, - public readonly shiftKey: boolean, - public readonly altKey: boolean, - public readonly metaKey: boolean, - public readonly scanCode: ScanCode, - ) {} - - equals(other: Chord): boolean { - return ( - other instanceof ScanCodeChord && - this.ctrlKey === other.ctrlKey && - this.shiftKey === other.shiftKey && - this.altKey === other.altKey && - this.metaKey === other.metaKey && - this.scanCode === other.scanCode - ); - } - - getHashCode(): string { - const ctrl = this.ctrlKey ? '1' : '0'; - const shift = this.shiftKey ? '1' : '0'; - const alt = this.altKey ? '1' : '0'; - const meta = this.metaKey ? '1' : '0'; - return `S${ctrl}${shift}${alt}${meta}${this.scanCode}`; - } - - /** - * Does this keybinding refer to the key code of a modifier and it also has the modifier flag? - */ - isDuplicateModifierCase(): boolean { - return ( - (this.ctrlKey && - (this.scanCode === ScanCode.ControlLeft || this.scanCode === ScanCode.ControlRight)) || - (this.shiftKey && - (this.scanCode === ScanCode.ShiftLeft || this.scanCode === ScanCode.ShiftRight)) || - (this.altKey && - (this.scanCode === ScanCode.AltLeft || this.scanCode === ScanCode.AltRight)) || - (this.metaKey && - (this.scanCode === ScanCode.MetaLeft || this.scanCode === ScanCode.MetaRight)) - ); - } -} - -export type Chord = KeyCodeChord | ScanCodeChord; - /** * A keybinding is a sequence of chords. */ export class Keybinding { - readonly chords: Chord[]; + readonly chords: KeyCodeChord[]; - constructor(chords: Chord[]) { + constructor(chords: KeyCodeChord[]) { if (chords.length === 0) { throw illegalArgument(`chords`); } @@ -243,11 +192,6 @@ export abstract class ResolvedKeybinding { * This prints the binding in a format suitable for ARIA. */ public abstract getAriaLabel(): string | null; - /** - * This prints the binding in a format suitable for electron's accelerators. - * See https://github.com/electron/electron/blob/master/docs/api/accelerator.md - */ - public abstract getElectronAccelerator(): string | null; /** * This prints the binding in a format suitable for user settings. */ @@ -279,3 +223,65 @@ export abstract class ResolvedKeybinding { */ public abstract getSingleModifierDispatchChords(): (SingleModifierChord | null)[]; } + +export abstract class BaseResolvedKeybinding extends ResolvedKeybinding { + protected readonly _chords: readonly KeyCodeChord[]; + + constructor(chords: readonly KeyCodeChord[]) { + super(); + if (chords.length === 0) { + throw illegalArgument(`chords`); + } + this._chords = chords; + } + + public getLabel(): string | null { + return UILabelProvider.toLabel(this._chords, (keybinding) => this._getLabel(keybinding)); + } + + public getAriaLabel(): string | null { + return AriaLabelProvider.toLabel(this._chords, (keybinding) => this._getAriaLabel(keybinding)); + } + + public getUserSettingsLabel(): string | null { + return UserSettingsLabelProvider.toLabel(this._chords, (keybinding) => this._getUserSettingsLabel(keybinding)); + } + + public isWYSIWYG(): boolean { + return this._chords.every((keybinding) => this._isWYSIWYG(keybinding)); + } + + public hasMultipleChords(): boolean { + return this._chords.length > 1; + } + + public getChords(): ResolvedChord[] { + return this._chords.map((keybinding) => this._getChord(keybinding)); + } + + private _getChord(keybinding: KeyCodeChord): ResolvedChord { + return new ResolvedChord( + keybinding.ctrlKey, + keybinding.shiftKey, + keybinding.altKey, + keybinding.metaKey, + this._getLabel(keybinding), + this._getAriaLabel(keybinding), + ); + } + + public getDispatchChords(): (string | null)[] { + return this._chords.map((keybinding) => this._getChordDispatch(keybinding)); + } + + public getSingleModifierDispatchChords(): (SingleModifierChord | null)[] { + return this._chords.map((keybinding) => this._getSingleModifierChordDispatch(keybinding)); + } + + protected abstract _getLabel(keybinding: KeyCodeChord): string | null; + protected abstract _getAriaLabel(keybinding: KeyCodeChord): string | null; + protected abstract _getUserSettingsLabel(keybinding: KeyCodeChord): string | null; + protected abstract _isWYSIWYG(keybinding: KeyCodeChord): boolean; + protected abstract _getChordDispatch(keybinding: KeyCodeChord): string | null; + protected abstract _getSingleModifierChordDispatch(keybinding: KeyCodeChord): SingleModifierChord | null; +} diff --git a/packages/engine-core/src/keybinding/keybingdingLabels.ts b/packages/engine-core/src/keybinding/keybingdingLabels.ts new file mode 100644 index 000000000..d8b877909 --- /dev/null +++ b/packages/engine-core/src/keybinding/keybingdingLabels.ts @@ -0,0 +1,156 @@ +import { OperatingSystem, OS } from '@alilc/lowcode-shared'; +import { Modifiers } from './keybindings'; + +export interface ModifierLabels { + readonly ctrlKey: string; + readonly shiftKey: string; + readonly altKey: string; + readonly metaKey: string; + readonly separator: string; +} + +export interface KeyLabelProvider { + (keybinding: T): string | null; +} + +export class ModifierLabelProvider { + public readonly modifierLabels: ModifierLabels[]; + + constructor(mac: ModifierLabels, windows: ModifierLabels, linux: ModifierLabels = windows) { + this.modifierLabels = [null!]; // index 0 will never me accessed. + this.modifierLabels[OperatingSystem.Macintosh] = mac; + this.modifierLabels[OperatingSystem.Windows] = windows; + this.modifierLabels[OperatingSystem.Linux] = linux; + } + + public toLabel(chords: readonly T[], keyLabelProvider: KeyLabelProvider): string | null { + if (chords.length === 0) { + return null; + } + + const result: string[] = []; + for (let i = 0, len = chords.length; i < len; i++) { + const chord = chords[i]; + const keyLabel = keyLabelProvider(chord); + if (keyLabel === null) { + // this keybinding cannot be expressed... + return null; + } + result[i] = _simpleAsString(chord, keyLabel, this.modifierLabels[OS]); + } + return result.join(' '); + } +} + +function _simpleAsString(modifiers: Modifiers, key: string, labels: ModifierLabels): string { + if (key === null) { + return ''; + } + + const result: string[] = []; + + // translate modifier keys: Ctrl-Shift-Alt-Meta + if (modifiers.ctrlKey) { + result.push(labels.ctrlKey); + } + + if (modifiers.shiftKey) { + result.push(labels.shiftKey); + } + + if (modifiers.altKey) { + result.push(labels.altKey); + } + + if (modifiers.metaKey) { + result.push(labels.metaKey); + } + + // the actual key + if (key !== '') { + result.push(key); + } + + return result.join(labels.separator); +} + +/** + * A label provider that prints modifiers in a suitable format for displaying in the UI. + */ +export const UILabelProvider = new ModifierLabelProvider( + { + ctrlKey: '\u2303', + shiftKey: '⇧', + altKey: '⌥', + metaKey: '⌘', + separator: '', + }, + { + ctrlKey: 'Ctrl', + shiftKey: 'Shift', + altKey: 'Alt', + metaKey: 'Windows', + separator: '+', + }, + { + ctrlKey: 'Ctrl', + shiftKey: 'Shift', + altKey: 'Alt', + metaKey: 'Super', + separator: '+', + }, +); + +/** + * A label provider that prints modifiers in a suitable format for ARIA. + */ +export const AriaLabelProvider = new ModifierLabelProvider( + { + ctrlKey: 'Control', + shiftKey: 'Shift', + altKey: 'Option', + metaKey: 'Command', + separator: '+', + }, + { + ctrlKey: 'Control', + shiftKey: 'Shift', + altKey: 'Alt', + metaKey: 'Windows', + separator: '+', + }, + { + ctrlKey: 'Control', + shiftKey: 'Shift', + altKey: 'Alt', + metaKey: 'Super', + separator: '+', + }, +); + +/** + * A label provider that prints modifiers in a suitable format for user settings. + */ +export const UserSettingsLabelProvider = new ModifierLabelProvider( + { + ctrlKey: 'ctrl', + shiftKey: 'shift', + altKey: 'alt', + metaKey: 'cmd', + separator: '+', + }, + { + ctrlKey: 'ctrl', + shiftKey: 'shift', + altKey: 'alt', + metaKey: 'win', + separator: '+', + }, + { + ctrlKey: 'ctrl', + shiftKey: 'shift', + altKey: 'alt', + metaKey: 'meta', + separator: '+', + }, +); diff --git a/packages/engine-core/src/keybinding/keyboardMapper.ts b/packages/engine-core/src/keybinding/keyboardMapper.ts new file mode 100644 index 000000000..4f638bf44 --- /dev/null +++ b/packages/engine-core/src/keybinding/keyboardMapper.ts @@ -0,0 +1,31 @@ +import { IKeyboardEvent } from './keybindingEvent'; +import { Keybinding, KeyCodeChord, ResolvedKeybinding } from './keybindings'; +import { USLayoutResolvedKeybinding } from './usLayoutResolvedKeybinding'; + +export interface IKeyboardMapper { + resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding; + resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[]; +} + +/** + * A keyboard mapper to be used when reading the keymap. + */ +export class USLayoutKeyboardMapper implements IKeyboardMapper { + public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding { + const ctrlKey = keyboardEvent.ctrlKey; + const altKey = keyboardEvent.altKey; + const chord = new KeyCodeChord( + ctrlKey, + keyboardEvent.shiftKey, + altKey, + keyboardEvent.metaKey, + keyboardEvent.keyCode, + ); + const result = this.resolveKeybinding(new Keybinding([chord])); + return result[0]; + } + + public resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[] { + return USLayoutResolvedKeybinding.resolveKeybinding(keybinding); + } +} diff --git a/packages/engine-core/src/keybinding/usLayoutResolvedKeybinding.ts b/packages/engine-core/src/keybinding/usLayoutResolvedKeybinding.ts new file mode 100644 index 000000000..30cac4262 --- /dev/null +++ b/packages/engine-core/src/keybinding/usLayoutResolvedKeybinding.ts @@ -0,0 +1,103 @@ +import { OperatingSystem, OS } from '@alilc/lowcode-shared'; +import { BaseResolvedKeybinding, Keybinding, KeyCodeChord, SingleModifierChord } from './keybindings'; +import { KeyCode, KeyCodeUtils } from '../common'; +import { toEmptyArrayIfContainsNull } from './keybindingResolver'; + +export class USLayoutResolvedKeybinding extends BaseResolvedKeybinding { + public static resolveKeybinding(keybinding: Keybinding): USLayoutResolvedKeybinding[] { + const chords: KeyCodeChord[] = toEmptyArrayIfContainsNull(keybinding.chords); + if (chords.length > 0) { + return [new USLayoutResolvedKeybinding(chords)]; + } + return []; + } + + constructor(chords: KeyCodeChord[]) { + super(chords); + } + + private _keyCodeToUILabel(keyCode: KeyCode): string { + if (OS === OperatingSystem.Macintosh) { + switch (keyCode) { + case KeyCode.LeftArrow: + return '←'; + case KeyCode.UpArrow: + return '↑'; + case KeyCode.RightArrow: + return '→'; + case KeyCode.DownArrow: + return '↓'; + } + } + return KeyCodeUtils.toString(keyCode); + } + + protected _getLabel(chord: KeyCodeChord): string | null { + if (chord.isDuplicateModifierCase()) { + return ''; + } + return this._keyCodeToUILabel(chord.keyCode); + } + + protected _getAriaLabel(chord: KeyCodeChord): string | null { + if (chord.isDuplicateModifierCase()) { + return ''; + } + return KeyCodeUtils.toString(chord.keyCode); + } + + protected _getUserSettingsLabel(chord: KeyCodeChord): string | null { + if (chord.isDuplicateModifierCase()) { + return ''; + } + const result = KeyCodeUtils.toUserSettingsUS(chord.keyCode); + return result ? result.toLowerCase() : result; + } + + protected _isWYSIWYG(): boolean { + return true; + } + + protected _getChordDispatch(chord: KeyCodeChord): string | null { + return USLayoutResolvedKeybinding.getDispatchStr(chord); + } + + public static getDispatchStr(chord: KeyCodeChord): string | null { + if (chord.isModifierKey()) { + return null; + } + let result = ''; + + if (chord.ctrlKey) { + result += 'ctrl+'; + } + if (chord.shiftKey) { + result += 'shift+'; + } + if (chord.altKey) { + result += 'alt+'; + } + if (chord.metaKey) { + result += 'meta+'; + } + result += KeyCodeUtils.toString(chord.keyCode); + + return result; + } + + protected _getSingleModifierChordDispatch(keybinding: KeyCodeChord): SingleModifierChord | null { + if (keybinding.keyCode === KeyCode.Ctrl && !keybinding.shiftKey && !keybinding.altKey && !keybinding.metaKey) { + return 'ctrl'; + } + if (keybinding.keyCode === KeyCode.Shift && !keybinding.ctrlKey && !keybinding.altKey && !keybinding.metaKey) { + return 'shift'; + } + if (keybinding.keyCode === KeyCode.Alt && !keybinding.ctrlKey && !keybinding.shiftKey && !keybinding.metaKey) { + return 'alt'; + } + if (keybinding.keyCode === KeyCode.Meta && !keybinding.ctrlKey && !keybinding.shiftKey && !keybinding.altKey) { + return 'meta'; + } + return null; + } +} diff --git a/packages/engine-core/src/main.ts b/packages/engine-core/src/main.ts index da9bbd813..04bb80026 100644 --- a/packages/engine-core/src/main.ts +++ b/packages/engine-core/src/main.ts @@ -1,10 +1,15 @@ import { InstantiationService, BeanContainer, CtorDescriptor } from '@alilc/lowcode-shared'; +import { URI } from './common/uri'; +import * as Schemas from './common/schemas'; + +import { CommandService, ICommandService } from './command'; +import { IKeybindingService, KeybindingService } from './keybinding'; import { ConfigurationService, IConfigurationService } from './configuration'; +import { IExtensionService, ExtensionService } from './extension'; import { IWorkspaceService, WorkspaceService, toWorkspaceIdentifier } from './workspace'; import { IWindowService, WindowService } from './window'; import { IFileService, FileService, InMemoryFileSystemProvider } from './file'; -import { URI } from './common/uri'; -import * as Schemas from './common/schemas'; +import { IResourceService, ResourceService } from './resource'; class TestMainApplication { instantiationService: InstantiationService; @@ -25,11 +30,14 @@ class TestMainApplication { fileService.registerProvider(Schemas.file, new InMemoryFileSystemProvider()); try { - const uri = URI.from({ path: '/Desktop' }); + const root = URI.from({ scheme: Schemas.file, path: '/' }); - await workspaceService.enterWorkspace(toWorkspaceIdentifier(uri.path)); + // empty or mutiple files + // 展示目录结构 + const workspace = await workspaceService.enterWorkspace(toWorkspaceIdentifier(root.path)); - const fileUri = URI.joinPath(uri, 'test.lc'); + // 打开页面 or 保留空白页 + const fileUri = URI.joinPath(workspace.uri, 'test.lc'); await windowService.open({ urisToOpen: [{ fileUri }], @@ -46,6 +54,13 @@ class TestMainApplication { const configurationService = new ConfigurationService(); container.set(IConfigurationService, configurationService); + const resourceService = new ResourceService(); + container.set(IResourceService, resourceService); + + container.set(ICommandService, new CtorDescriptor(CommandService)); + container.set(IKeybindingService, new CtorDescriptor(KeybindingService)); + container.set(IExtensionService, new CtorDescriptor(ExtensionService)); + const workspaceService = new WorkspaceService(); container.set(IWorkspaceService, workspaceService); diff --git a/packages/engine-core/src/window/window.ts b/packages/engine-core/src/window/window.ts index 0e82f827b..a87dbc33e 100644 --- a/packages/engine-core/src/window/window.ts +++ b/packages/engine-core/src/window/window.ts @@ -40,7 +40,7 @@ export interface IPath { * file exists, if false it does not. with * `undefined` the state is unknown. */ - readonly exists?: boolean; + readonly exists: boolean; /** * Optional editor options to apply in the file @@ -52,6 +52,8 @@ export interface IWindowConfiguration { fileToOpenOrCreate: IPath; workspace?: IWorkspaceIdentifier; + + contentType: string; } export interface IEditWindow extends IDisposable { @@ -70,10 +72,11 @@ export interface IEditWindow extends IDisposable { readonly isReady: boolean; ready(): Promise; - load(config: IWindowConfiguration, options?: { isReload?: boolean }): void; - reload(): void; + load(config: IWindowConfiguration, options?: { isReload?: boolean }): Promise; + reload(): Promise; close(): void; + destory(): Promise; sendWhenReady(channel: string, ...args: any[]): void; } diff --git a/packages/engine-core/src/window/windowImpl.ts b/packages/engine-core/src/window/windowImpl.ts index 6171e2b25..fffba42de 100644 --- a/packages/engine-core/src/window/windowImpl.ts +++ b/packages/engine-core/src/window/windowImpl.ts @@ -1,6 +1,9 @@ import { Disposable, Events } from '@alilc/lowcode-shared'; import { IWindowState, IEditWindow, IWindowConfiguration } from './window'; import { IFileService } from '../file'; +import { Registry, Extensions } from '../extension/registry'; +import { IContentEditorRegistry } from '../contentEditor/contentEditorRegistry'; +import { IContentEditor } from '../contentEditor/contentEditor'; export interface IWindowCreationOptions { readonly state: IWindowState; @@ -76,18 +79,45 @@ export class EditWindow extends Disposable implements IEditWindow { } } - load(config: IWindowConfiguration): void { + async load(config: IWindowConfiguration): Promise { + const { fileToOpenOrCreate, contentType } = config; + const { exists, fileUri } = fileToOpenOrCreate; + + const contentEditor = Registry.as(Extensions.ContentEditor).getContentEditor(contentType); + + if (!contentEditor) { + throw Error('content editor not found'); + } + + let content: string = ''; + + const fs = this.fileService.withProvider(fileToOpenOrCreate.fileUri)!; + if (!exists) { + await fs.writeFile(fileUri, content); + } else { + content = await fs.readFile(fileUri); + } + + contentEditor.load(content); + this._onWillLoad.notify(); this._config = config; + + this.setReady(); } - reload(): void {} + async reload(): Promise { + await this.destory(); + return this.load(this._config!); + } focus(): void {} close(): void {} + async destory(): Promise {} + sendWhenReady(channel: string, ...args: any[]): void { if (this.isReady) { this.send(channel, ...args); diff --git a/packages/engine-core/src/window/windowManagement.ts b/packages/engine-core/src/window/windowManagement.ts new file mode 100644 index 000000000..35ef7a8c6 --- /dev/null +++ b/packages/engine-core/src/window/windowManagement.ts @@ -0,0 +1 @@ +export interface IWindowManagemant {} diff --git a/packages/engine-core/src/window/windowManagementService.ts b/packages/engine-core/src/window/windowManagementService.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/engine-core/src/window/windowService.ts b/packages/engine-core/src/window/windowService.ts index e44515440..ad8174f99 100644 --- a/packages/engine-core/src/window/windowService.ts +++ b/packages/engine-core/src/window/windowService.ts @@ -1,8 +1,10 @@ import { createDecorator, Disposable, Events, IInstantiationService } from '@alilc/lowcode-shared'; import { defaultWindowState, IEditWindow, IWindowConfiguration } from './window'; -import { Schemas, URI } from '../common'; +import { Schemas, URI, extname } from '../common'; import { EditWindow } from './windowImpl'; import { IFileService } from '../file'; +import { Extensions, Registry } from '../extension/registry'; +import { IContentEditorRegistry } from '../contentEditor/contentEditorRegistry'; export interface IOpenConfiguration { readonly urisToOpen: IWindowOpenable[]; @@ -30,9 +32,9 @@ export interface IWindowService { open(openConfig: IOpenConfiguration): Promise; - // sendToFocused(channel: string, ...args: any[]): void; - // sendToOpeningWindow(channel: string, ...args: any[]): void; - // sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void; + sendToFocused(channel: string, ...args: any[]): void; + sendToOpeningWindow(channel: string, ...args: any[]): void; + sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void; getWindows(): IEditWindow[]; getWindowCount(): number; @@ -103,16 +105,24 @@ export class WindowService extends Disposable implements IWindowService { if (openOnlyIfExists) continue; } - const config: IWindowConfiguration = { - fileToOpenOrCreate: { - fileUri: item.fileUri, - exists, - options: {}, - }, - }; - const window = await this._openInEditWindow(config); - - usedWindows.push(window); + const fileExt = extname(item.fileUri.path); + const registeredContentType = Registry.as(Extensions.ContentEditor).getContentTypeByExt( + fileExt, + ); + + if (registeredContentType) { + const config: IWindowConfiguration = { + fileToOpenOrCreate: { + fileUri: item.fileUri, + exists, + options: {}, + }, + contentType: registeredContentType, + }; + const window = await this._openInEditWindow(config); + + usedWindows.push(window); + } } return usedWindows; @@ -128,6 +138,19 @@ export class WindowService extends Disposable implements IWindowService { newWindow.load(config); + newWindow.onDidDestroy(() => { + this.windows.delete(newWindow.id); + }); + return newWindow; } + + sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void {} + + sendToFocused(channel: string, ...args: any[]): void { + const focusedWindow = this.getLastActiveWindow(); + focusedWindow?.sendWhenReady(channel, ...args); + } + + sendToOpeningWindow(channel: string, ...args: any[]): void {} } diff --git a/packages/engine-core/src/workspace/workspaceService.ts b/packages/engine-core/src/workspace/workspaceService.ts index 8e9ceea80..c18c83b57 100644 --- a/packages/engine-core/src/workspace/workspaceService.ts +++ b/packages/engine-core/src/workspace/workspaceService.ts @@ -1,22 +1,19 @@ import { createDecorator, Disposable } from '@alilc/lowcode-shared'; import { Workspace, type IWorkspaceIdentifier, isWorkspaceIdentifier } from './workspace'; -import { toWorkspaceFolder, IWorkspaceFolder } from './workspaceFolder'; -import { URI } from '../common'; +import { toWorkspaceFolder } from './workspaceFolder'; export interface IWorkspaceService { initialize(): Promise; - enterWorkspace(identifier: IWorkspaceIdentifier): Promise; + enterWorkspace(identifier: IWorkspaceIdentifier): Promise; - getWorkspace(): Workspace; - - getWorkspaceFolder(resource: URI): IWorkspaceFolder | null; + getWorkspace(id: string | IWorkspaceIdentifier): Workspace | undefined; } export const IWorkspaceService = createDecorator('workspaceService'); export class WorkspaceService extends Disposable implements IWorkspaceService { - private _workspace: Workspace; + private _workspacesMap = new Map(); constructor() { super(); @@ -24,19 +21,23 @@ export class WorkspaceService extends Disposable implements IWorkspaceService { async initialize() {} - async enterWorkspace(identifier: IWorkspaceIdentifier) { + async enterWorkspace(identifier: IWorkspaceIdentifier): Promise { if (!isWorkspaceIdentifier(identifier)) { throw new Error('Invalid workspace identifier'); } - this._workspace = new Workspace(identifier.id, identifier.uri, [toWorkspaceFolder(identifier.uri)]); - } + const workspace = new Workspace(identifier.id, identifier.uri, [toWorkspaceFolder(identifier.uri)]); + + this._workspacesMap.set(identifier.id, workspace); - getWorkspace(): Workspace { - return this._workspace; + return workspace; } - getWorkspaceFolder(resource: URI) { - return this._workspace.getFolder(resource); + getWorkspace(identifier: string | IWorkspaceIdentifier): Workspace | undefined { + if (isWorkspaceIdentifier(identifier)) { + return this._workspacesMap.get(identifier.id); + } + + return this._workspacesMap.get(identifier); } } diff --git a/packages/renderer-core/src/extension/extensionHostService.ts b/packages/renderer-core/src/extension/extensionHostService.ts index 4a81ab70d..d308b1d9e 100644 --- a/packages/renderer-core/src/extension/extensionHostService.ts +++ b/packages/renderer-core/src/extension/extensionHostService.ts @@ -15,8 +15,7 @@ export interface IExtensionHostService { getPlugin(name: string): Plugin | undefined; } -export const IExtensionHostService = - createDecorator('pluginManagementService'); +export const IExtensionHostService = createDecorator('pluginManagementService'); export class ExtensionHostService extends Disposable implements IExtensionHostService { boostsManager: BoostsManager; @@ -35,11 +34,7 @@ export class ExtensionHostService extends Disposable implements IExtensionHostSe ) { super(); - this.boostsManager = new BoostsManager( - codeRuntimeService, - runtimeIntlService, - runtimeUtilService, - ); + this.boostsManager = new BoostsManager(codeRuntimeService, runtimeIntlService, runtimeUtilService); this._pluginSetupContext = { globalState: new Map(), diff --git a/packages/shared/src/common/instantiation/container.ts b/packages/shared/src/common/instantiation/container.ts index ec7201f51..a58f2838e 100644 --- a/packages/shared/src/common/instantiation/container.ts +++ b/packages/shared/src/common/instantiation/container.ts @@ -11,6 +11,7 @@ export class CtorDescriptor { constructor( readonly ctor: Constructor, readonly staticArguments: any[] = [], + readonly supportsDelayedInstantiation: boolean = false, ) {} } diff --git a/packages/shared/src/common/instantiation/instantiationService.ts b/packages/shared/src/common/instantiation/instantiationService.ts index fe5d64a12..c2b0e16db 100644 --- a/packages/shared/src/common/instantiation/instantiationService.ts +++ b/packages/shared/src/common/instantiation/instantiationService.ts @@ -194,7 +194,12 @@ export class InstantiationService implements IInstantiationService { const instanceOrDesc = this._container.get(data.id); if (instanceOrDesc instanceof CtorDescriptor) { // create instance and overwrite the service collections - const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments); + const instance = this._createServiceInstanceWithOwner( + data.id, + data.desc.ctor, + data.desc.staticArguments, + data.desc.supportsDelayedInstantiation, + ); this._setCreatedServiceInstance(data.id, instance); } graph.removeNode(data); @@ -205,14 +210,23 @@ export class InstantiationService implements IInstantiationService { return this._container.get(id) as T; } - private _createServiceInstanceWithOwner(id: BeanIdentifier, ctor: any, args: any[]): T { + private _createServiceInstanceWithOwner( + id: BeanIdentifier, + ctor: any, + args: any[], + supportsDelayedInstantiation: boolean, + ): T { if (this._container.get(id) instanceof CtorDescriptor) { + if (supportsDelayedInstantiation) { + // todo + } + const instance = this.createInstance(ctor, args); this._beansToMaybeDispose.add(instance); return instance; } else if (this._parent) { - return this._parent._createServiceInstanceWithOwner(id, ctor, args); + return this._parent._createServiceInstanceWithOwner(id, ctor, args, supportsDelayedInstantiation); } else { throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`); } diff --git a/packages/shared/src/common/platform.ts b/packages/shared/src/common/platform.ts index 420d47034..49c3584fb 100644 --- a/packages/shared/src/common/platform.ts +++ b/packages/shared/src/common/platform.ts @@ -35,9 +35,10 @@ export function platformToString(platform: PlatformEnum): PlatformName { export const enum OperatingSystem { Windows = 1, Macintosh = 2, - Linux = 3 + Linux = 3, } -export const OS = (isMacintosh || isIOS ? OperatingSystem.Macintosh : (isWindows ? OperatingSystem.Windows : OperatingSystem.Linux)); +export const OS = + isMacintosh || isIOS ? OperatingSystem.Macintosh : isWindows ? OperatingSystem.Windows : OperatingSystem.Linux; export let platform: PlatformEnum = PlatformEnum.Unknown; if (isMacintosh) { @@ -48,8 +49,9 @@ if (isMacintosh) { platform = PlatformEnum.Linux; } -export const isChrome = !!(userAgent && userAgent.indexOf('Chrome') >= 0); -export const isFirefox = !!(userAgent && userAgent.indexOf('Firefox') >= 0); -export const isSafari = !!(!isChrome && userAgent && userAgent.indexOf('Safari') >= 0); -export const isEdge = !!(userAgent && userAgent.indexOf('Edg/') >= 0); -export const isAndroid = !!(userAgent && userAgent.indexOf('Android') >= 0); +export const isChrome = userAgent && userAgent.indexOf('Chrome') >= 0; +export const isWebKit = userAgent.indexOf('AppleWebKit') >= 0; +export const isFirefox = userAgent && userAgent.indexOf('Firefox') >= 0; +export const isSafari = !isChrome && userAgent && userAgent.indexOf('Safari') >= 0; +export const isEdge = userAgent && userAgent.indexOf('Edg/') >= 0; +export const isAndroid = userAgent && userAgent.indexOf('Android') >= 0; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index c6d1e2fc6..d16c7d67d 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -1,3 +1,2 @@ export * from './specs'; -export * from './json'; export * from './common'; diff --git a/packages/shared/src/types/json.ts b/packages/shared/src/types/json.ts deleted file mode 100644 index 13b23c814..000000000 --- a/packages/shared/src/types/json.ts +++ /dev/null @@ -1,86 +0,0 @@ -export type JSONValue = number | string | boolean | null; - -export interface JSONObject { - [key: string]: JSONValue | JSONObject | JSONObject[]; -} - -export type JSONSchemaType = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object'; - -/** - * fork from vscode - */ -export interface IJSONSchema { - id?: string; - $id?: string; - $schema?: string; - type?: JSONSchemaType | JSONSchemaType[]; - title?: string; - default?: any; - definitions?: IJSONSchemaMap; - description?: string; - properties?: IJSONSchemaMap; - patternProperties?: IJSONSchemaMap; - additionalProperties?: boolean | IJSONSchema; - minProperties?: number; - maxProperties?: number; - dependencies?: IJSONSchemaMap | { [prop: string]: string[] }; - items?: IJSONSchema | IJSONSchema[]; - minItems?: number; - maxItems?: number; - uniqueItems?: boolean; - additionalItems?: boolean | IJSONSchema; - pattern?: string; - minLength?: number; - maxLength?: number; - minimum?: number; - maximum?: number; - exclusiveMinimum?: boolean | number; - exclusiveMaximum?: boolean | number; - multipleOf?: number; - required?: string[]; - $ref?: string; - anyOf?: IJSONSchema[]; - allOf?: IJSONSchema[]; - oneOf?: IJSONSchema[]; - not?: IJSONSchema; - enum?: any[]; - format?: string; - - // schema draft 06 - const?: any; - contains?: IJSONSchema; - propertyNames?: IJSONSchema; - examples?: any[]; - - // schema draft 07 - $comment?: string; - if?: IJSONSchema; - then?: IJSONSchema; - else?: IJSONSchema; - - // schema 2019-09 - unevaluatedProperties?: boolean | IJSONSchema; - unevaluatedItems?: boolean | IJSONSchema; - minContains?: number; - maxContains?: number; - deprecated?: boolean; - dependentRequired?: { [prop: string]: string[] }; - dependentSchemas?: IJSONSchemaMap; - $defs?: { [name: string]: IJSONSchema }; - $anchor?: string; - $recursiveRef?: string; - $recursiveAnchor?: string; - $vocabulary?: any; - - // schema 2020-12 - prefixItems?: IJSONSchema[]; - $dynamicRef?: string; - $dynamicAnchor?: string; - - // internal extensions - errorMessage?: string; -} - -export interface IJSONSchemaMap { - [name: string]: IJSONSchema; -} diff --git a/packages/shared/src/types/specs/lowcode-spec.ts b/packages/shared/src/types/specs/lowcode-spec.ts index 3d934e1d2..ae342557d 100644 --- a/packages/shared/src/types/specs/lowcode-spec.ts +++ b/packages/shared/src/types/specs/lowcode-spec.ts @@ -2,9 +2,14 @@ * https://lowcode-engine.cn/site/docs/specs/lowcode-spec * 低代码引擎搭建协议规范 */ -import { JSONObject, JSONValue } from '../json'; import { Reference } from './material-spec'; +export type JSONValue = number | string | boolean | null; + +export interface JSONObject { + [key: string]: JSONValue | JSONObject | JSONObject[]; +} + /** * https://lowcode-engine.cn/site/docs/specs/lowcode-spec#2-%E5%8D%8F%E8%AE%AE%E7%BB%93%E6%9E%84 * 应用协议 diff --git a/packages/shared/src/utils/async.ts b/packages/shared/src/utils/async.ts index e5235feed..902297925 100644 --- a/packages/shared/src/utils/async.ts +++ b/packages/shared/src/utils/async.ts @@ -44,3 +44,83 @@ export class AutoOpenBarrier extends Barrier { super.open(); } } + +const canceledName = 'Canceled'; + +/** + * Checks if the given error is a promise in canceled state + */ +export function isCancellationError(error: any): boolean { + if (error instanceof CancellationError) { + return true; + } + return error instanceof Error && error.name === canceledName && error.message === canceledName; +} + +export class CancellationError extends Error { + constructor() { + super(canceledName); + this.name = this.message; + } +} + +export type ValueCallback = (value: T | Promise) => void; + +const enum DeferredOutcome { + Resolved, + Rejected, +} + +/** + * Creates a promise whose resolution or rejection can be controlled imperatively. + */ +export class DeferredPromise { + private completeCallback!: ValueCallback; + private errorCallback!: (err: unknown) => void; + private outcome?: { outcome: DeferredOutcome.Rejected; value: any } | { outcome: DeferredOutcome.Resolved; value: T }; + + public get isRejected() { + return this.outcome?.outcome === DeferredOutcome.Rejected; + } + + public get isResolved() { + return this.outcome?.outcome === DeferredOutcome.Resolved; + } + + public get isSettled() { + return !!this.outcome; + } + + public get value() { + return this.outcome?.outcome === DeferredOutcome.Resolved ? this.outcome?.value : undefined; + } + + public readonly p: Promise; + + constructor() { + this.p = new Promise((c, e) => { + this.completeCallback = c; + this.errorCallback = e; + }); + } + + public complete(value: T) { + return new Promise((resolve) => { + this.completeCallback(value); + this.outcome = { outcome: DeferredOutcome.Resolved, value }; + resolve(); + }); + } + + public error(err: unknown) { + return new Promise((resolve) => { + this.errorCallback(err); + this.outcome = { outcome: DeferredOutcome.Rejected, value: err }; + resolve(); + }); + } + + public cancel() { + return this.error(new CancellationError()); + } +} diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 659c24545..dd1df9e83 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -9,3 +9,4 @@ export * from './async'; export * from './node'; export * from './resource'; export * from './functional'; +export * from './timer'; diff --git a/packages/shared/src/utils/invariant.ts b/packages/shared/src/utils/invariant.ts index fc7a03680..deaee18c4 100644 --- a/packages/shared/src/utils/invariant.ts +++ b/packages/shared/src/utils/invariant.ts @@ -11,3 +11,11 @@ export function illegalArgument(name?: string): Error { return new Error('Illegal argument'); } } + +export function illegalState(name?: string): Error { + if (name) { + return new Error(`Illegal state: ${name}`); + } else { + return new Error('Illegal state'); + } +} diff --git a/packages/shared/src/utils/timer.ts b/packages/shared/src/utils/timer.ts new file mode 100644 index 000000000..ef744897a --- /dev/null +++ b/packages/shared/src/utils/timer.ts @@ -0,0 +1,86 @@ +import { IDisposable, toDisposable } from '../common'; + +export class TimeoutTimer implements IDisposable { + private _token: any; + private _isDisposed = false; + + constructor(); + constructor(runner: () => void, timeout: number); + constructor(runner?: () => void, timeout?: number) { + this._token = -1; + + if (typeof runner === 'function' && typeof timeout === 'number') { + this.setIfNotSet(runner, timeout); + } + } + + dispose(): void { + this.cancel(); + this._isDisposed = true; + } + + cancel(): void { + if (this._token !== -1) { + clearTimeout(this._token); + this._token = -1; + } + } + + cancelAndSet(runner: () => void, timeout: number): void { + if (this._isDisposed) { + throw Error(`Calling 'cancelAndSet' on a disposed TimeoutTimer`); + } + + this.cancel(); + this._token = setTimeout(() => { + this._token = -1; + runner(); + }, timeout); + } + + setIfNotSet(runner: () => void, timeout: number): void { + if (this._isDisposed) { + throw Error(`Calling 'setIfNotSet' on a disposed TimeoutTimer`); + } + + if (this._token !== -1) { + // timer is already set + return; + } + this._token = setTimeout(() => { + this._token = -1; + runner(); + }, timeout); + } +} + +export class IntervalTimer implements IDisposable { + private disposable: IDisposable | undefined = undefined; + private isDisposed = false; + + cancel(): void { + this.disposable?.dispose(); + this.disposable = undefined; + } + + cancelAndSet(runner: () => void, interval: number): void { + if (this.isDisposed) { + throw new Error(`Calling 'cancelAndSet' on a disposed IntervalTimer`); + } + + this.cancel(); + const handle = setInterval(() => { + runner(); + }, interval); + + this.disposable = toDisposable(() => { + clearInterval(handle); + this.disposable = undefined; + }); + } + + dispose(): void { + this.cancel(); + this.isDisposed = true; + } +}