diff --git a/packages/engine-core/src/command/commandRegistry.ts b/packages/engine-core/src/command/commandRegistry.ts index bc5f7296f..abf01e129 100644 --- a/packages/engine-core/src/command/commandRegistry.ts +++ b/packages/engine-core/src/command/commandRegistry.ts @@ -9,7 +9,7 @@ import { Iterable, } from '@alilc/lowcode-shared'; import { ICommand, ICommandHandler } from './command'; -import { Extensions, Registry } from '../common/registry'; +import { Extensions, Registry } from '../extension/registry'; import { ICommandService } from './commandService'; export type ICommandsMap = Map; @@ -26,7 +26,7 @@ export interface ICommandRegistry { getCommands(): ICommandsMap; } -class CommandsRegistry implements ICommandRegistry { +class CommandsRegistryImpl implements ICommandRegistry { private readonly _commands = new Map>(); private readonly _didRegisterCommandEmitter = new Emitter(); @@ -111,6 +111,6 @@ class CommandsRegistry implements ICommandRegistry { } } -const commandsRegistry = new CommandsRegistry(); +export const CommandsRegistry = new CommandsRegistryImpl(); -Registry.add(Extensions.Command, commandsRegistry); +Registry.add(Extensions.Command, CommandsRegistry); diff --git a/packages/engine-core/src/command/commandService.ts b/packages/engine-core/src/command/commandService.ts index f17ab8c43..3d7b0932b 100644 --- a/packages/engine-core/src/command/commandService.ts +++ b/packages/engine-core/src/command/commandService.ts @@ -1,6 +1,5 @@ -import { createDecorator, Provide, IInstantiationService } from '@alilc/lowcode-shared'; -import { Registry, Extensions } from '../common/registry'; -import { ICommandRegistry } from './commandRegistry'; +import { createDecorator, IInstantiationService } from '@alilc/lowcode-shared'; +import { CommandsRegistry } from './commandRegistry'; export interface ICommandService { executeCommand(commandId: string, ...args: any[]): Promise; @@ -8,7 +7,6 @@ export interface ICommandService { export const ICommandService = createDecorator('commandService'); -@Provide(ICommandService) export class CommandService implements ICommandService { constructor(@IInstantiationService private instantiationService: IInstantiationService) {} @@ -17,7 +15,7 @@ export class CommandService implements ICommandService { } private tryExecuteCommand(id: string, args: any[]): Promise { - const command = Registry.as(Extensions.Command).getCommand(id); + const command = CommandsRegistry.getCommand(id); if (!command) { return Promise.reject(new Error(`command '${id}' not found`)); } diff --git a/packages/engine-core/src/configuration/configurationRegistry.ts b/packages/engine-core/src/configuration/configurationRegistry.ts index 409b793f4..b077ff551 100644 --- a/packages/engine-core/src/configuration/configurationRegistry.ts +++ b/packages/engine-core/src/configuration/configurationRegistry.ts @@ -8,7 +8,7 @@ import { types, } from '@alilc/lowcode-shared'; import { isUndefined, isObject } from 'lodash-es'; -import { Extensions, Registry } from '../common/registry'; +import { Extensions, Registry } from '../extension/registry'; import { OVERRIDE_PROPERTY_REGEX, overrideIdentifiersFromKey } from './configuration'; export interface IConfigurationRegistry { @@ -133,9 +133,9 @@ export const allSettings: { patternProperties: StringDictionary; } = { properties: {}, patternProperties: {} }; -export class ConfigurationRegistry implements IConfigurationRegistry { +export class ConfigurationRegistryImpl implements IConfigurationRegistry { private registeredConfigurationDefaults: IConfigurationDefaults[] = []; - private configurationDefaultsOverrides: Map< + private readonly configurationDefaultsOverrides: Map< string, { configurationDefaultOverrides: IConfigurationDefaultOverride[]; @@ -143,8 +143,8 @@ export class ConfigurationRegistry implements IConfigurationRegistry { } >; - private configurationProperties: StringDictionary; - private excludedConfigurationProperties: StringDictionary; + private readonly configurationProperties: StringDictionary; + private readonly excludedConfigurationProperties: StringDictionary; private overrideIdentifiers = new Set(); private propertiesChangeEmitter = new Emitter<{ @@ -645,4 +645,6 @@ function isSameExtension(a?: IExtensionInfo, b?: IExtensionInfo): boolean { return a.id === b.id && a.version === b.version; } -Registry.add(Extensions.Configuration, new ConfigurationRegistry()); +export const ConfigurationRegistry = new ConfigurationRegistryImpl(); + +Registry.add(Extensions.Configuration, ConfigurationRegistry); diff --git a/packages/engine-core/src/configuration/configurationService.ts b/packages/engine-core/src/configuration/configurationService.ts index 9f79cc121..c257c1736 100644 --- a/packages/engine-core/src/configuration/configurationService.ts +++ b/packages/engine-core/src/configuration/configurationService.ts @@ -1,10 +1,4 @@ -import { - createDecorator, - Emitter, - Provide, - type Event, - type EventListener, -} from '@alilc/lowcode-shared'; +import { createDecorator, Emitter, type Event, type EventListener } from '@alilc/lowcode-shared'; import { Configuration, DefaultConfiguration, @@ -69,7 +63,6 @@ export interface IConfigurationService { export const IConfigurationService = createDecorator('configurationService'); -@Provide(IConfigurationService) export class ConfigurationService implements IConfigurationService { private configuration: Configuration; private readonly defaultConfiguration: DefaultConfiguration; diff --git a/packages/engine-core/src/configuration/configurations.ts b/packages/engine-core/src/configuration/configurations.ts index a5acc526c..42f40dafe 100644 --- a/packages/engine-core/src/configuration/configurations.ts +++ b/packages/engine-core/src/configuration/configurations.ts @@ -6,11 +6,11 @@ import { type IOverrides, } from './configurationModel'; import { + ConfigurationRegistry, type IConfigurationPropertySchema, type IConfigurationRegistry, type IRegisteredConfigurationPropertySchema, } from './configurationRegistry'; -import { Registry, Extensions } from '../common/registry'; import { isEqual, isNil, isPlainObject, get as lodasgGet } from 'lodash-es'; import { IInspectValue, @@ -37,8 +37,8 @@ export class DefaultConfiguration { initialize(): ConfigurationModel { this.resetConfigurationModel(); - Registry.as(Extensions.Configuration).onDidUpdateConfiguration( - ({ properties }) => this.onDidUpdateConfiguration([...properties]), + ConfigurationRegistry.onDidUpdateConfiguration(({ properties }) => + this.onDidUpdateConfiguration([...properties]), ); return this.configurationModel; @@ -56,19 +56,14 @@ export class DefaultConfiguration { } private onDidUpdateConfiguration(properties: string[]): void { - this.updateConfigurationModel( - properties, - Registry.as(Extensions.Configuration).getConfigurationProperties(), - ); + this.updateConfigurationModel(properties, ConfigurationRegistry.getConfigurationProperties()); this.emitter.emit({ defaults: this.configurationModel, properties }); } private resetConfigurationModel(): void { this._configurationModel = ConfigurationModel.createEmptyModel(); - const properties = Registry.as( - Extensions.Configuration, - ).getConfigurationProperties(); + const properties = ConfigurationRegistry.getConfigurationProperties(); this.updateConfigurationModel(Object.keys(properties), properties); } @@ -156,9 +151,7 @@ class ConfigurationModelParser { raw: any, options?: ConfigurationParseOptions, ): IConfigurationModel & { hasExcludedProperties?: boolean } { - const configurationProperties = Registry.as( - Extensions.Configuration, - ).getConfigurationProperties(); + const configurationProperties = ConfigurationRegistry.getConfigurationProperties(); const filtered = this.filter(raw, configurationProperties, true, options); raw = filtered.raw; diff --git a/packages/engine-core/src/extension/extensionHost.ts b/packages/engine-core/src/extension/extensionHost.ts index b71073238..514306655 100644 --- a/packages/engine-core/src/extension/extensionHost.ts +++ b/packages/engine-core/src/extension/extensionHost.ts @@ -1,5 +1,4 @@ -import { type IConfigurationRegistry, type IConfigurationNode } from '../configuration'; -import { Registry, Extensions } from '../common/registry'; +import { ConfigurationRegistry, type IConfigurationNode } from '../configuration'; import { type ExtensionInitializer, type IExtensionInstance } from './extension'; import { invariant } from '@alilc/lowcode-shared'; @@ -19,9 +18,8 @@ export class ExtensionHost { initializer: ExtensionInitializer, preferenceConfigurations: IConfigurationNode[], ) { - const configurationRegistry = Registry.as(Extensions.Configuration); this.configurationProperties = - configurationRegistry.registerConfigurations(preferenceConfigurations); + ConfigurationRegistry.registerConfigurations(preferenceConfigurations); this.instance = initializer({}); } diff --git a/packages/engine-core/src/extension/extensionManagement.ts b/packages/engine-core/src/extension/extensionManagement.ts index 9a79d56aa..790aab68c 100644 --- a/packages/engine-core/src/extension/extensionManagement.ts +++ b/packages/engine-core/src/extension/extensionManagement.ts @@ -37,7 +37,7 @@ export class ExtensionManagement { ): Promise { if (!this.validateExtension(extension, override)) return; - const metadata = extension.meta ?? {}; + const metadata = extension.metadata ?? {}; const host = new ExtensionHost( extension.name, extension, diff --git a/packages/engine-core/src/extension/extensionService.ts b/packages/engine-core/src/extension/extensionService.ts index 621fe65a5..959f889d3 100644 --- a/packages/engine-core/src/extension/extensionService.ts +++ b/packages/engine-core/src/extension/extensionService.ts @@ -1,4 +1,4 @@ -import { createDecorator, Provide } from '@alilc/lowcode-shared'; +import { createDecorator } from '@alilc/lowcode-shared'; import { ExtensionManagement, type IExtensionRegisterOptions } from './extensionManagement'; import { type IFunctionExtension } from './extension'; import { ExtensionHost } from './extensionHost'; @@ -15,7 +15,6 @@ export interface IExtensionService { export const IExtensionService = createDecorator('extensionService'); -@Provide(IExtensionService) export class ExtensionService implements IExtensionService { private extensionManagement = new ExtensionManagement(); diff --git a/packages/engine-core/src/common/registry.ts b/packages/engine-core/src/extension/registry.ts similarity index 95% rename from packages/engine-core/src/common/registry.ts rename to packages/engine-core/src/extension/registry.ts index df89ed433..a6d7be19b 100644 --- a/packages/engine-core/src/common/registry.ts +++ b/packages/engine-core/src/extension/registry.ts @@ -41,5 +41,6 @@ export const Registry: IRegistry = new RegistryImpl(); export const Extensions = { Configuration: 'base.contributions.configuration', Command: 'base.contributions.command', + Keybinding: 'base.contributions.keybinding', Widget: 'base.contributions.widget', }; diff --git a/packages/engine-core/src/index.ts b/packages/engine-core/src/index.ts index 4704080c8..a3c0f28b6 100644 --- a/packages/engine-core/src/index.ts +++ b/packages/engine-core/src/index.ts @@ -4,5 +4,9 @@ export * from './resource'; export * from './command'; // test -export * from './common/registry'; +export * from './extension/registry'; export * from './main'; +export * from './keybinding/keybindingRegistry'; +export * from './keybinding/keybindingParser'; +export * from './keybinding/keybindingResolver'; +export * from './keybinding/keybindings'; diff --git a/packages/engine-core/src/keybinding/keybinding.ts b/packages/engine-core/src/keybinding/keybinding.ts index 84796f871..e69de29bb 100644 --- a/packages/engine-core/src/keybinding/keybinding.ts +++ b/packages/engine-core/src/keybinding/keybinding.ts @@ -1,39 +0,0 @@ -/** - * A keybinding is a sequence of chords. - */ -export class Keybinding { - public readonly chords: Chord[]; - - constructor(chords: Chord[]) { - if (chords.length === 0) { - throw illegalArgument(`chords`); - } - this.chords = chords; - } - - public getHashCode(): string { - let result = ''; - for (let i = 0, len = this.chords.length; i < len; i++) { - if (i !== 0) { - result += ';'; - } - result += this.chords[i].getHashCode(); - } - return result; - } - - public equals(other: Keybinding | null): boolean { - if (other === null) { - return false; - } - if (this.chords.length !== other.chords.length) { - return false; - } - for (let i = 0; i < this.chords.length; i++) { - if (!this.chords[i].equals(other.chords[i])) { - return false; - } - } - return true; - } -} diff --git a/packages/engine-core/src/keybinding/keybindingParser.ts b/packages/engine-core/src/keybinding/keybindingParser.ts new file mode 100644 index 000000000..6232b0f32 --- /dev/null +++ b/packages/engine-core/src/keybinding/keybindingParser.ts @@ -0,0 +1,99 @@ +import { KeyCodeUtils, ScanCodeUtils } from '@alilc/lowcode-shared'; +import { KeyCodeChord, ScanCodeChord, Keybinding, Chord } from './keybindings'; + +export class KeybindingParser { + private static _readModifiers(input: string) { + input = input.toLowerCase().trim(); + + let ctrl = false; + let shift = false; + let alt = false; + let meta = false; + + let matchedModifier: boolean; + + do { + matchedModifier = false; + if (/^ctrl(\+|-)/.test(input)) { + ctrl = true; + input = input.slice('ctrl-'.length); + matchedModifier = true; + } + if (/^shift(\+|-)/.test(input)) { + shift = true; + input = input.slice('shift-'.length); + matchedModifier = true; + } + if (/^alt(\+|-)/.test(input)) { + alt = true; + input = input.slice('alt-'.length); + matchedModifier = true; + } + if (/^meta(\+|-)/.test(input)) { + meta = true; + input = input.slice('meta-'.length); + matchedModifier = true; + } + if (/^win(\+|-)/.test(input)) { + meta = true; + input = input.slice('win-'.length); + matchedModifier = true; + } + if (/^cmd(\+|-)/.test(input)) { + meta = true; + input = input.slice('cmd-'.length); + matchedModifier = true; + } + } while (matchedModifier); + + let key: string; + + const firstSpaceIdx = input.indexOf(' '); + if (firstSpaceIdx > 0) { + key = input.substring(0, firstSpaceIdx); + input = input.substring(firstSpaceIdx); + } else { + key = input; + input = ''; + } + + return { + remains: input, + ctrl, + shift, + alt, + meta, + key, + }; + } + + private static parseChord(input: string): [Chord, 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]; + } + + static parseKeybinding(input: string): Keybinding | null { + if (!input) { + return null; + } + + const chords: Chord[] = []; + let chord: Chord; + + while (input.length > 0) { + [chord, input] = this.parseChord(input); + chords.push(chord); + } + return chords.length > 0 ? new Keybinding(chords) : null; + } +} diff --git a/packages/engine-core/src/keybinding/keybindingRegistry.ts b/packages/engine-core/src/keybinding/keybindingRegistry.ts index 3d3a54ae5..471e1188a 100644 --- a/packages/engine-core/src/keybinding/keybindingRegistry.ts +++ b/packages/engine-core/src/keybinding/keybindingRegistry.ts @@ -1,3 +1,8 @@ +import { OperatingSystem, OS } from '@alilc/lowcode-shared'; +import { ICommandHandler, ICommandMetadata, CommandsRegistry } from '../command'; +import { Keybinding } from './keybindings'; +import { Extensions, Registry } from '../extension/registry'; + export interface IKeybindingItem { keybinding: Keybinding | null; command: string | null; @@ -24,3 +29,81 @@ export interface IKeybindings { secondary?: number[]; }; } + +export interface IKeybindingRule extends IKeybindings { + id: string; + weight: number; + args?: any; +} + +export interface ICommandAndKeybindingRule extends IKeybindingRule { + handler: ICommandHandler; + metadata?: ICommandMetadata | null; +} + +export interface IExtensionKeybindingRule { + keybinding: Keybinding | null; + id: string; + args?: any; + weight: number; + extensionId?: string; + isBuiltinExtension?: boolean; +} + +export const enum KeybindingWeight { + EditorCore = 0, + EditorContrib = 100, + WorkbenchContrib = 200, + BuiltinExtension = 300, + ExternalExtension = 400, +} + +export interface IKeybindingsRegistry { + registerKeybindingRule(rule: IKeybindingRule): void; + setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void; + registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void; + getDefaultKeybindings(): IKeybindingItem[]; +} + +export class KeybindingsRegistryImpl implements IKeybindingsRegistry { + /** + * Take current platform into account and reduce to primary & secondary. + */ + private static bindToCurrentPlatform(kb: IKeybindings): { + primary?: number; + secondary?: number[]; + } { + if (OS === OperatingSystem.Windows) { + if (kb && kb.win) { + return kb.win; + } + } else if (OS === OperatingSystem.Macintosh) { + if (kb && kb.mac) { + return kb.mac; + } + } else { + if (kb && kb.linux) { + return kb.linux; + } + } + + return kb; + } + + registerKeybindingRule(rule: IKeybindingRule): void { + const actualKb = KeybindingsRegistryImpl.bindToCurrentPlatform(rule); + } + + registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void { + this.registerKeybindingRule(desc); + CommandsRegistry.registerCommand(desc); + } + + setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void {} + + getDefaultKeybindings(): IKeybindingItem[] {} +} + +export const KeybindingsRegistry = new KeybindingsRegistryImpl(); + +Registry.add(Extensions.Keybinding, KeybindingsRegistry); diff --git a/packages/engine-core/src/keybinding/keybindingResolver.ts b/packages/engine-core/src/keybinding/keybindingResolver.ts new file mode 100644 index 000000000..9e276aa83 --- /dev/null +++ b/packages/engine-core/src/keybinding/keybindingResolver.ts @@ -0,0 +1,79 @@ +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 + +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()) + : []; + if (resolvedKeybinding && this.chords.length === 0) { + // handle possible single modifier chord keybindings + 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; + } +} + +export function toEmptyArrayIfContainsNull(arr: (T | null)[]): T[] { + const result: T[] = []; + for (let i = 0, len = arr.length; i < len; i++) { + const element = arr[i]; + if (!element) { + return []; + } + result.push(element); + } + return result; +} diff --git a/packages/engine-core/src/keybinding/keybindingService.ts b/packages/engine-core/src/keybinding/keybindingService.ts new file mode 100644 index 000000000..a6018873f --- /dev/null +++ b/packages/engine-core/src/keybinding/keybindingService.ts @@ -0,0 +1,100 @@ +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; + + readonly ctrlKey: boolean; + readonly shiftKey: boolean; + readonly altKey: boolean; + readonly metaKey: boolean; + readonly altGraphKey: boolean; + readonly keyCode: KeyCode; + readonly code: string; +} + +export interface KeybindingsSchemaContribution { + readonly onDidChange?: Event; + + getSchemaAdditions(): IJSONSchema[]; +} + +export interface IKeybindingService { + readonly _serviceBrand: undefined; + + readonly inChordMode: boolean; + + onDidUpdateKeybindings: 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; + + getDefaultKeybindingsContent(): string; + + getDefaultKeybindings(): readonly ResolvedKeybindingItem[]; + + getKeybindings(): readonly ResolvedKeybindingItem[]; + + customKeybindingsCount(): number; + + /** + * 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; + + registerSchemaContribution(contribution: KeybindingsSchemaContribution): void; + + toggleLogging(): boolean; + + _dumpDebugInfo(): string; + _dumpDebugInfoJSON(): string; +} + +export const IKeybindingService = createDecorator('keybindingService'); diff --git a/packages/engine-core/src/keybinding/keybindings.ts b/packages/engine-core/src/keybinding/keybindings.ts new file mode 100644 index 000000000..36fb98daf --- /dev/null +++ b/packages/engine-core/src/keybinding/keybindings.ts @@ -0,0 +1,280 @@ +import { illegalArgument, KeyCode, OperatingSystem, ScanCode } from '@alilc/lowcode-shared'; + +/** + * Binary encoding strategy: + * ``` + * 1111 11 + * 5432 1098 7654 3210 + * ---- CSAW KKKK KKKK + * C = bit 11 = ctrlCmd flag + * S = bit 10 = shift flag + * A = bit 9 = alt flag + * W = bit 8 = winCtrl flag + * K = bits 0-7 = key code + * ``` + */ +const enum BinaryKeybindingsMask { + CtrlCmd = (1 << 11) >>> 0, + Shift = (1 << 10) >>> 0, + Alt = (1 << 9) >>> 0, + WinCtrl = (1 << 8) >>> 0, + KeyCode = 0x000000ff, +} + +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)]); + } else { + const chords = []; + for (let i = 0; i < keybinding.length; i++) { + chords.push(createSimpleKeybinding(keybinding[i], OS)); + } + return new Keybinding(chords); + } +} + +export function createSimpleKeybinding(keybinding: number, OS: OperatingSystem): KeyCodeChord { + const ctrlCmd = keybinding & BinaryKeybindingsMask.CtrlCmd ? true : false; + const winCtrl = keybinding & BinaryKeybindingsMask.WinCtrl ? true : false; + + const ctrlKey = OS === OperatingSystem.Macintosh ? winCtrl : ctrlCmd; + const shiftKey = keybinding & BinaryKeybindingsMask.Shift ? true : false; + const altKey = keybinding & BinaryKeybindingsMask.Alt ? true : false; + const metaKey = OS === OperatingSystem.Macintosh ? ctrlCmd : winCtrl; + const keyCode = keybinding & BinaryKeybindingsMask.KeyCode; + + return new KeyCodeChord(ctrlKey, shiftKey, altKey, metaKey, keyCode); +} + +export interface Modifiers { + readonly ctrlKey: boolean; + readonly shiftKey: boolean; + readonly altKey: boolean; + readonly metaKey: boolean; +} + +/** + * Represents a chord which uses the `keyCode` field of keyboard events. + * A chord is a combination of keys pressed simultaneously. + */ +export class KeyCodeChord implements Modifiers { + constructor( + public readonly ctrlKey: boolean, + public readonly shiftKey: boolean, + public readonly altKey: boolean, + public readonly metaKey: boolean, + public readonly keyCode: KeyCode, + ) {} + + equals(other: Chord): boolean { + return ( + other instanceof KeyCodeChord && + this.ctrlKey === other.ctrlKey && + this.shiftKey === other.shiftKey && + this.altKey === other.altKey && + this.metaKey === other.metaKey && + this.keyCode === other.keyCode + ); + } + + 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 `K${ctrl}${shift}${alt}${meta}${this.keyCode}`; + } + + isModifierKey(): boolean { + return ( + this.keyCode === KeyCode.Unknown || + this.keyCode === KeyCode.Ctrl || + this.keyCode === KeyCode.Meta || + this.keyCode === KeyCode.Alt || + this.keyCode === KeyCode.Shift + ); + } + + toKeybinding(): Keybinding { + return new Keybinding([this]); + } + + /** + * Does this keybinding refer to the key code of a modifier and it also has the modifier flag? + */ + isDuplicateModifierCase(): boolean { + return ( + (this.ctrlKey && this.keyCode === KeyCode.Ctrl) || + (this.shiftKey && this.keyCode === KeyCode.Shift) || + (this.altKey && this.keyCode === KeyCode.Alt) || + (this.metaKey && this.keyCode === KeyCode.Meta) + ); + } +} + +/** + * 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[]; + + constructor(chords: Chord[]) { + if (chords.length === 0) { + throw illegalArgument(`chords`); + } + this.chords = chords; + } + + getHashCode(): string { + let result = ''; + for (let i = 0, len = this.chords.length; i < len; i++) { + if (i !== 0) { + result += ';'; + } + result += this.chords[i].getHashCode(); + } + return result; + } + + equals(other: Keybinding | null): boolean { + if (other === null) { + return false; + } + if (this.chords.length !== other.chords.length) { + return false; + } + for (let i = 0; i < this.chords.length; i++) { + if (!this.chords[i].equals(other.chords[i])) { + return false; + } + } + return true; + } +} + +export class ResolvedChord { + constructor( + public readonly ctrlKey: boolean, + public readonly shiftKey: boolean, + public readonly altKey: boolean, + public readonly metaKey: boolean, + public readonly keyLabel: string | null, + public readonly keyAriaLabel: string | null, + ) {} +} + +export type SingleModifierChord = 'ctrl' | 'shift' | 'alt' | 'meta'; + +/** + * A resolved keybinding. Consists of one or multiple chords. + */ +export abstract class ResolvedKeybinding { + /** + * This prints the binding in a format suitable for displaying in the UI. + */ + public abstract getLabel(): string | null; + /** + * 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. + */ + public abstract getUserSettingsLabel(): string | null; + /** + * Is the user settings label reflecting the label? + */ + public abstract isWYSIWYG(): boolean; + /** + * Does the keybinding consist of more than one chord? + */ + public abstract hasMultipleChords(): boolean; + /** + * Returns the chords that comprise of the keybinding. + */ + public abstract getChords(): ResolvedChord[]; + /** + * Returns the chords as strings useful for dispatching. + * Returns null for modifier only chords. + * @example keybinding "Shift" -> null + * @example keybinding ("D" with shift == true) -> "shift+D" + */ + public abstract getDispatchChords(): (string | null)[]; + /** + * Returns the modifier only chords as strings useful for dispatching. + * Returns null for chords that contain more than one modifier or a regular key. + * @example keybinding "Shift" -> "shift" + * @example keybinding ("D" with shift == true") -> null + */ + public abstract getSingleModifierDispatchChords(): (SingleModifierChord | null)[]; +} diff --git a/packages/engine-core/src/resource/resourceService.ts b/packages/engine-core/src/resource/resourceService.ts index a1099f803..93e25145a 100644 --- a/packages/engine-core/src/resource/resourceService.ts +++ b/packages/engine-core/src/resource/resourceService.ts @@ -1,6 +1,5 @@ import { createDecorator, - Provide, type Package, type Reference, mapPackageToUniqueId, @@ -22,7 +21,6 @@ export interface IResourceService { export const IResourceService = createDecorator('resourceService'); -@Provide(IResourceService) export class ResourceService implements IResourceService { private resourceModel = new ResourceModel(); diff --git a/packages/engine-core/src/workbench/layout/layout.ts b/packages/engine-core/src/workbench/layout/layout.ts index 727720d13..6b1991d79 100644 --- a/packages/engine-core/src/workbench/layout/layout.ts +++ b/packages/engine-core/src/workbench/layout/layout.ts @@ -1,5 +1,4 @@ -import { Extensions, Registry } from '../../extension/extension'; -import { IWidgetRegistry } from '../widget/widgetRegistry'; +import { WidgetRegistry } from '../widget/widgetRegistry'; export const enum LayoutParts { TopBar = 1, @@ -21,7 +20,7 @@ export interface ILayout { export class Layout implements ILayout { constructor(public mainContainer: HTMLElement) { - Registry.as>(Extensions.Widget).onDidRegister(() => {}); + WidgetRegistry.onDidRegister(() => {}); } registerPart(part: LayoutParts): void {} diff --git a/packages/engine-core/src/workbench/widget/widgetRegistry.ts b/packages/engine-core/src/workbench/widget/widgetRegistry.ts index 9213f9852..fb3c4490e 100644 --- a/packages/engine-core/src/workbench/widget/widgetRegistry.ts +++ b/packages/engine-core/src/workbench/widget/widgetRegistry.ts @@ -1,6 +1,6 @@ import { type Event, type EventListener, Emitter } from '@alilc/lowcode-shared'; import { IWidget } from './widget'; -import { Extensions, Registry } from '../../extension/extension'; +import { Extensions, Registry } from '../../extension/registry'; export interface IWidgetRegistry { onDidRegister: Event[]>; @@ -12,7 +12,7 @@ export interface IWidgetRegistry { getWidgets(): IWidget[]; } -export class WidgetRegistry implements IWidgetRegistry { +export class WidgetRegistryImpl implements IWidgetRegistry { private _widgets: Map> = new Map(); private emitter = new Emitter[]>(); @@ -34,4 +34,6 @@ export class WidgetRegistry implements IWidgetRegistry { } } -Registry.add(Extensions.Widget, new WidgetRegistry()); +export const WidgetRegistry = new WidgetRegistryImpl(); + +Registry.add(Extensions.Widget, WidgetRegistry); diff --git a/packages/engine-core/src/workbench/workbenchService.ts b/packages/engine-core/src/workbench/workbenchService.ts index 1b8ebd217..abffd60d6 100644 --- a/packages/engine-core/src/workbench/workbenchService.ts +++ b/packages/engine-core/src/workbench/workbenchService.ts @@ -1,4 +1,4 @@ -import { createDecorator, Provide } from '@alilc/lowcode-shared'; +import { createDecorator } from '@alilc/lowcode-shared'; export interface IWorkbenchService { initialize(): void; @@ -6,7 +6,6 @@ export interface IWorkbenchService { export const IWorkbenchService = createDecorator('workbenchService'); -@Provide(IWorkbenchService) export class WorkbenchService implements IWorkbenchService { initialize(): void { console.log('workbench service'); diff --git a/packages/engine-core/src/workspace/workspaceService.ts b/packages/engine-core/src/workspace/workspaceService.ts index 8a1a14d59..13ea2cca9 100644 --- a/packages/engine-core/src/workspace/workspaceService.ts +++ b/packages/engine-core/src/workspace/workspaceService.ts @@ -1,8 +1,7 @@ -import { createDecorator, Provide } from '@alilc/lowcode-shared'; +import { createDecorator } from '@alilc/lowcode-shared'; export interface IWorkspaceService {} export const IWorkspaceService = createDecorator('workspaceService'); -@Provide(IWorkspaceService) export class WorkspaceService implements IWorkspaceService {} diff --git a/packages/engine/src/themeService.ts b/packages/engine/src/themeService.ts index 5f2d47d6e..ac7919d57 100644 --- a/packages/engine/src/themeService.ts +++ b/packages/engine/src/themeService.ts @@ -1,4 +1,4 @@ -import { type Event, type EventListener, createDecorator, Provide } from '@alilc/lowcode-shared'; +import { type Event, type EventListener, createDecorator } from '@alilc/lowcode-shared'; export interface ITheme { type: string; @@ -14,7 +14,6 @@ export interface IThemeService { export const IThemeService = createDecorator('themeService'); -@Provide(IThemeService) export class ThemeService implements IThemeService { private activeTheme: ITheme; diff --git a/packages/global.d.ts b/packages/global.d.ts index e69de29bb..5b3f7295c 100644 --- a/packages/global.d.ts +++ b/packages/global.d.ts @@ -0,0 +1,4 @@ +// Global compile-time constants +declare var __DEV__: boolean; +declare var __VERSION__: string; +declare var __COMPAT__: boolean; diff --git a/packages/react-renderer/src/api/context.ts b/packages/react-renderer/src/api/context.ts index b2d6d329c..2f924f9ca 100644 --- a/packages/react-renderer/src/api/context.ts +++ b/packages/react-renderer/src/api/context.ts @@ -1,9 +1,10 @@ import { - IBoostsService, + IBoostsManager, IComponentTreeModelService, ILifeCycleService, IPackageManagementService, ISchemaService, + IExtensionHostService, } from '@alilc/lowcode-renderer-core'; import { InstanceAccessor } from '@alilc/lowcode-shared'; import { createContext, useContext } from 'react'; @@ -16,7 +17,7 @@ export interface IRendererContext { readonly packageManager: IPackageManagementService; - readonly boostsManager: IBoostsService; + readonly boostsManager: IBoostsManager; readonly componentTreeModel: IComponentTreeModelService; @@ -27,7 +28,7 @@ export const getRenderInstancesByAccessor = (accessor: InstanceAccessor) => { return { schema: accessor.get(ISchemaService), packageManager: accessor.get(IPackageManagementService), - boostsManager: accessor.get(IBoostsService), + boostsManager: accessor.get(IExtensionHostService).boostsManager, componentTreeModel: accessor.get(IComponentTreeModelService), lifeCycle: accessor.get(ILifeCycleService), }; diff --git a/packages/react-renderer/src/runtime/hooks/useReactiveStore.tsx b/packages/react-renderer/src/runtime/hooks/useReactiveStore.tsx index 0bb8e9b23..b1bd18bea 100644 --- a/packages/react-renderer/src/runtime/hooks/useReactiveStore.tsx +++ b/packages/react-renderer/src/runtime/hooks/useReactiveStore.tsx @@ -3,8 +3,7 @@ import { type AnyFunction, type StringDictionary, specTypes, - computed, - watch, + Signals, invariant, } from '@alilc/lowcode-shared'; import { useRef } from 'react'; @@ -60,10 +59,10 @@ function createReactiveStore( }; if (getter) { - const computedValue = computed(() => getter(target)); + const computedValue = Signals.computed(() => getter(target)); cleanups.push( - watch( + Signals.watch( computedValue, (newValue) => { Promise.resolve().then(() => { @@ -76,8 +75,8 @@ function createReactiveStore( ); } else if (valueGetter) { const initValue = mapValue(target, filter, (node: any, paths) => { - const computedValue = computed(() => valueGetter(node)); - const unwatch = watch(computedValue, (newValue) => { + const computedValue = Signals.computed(() => valueGetter(node)); + const unwatch = Signals.watch(computedValue, (newValue) => { waitPathToSetValueMap.set(paths, newValue); if (!isFlushPending && !isFlushing) { diff --git a/packages/react-renderer/src/runtime/reactiveState.ts b/packages/react-renderer/src/runtime/reactiveState.ts index 0ec8e5318..f89246771 100644 --- a/packages/react-renderer/src/runtime/reactiveState.ts +++ b/packages/react-renderer/src/runtime/reactiveState.ts @@ -1,8 +1,8 @@ -import { signal, type StringDictionary, type InstanceStateApi } from '@alilc/lowcode-shared'; +import { Signals, type StringDictionary, type InstanceStateApi } from '@alilc/lowcode-shared'; import { isPlainObject } from 'lodash-es'; export function reactiveStateFactory(initState: StringDictionary): InstanceStateApi { - const proxyState = signal(initState); + const proxyState = Signals.signal(initState); return { get state() { diff --git a/packages/react-renderer/tsconfig.json b/packages/react-renderer/tsconfig.json index 946b55298..b4e69ae1f 100644 --- a/packages/react-renderer/tsconfig.json +++ b/packages/react-renderer/tsconfig.json @@ -2,6 +2,5 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist" - }, - "include": ["src", "../../playground/renderer/src/plugin/remote/element.ts"] + } } diff --git a/packages/renderer-core/__tests__/api/component.spec.ts b/packages/renderer-core/__tests__/api/component.spec.ts deleted file mode 100644 index ad100a576..000000000 --- a/packages/renderer-core/__tests__/api/component.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -describe('createComponentFunction', () => { - it('', () => {}); -}); diff --git a/packages/renderer-core/__tests__/services/code-runtime/codeScope.spec.ts b/packages/renderer-core/__tests__/services/code-runtime/codeScope.spec.ts deleted file mode 100644 index 550b47eaa..000000000 --- a/packages/renderer-core/__tests__/services/code-runtime/codeScope.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @vitest-environment jsdom - */ -import { describe, it, expect } from 'vitest'; -import { CodeScope } from '../../../src/services/code-runtime'; - -describe('CodeScope', () => { - it('should return initial values', () => { - const initValue = { a: 1, b: 2 }; - const scope = new CodeScope(initValue); - expect(scope.value.a).toBe(1); - expect(scope.value.b).toBe(2); - }); - - it('inject should add new values', () => { - const scope = new CodeScope({}); - scope.set('c', 3); - expect(scope.value.c).toBe(3); - }); - - it('inject should not overwrite existing values without force', () => { - const initValue = { a: 1 }; - const scope = new CodeScope(initValue); - expect(scope.value.a).not.toBe(2); - scope.set('a', 3); - expect(scope.value.a).toBe(3); - }); - - it('setValue should merge values by default', () => { - const initValue = { a: 1 }; - const scope = new CodeScope(initValue); - scope.setValue({ b: 2 }); - expect(scope.value.a).toBe(1); - expect(scope.value.b).toBe(2); - }); - - it('setValue should replace values when replace is true', () => { - const initValue = { a: 1 }; - const scope = new CodeScope(initValue); - scope.setValue({ b: 2 }, true); - expect(scope.value.a).toBeUndefined(); - expect(scope.value.b).toBe(2); - }); - - it('should create child scopes and respect scope hierarchy', () => { - const parentValue = { a: 1, b: 2 }; - const childValue = { b: 3, c: 4 }; - - const parentScope = new CodeScope(parentValue); - const childScope = parentScope.createChild(childValue); - - expect(childScope.value.a).toBe(1); // Inherits from parent scope - expect(childScope.value.b).toBe(3); // Overridden by child scope - expect(childScope.value.c).toBe(4); // Unique to child scope - expect(parentScope.value.c).toBeUndefined(); // Parent scope should not have child's properties - }); -}); diff --git a/packages/renderer-core/__tests__/services/lifeCycle.spec.ts b/packages/renderer-core/__tests__/services/lifeCycle.spec.ts deleted file mode 100644 index 8d068c6f6..000000000 --- a/packages/renderer-core/__tests__/services/lifeCycle.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { LifeCycleService, LifecyclePhase } from '../../src/services/lifeCycleService'; - -const sleep = () => new Promise((r) => setTimeout(r, 500)); - -describe('LifeCycleService', () => { - it('it works', async () => { - let result = ''; - const lifeCycle = new LifeCycleService(); - - lifeCycle.when(LifecyclePhase.Ready).then(() => { - result += '1'; - }); - lifeCycle.when(LifecyclePhase.Ready).finally(() => { - result += '2'; - }); - lifeCycle.when(LifecyclePhase.Inited).then(() => { - result += '3'; - }); - lifeCycle.when(LifecyclePhase.Inited).finally(() => { - result += '4'; - }); - - lifeCycle.phase = LifecyclePhase.Ready; - - await sleep(); - - expect(result).toEqual('12'); - - lifeCycle.phase = LifecyclePhase.Inited; - - await sleep(); - - expect(result).toEqual('1234'); - }); -}); diff --git a/packages/renderer-core/__tests__/utils/node.spec.ts b/packages/renderer-core/__tests__/utils/node.spec.ts deleted file mode 100644 index 1120ef177..000000000 --- a/packages/renderer-core/__tests__/utils/node.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { walk } from '../../src/utils/node'; - -describe('sync walker', () => { - it('down', () => { - const ast = { - hasMask: true, - visible: false, - footer: false, - cancelProps: { - text: false, - type: 'normal', - }, - confirmState: '确定', - confirmStyle: 'primary', - footerActions: 'cancel,ok', - className: 'dialog_lkz6xvcv', - confirmText: { - en_US: 'Confirm', - use: '', - zh_CN: '确定', - type: 'JSExpression', - value: `({"en_US":"OK","key":"i18n-xgse6q6a","type":"i18n","zh_CN":"确定"})[this.utils.getLocale()]`, - key: 'i18n-xgse6q6a', - extType: 'i18n', - }, - autoFocus: true, - title: { - mock: { - en_US: 'Dialog Title', - use: '', - zh_CN: 'Dialog标题', - type: 'JSExpression', - value: `({"en_US":"Dialog Title","key":"i18n-0m3kaceq","type":"i18n","zh_CN":"Dialog标题"})[this.utils.getLocale()]`, - key: 'i18n-0m3kaceq', - extType: 'i18n', - }, - type: 'JSExpression', - value: 'state.dialogInfo && state.dialogInfo.title', - }, - closeable: [ - 'esc', - 'mask', - { - type: 'JSExpression', - value: '1', - }, - ], - cancelText: { - en_US: 'Cancel', - use: '', - zh_CN: '取消', - type: 'JSExpression', - value: `({"en_US":"Cancel","key":"i18n-wtq23279","type":"i18n","zh_CN":"取消"})[this.utils.getLocale()]`, - key: 'i18n-wtq23279', - extType: 'i18n', - }, - width: '800px', - footerAlign: 'right', - popupOutDialog: true, - __style__: ':root {}', - fieldId: 'dialog_case', - height: '500px', - }; - - const newAst = walk(ast, { - enter(node, parent, key, index) { - if (node.type === 'JSExpression') { - this.replace({ - type: '1', - value: '2', - }); - } - }, - }); - - console.log(newAst, Object.is(newAst, ast)); - }); -}); diff --git a/packages/renderer-core/__tests__/utils/value.spec.ts b/packages/renderer-core/__tests__/utils/value.spec.ts deleted file mode 100644 index 6766ce0ef..000000000 --- a/packages/renderer-core/__tests__/utils/value.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { someValue, mapValue } from '../../src/utils/value'; - -describe('someValue', () => { - it('should return false for non-plain objects or empty objects', () => { - expect(someValue([], (val) => val > 10)).toBe(false); - expect(someValue({}, (val) => val > 10)).toBe(false); - expect(someValue(null, (val) => val > 10)).toBe(false); - expect(someValue(undefined, (val) => val > 10)).toBe(false); - }); - - it('should return true if the predicate matches object value', () => { - const obj = { a: 5, b: { c: 15 } }; - expect(someValue(obj, (val) => val.c > 10)).toBe(true); - }); - - it('should return true if the predicate matches nested array element', () => { - const obj = { a: [1, 2, { d: 14 }] }; - expect(someValue(obj, (val) => val.d > 10)).toBe(true); - }); - - it('should return false if the predicate does not match any value', () => { - const obj = { a: 5, b: { c: 9 } }; - expect(someValue(obj, (val) => val.c > 10)).toBe(false); - }); - - it('should handle primitives in object values', () => { - const obj = { a: 1, b: 'string', c: true }; - const strPredicate = (val: any) => typeof val.b === 'string'; - expect(someValue(obj, strPredicate)).toBe(true); - - const boolPredicate = (val: any) => typeof val.c === 'boolean'; - expect(someValue(obj, boolPredicate)).toBe(true); - }); - - it('should handle deep nesting with mixed arrays and objects', () => { - const complexObj = { a: { b: [1, 2, { c: 3 }, [{ d: 4 }]] } }; - expect(someValue(complexObj, (val) => val.d === 4)).toBe(true); - }); - - it('should handle functions and undefined values', () => { - const objWithFunc = { a: () => {}, b: undefined }; - const funcPredicate = (val: any) => typeof val.a === 'function'; - expect(someValue(objWithFunc, funcPredicate)).toBe(true); - - const undefinedPredicate = (val: any) => val.b === undefined; - expect(someValue(objWithFunc, undefinedPredicate)).toBe(true); - }); -}); - -describe('mapValue', () => { - const predicate = (obj: any) => obj && obj.process; - const processor = (obj: any, paths: any[]) => ({ ...obj, processed: true, paths }); - - it('should not process object if it does not match the predicate', () => { - const obj = { a: 3, b: { c: 4 } }; - expect(mapValue(obj, predicate, processor)).toEqual(obj); - }); - - it('should process object that matches the predicate', () => { - const obj = { a: { process: true } }; - expect(mapValue(obj, predicate, processor)).toEqual({ - a: { process: true, processed: true, paths: ['a'] }, - }); - }); - - it('should handle nested objects and arrays with various types of predicates', () => { - const complexObj = { - a: { key: 'value' }, - b: [{ key: 'value' }, undefined, null, 0, false], - c: () => {}, - }; - const truthyPredicate = (obj: any) => 'key' in obj && obj.key === 'value'; - const falsePredicate = (obj: any) => false; - - expect(mapValue(complexObj, truthyPredicate, processor)).toEqual({ - a: { key: 'value', processed: true, paths: ['a'] }, - b: [{ key: 'value', processed: true, paths: ['b', 0] }, undefined, null, 0, false], - c: complexObj.c, - }); - - expect(mapValue(complexObj, falsePredicate, processor)).toEqual(complexObj); - }); - - it('should process nested object and arrays that match the predicate', () => { - const nestedObj = { - a: { key: 'value', nested: { key: 'value' } }, - }; - const predicate = (obj: any) => 'key' in obj; - - const result = mapValue(nestedObj, predicate, processor); - - expect(result).toEqual({ - a: { key: 'value', processed: true, paths: ['a'], nested: { key: 'value' } }, - }); - expect(result.a.nested).not.toHaveProperty('processed'); - }); -}); diff --git a/packages/renderer-core/src/index.ts b/packages/renderer-core/src/index.ts index d16f4c6e4..1d37db614 100644 --- a/packages/renderer-core/src/index.ts +++ b/packages/renderer-core/src/index.ts @@ -1,6 +1,6 @@ /* --------------- api -------------------- */ export { createRenderer } from './main'; -export { IBoostsService, IExtensionHostService } from './services/extension'; +export { IExtensionHostService } from './services/extension'; export { definePackageLoader, IPackageManagementService } from './services/package'; export { LifecyclePhase, ILifeCycleService } from './services/lifeCycleService'; export { IComponentTreeModelService } from './services/model'; diff --git a/packages/renderer-core/src/main.ts b/packages/renderer-core/src/main.ts index 41584beca..fcc0f7f2c 100644 --- a/packages/renderer-core/src/main.ts +++ b/packages/renderer-core/src/main.ts @@ -1,54 +1,78 @@ import { invariant, InstantiationService } from '@alilc/lowcode-shared'; -import { ICodeRuntimeService } from './services/code-runtime'; +import type { AppOptions, RendererApplication } from './types'; +import { CodeRuntimeService, ICodeRuntimeService } from './services/code-runtime'; import { IExtensionHostService, type RenderAdapter, type IRenderObject, + ExtensionHostService, } from './services/extension'; -import { IPackageManagementService } from './services/package'; -import { ISchemaService } from './services/schema'; -import { ILifeCycleService, LifecyclePhase } from './services/lifeCycleService'; -import type { AppOptions, RendererApplication } from './types'; +import { IPackageManagementService, PackageManagementService } from './services/package'; +import { ISchemaService, SchemaService } from './services/schema'; +import { ILifeCycleService, LifecyclePhase, LifeCycleService } from './services/lifeCycleService'; +import { IRuntimeIntlService, RuntimeIntlService } from './services/runtimeIntlService'; +import { IRuntimeUtilService, RuntimeUtilService } from './services/runtimeUtilService'; -/** - * 创建 createRenderer 的辅助函数 - * @param schema - * @param options - * @returns - */ export function createRenderer( renderAdapter: RenderAdapter, ): (options: AppOptions) => Promise> { invariant(typeof renderAdapter === 'function', 'The first parameter must be a function.'); - const accessor = new InstantiationService({ defaultScope: 'Singleton' }); - let mode: 'development' | 'production' = 'production'; + const instantiationService = new InstantiationService(); - const schemaService = accessor.get(ISchemaService); - const packageManagementService = accessor.get(IPackageManagementService); - const codeRuntimeService = accessor.get(ICodeRuntimeService); - const lifeCycleService = accessor.get(ILifeCycleService); - const extensionHostService = accessor.get(IExtensionHostService); + // create services + const lifeCycleService = new LifeCycleService(); + instantiationService.container.set(ILifeCycleService, lifeCycleService); return async (options) => { - if (options.mode) mode = options.mode; + const schemaService = new SchemaService(options.schema); + instantiationService.container.set(ISchemaService, schemaService); - // valid schema - schemaService.initialize(options.schema); - codeRuntimeService.initialize(options.codeRuntime ?? {}); - await lifeCycleService.setPhase(LifecyclePhase.OptionsResolved); + const codeRuntimeService = instantiationService.createInstance( + CodeRuntimeService, + options.codeRuntime, + ); + instantiationService.container.set(ICodeRuntimeService, codeRuntimeService); - const renderObject = await renderAdapter(accessor); + const packageManagementService = instantiationService.createInstance(PackageManagementService); + instantiationService.container.set(IPackageManagementService, packageManagementService); + + const utils = schemaService.get('utils'); + const runtimeUtilService = instantiationService.createInstance(RuntimeUtilService, utils); + instantiationService.container.set(IRuntimeUtilService, runtimeUtilService); + + const defaultLocale = schemaService.get('config.defaultLocale'); + const i18ns = schemaService.get('i18n', {}); + const runtimeIntlService = instantiationService.createInstance( + RuntimeIntlService, + defaultLocale, + i18ns, + ); + instantiationService.container.set(IRuntimeIntlService, runtimeIntlService); + + const extensionHostService = new ExtensionHostService( + lifeCycleService, + packageManagementService, + schemaService, + codeRuntimeService, + runtimeIntlService, + runtimeUtilService, + ); + instantiationService.container.set(IExtensionHostService, extensionHostService); + + lifeCycleService.setPhase(LifecyclePhase.OptionsResolved); + + const renderObject = await renderAdapter(instantiationService); await extensionHostService.registerPlugin(options.plugins ?? []); // 先加载插件提供 package loader await packageManagementService.loadPackages(options.packages ?? []); - await lifeCycleService.setPhase(LifecyclePhase.Ready); + lifeCycleService.setPhase(LifecyclePhase.Ready); const app: RendererApplication = { get mode() { - return mode; + return __DEV__ ? 'development' : 'production'; }, schema: schemaService, packageManager: packageManagementService, @@ -57,12 +81,12 @@ export function createRenderer( use: (plugin) => { return extensionHostService.registerPlugin(plugin); }, - destroy: async () => { - return lifeCycleService.setPhase(LifecyclePhase.Destroying); + destroy: () => { + lifeCycleService.setPhase(LifecyclePhase.Destroying); }, }; - if (mode === 'development') { + if (__DEV__) { Object.defineProperty(app, '__options', { get: () => options }); } diff --git a/packages/renderer-core/src/services/code-runtime/codeRuntimeService.ts b/packages/renderer-core/src/services/code-runtime/codeRuntimeService.ts index 134b70e9b..d405eacfa 100644 --- a/packages/renderer-core/src/services/code-runtime/codeRuntimeService.ts +++ b/packages/renderer-core/src/services/code-runtime/codeRuntimeService.ts @@ -1,11 +1,15 @@ -import { createDecorator, invariant, Provide, type StringDictionary } from '@alilc/lowcode-shared'; +import { + createDecorator, + invariant, + Disposable, + type StringDictionary, +} from '@alilc/lowcode-shared'; import { type ICodeRuntime, type CodeRuntimeOptions, CodeRuntime } from './codeRuntime'; +import { ISchemaService } from '../schema'; export interface ICodeRuntimeService { readonly rootRuntime: ICodeRuntime; - initialize(options: CodeRuntimeOptions): void; - createCodeRuntime( options?: CodeRuntimeOptions, ): ICodeRuntime; @@ -13,12 +17,23 @@ export interface ICodeRuntimeService { export const ICodeRuntimeService = createDecorator('codeRuntimeService'); -@Provide(ICodeRuntimeService) -export class CodeRuntimeService implements ICodeRuntimeService { +export class CodeRuntimeService extends Disposable implements ICodeRuntimeService { rootRuntime: ICodeRuntime; - initialize(options?: CodeRuntimeOptions) { + constructor( + options: CodeRuntimeOptions = {}, + @ISchemaService private schemaService: ISchemaService, + ) { + super(); this.rootRuntime = new CodeRuntime(options); + + this.addDispose( + this.schemaService.onSchemaUpdate(({ key, data }) => { + if (key === 'constants') { + this.rootRuntime.getScope().set('constants', data); + } + }), + ); } createCodeRuntime( diff --git a/packages/renderer-core/src/services/extension/boosts.ts b/packages/renderer-core/src/services/extension/boosts.ts index 4a0c1cdd8..d13079d33 100644 --- a/packages/renderer-core/src/services/extension/boosts.ts +++ b/packages/renderer-core/src/services/extension/boosts.ts @@ -1,4 +1,4 @@ -import { createDecorator, Provide, type StringDictionary } from '@alilc/lowcode-shared'; +import { type StringDictionary } from '@alilc/lowcode-shared'; import { isObject } from 'lodash-es'; import { ICodeRuntime, ICodeRuntimeService } from '../code-runtime'; import { IRuntimeUtilService } from '../runtimeUtilService'; @@ -24,17 +24,14 @@ export interface IBoostsApi { /** * 提供了与运行时交互的接口 */ -export interface IBoostsService { +export interface IBoostsManager { extend(name: string, value: any, force?: boolean): void; extend(value: StringDictionary, force?: boolean): void; toExpose(): IBoosts; } -export const IBoostsService = createDecorator('boostsService'); - -@Provide(IBoostsService) -export class BoostsService implements IBoostsService { +export class BoostsManager implements IBoostsManager { private builtInApis: IBoostsApi; private extendsValue: StringDictionary = {}; @@ -42,16 +39,16 @@ export class BoostsService implements IBoostsService { private _expose: any; constructor( - @ICodeRuntimeService codeRuntimeService: ICodeRuntimeService, - @IRuntimeIntlService private runtimeIntlService: IRuntimeIntlService, - @IRuntimeUtilService private runtimeUtilService: IRuntimeUtilService, + codeRuntimeService: ICodeRuntimeService, + runtimeIntlService: IRuntimeIntlService, + runtimeUtilService: IRuntimeUtilService, ) { this.builtInApis = { get codeRuntime() { return codeRuntimeService.rootRuntime; }, - intl: this.runtimeIntlService, - util: this.runtimeUtilService, + intl: runtimeIntlService, + util: runtimeUtilService, temporaryUse: (name, value) => { this.extend(name, value); }, diff --git a/packages/renderer-core/src/services/extension/extensionHostService.ts b/packages/renderer-core/src/services/extension/extensionHostService.ts index fdd2115d5..5533770c3 100644 --- a/packages/renderer-core/src/services/extension/extensionHostService.ts +++ b/packages/renderer-core/src/services/extension/extensionHostService.ts @@ -1,102 +1,112 @@ -import { createDecorator, Provide, EventEmitter } from '@alilc/lowcode-shared'; +import { createDecorator, CyclicDependencyError, Disposable, Graph } from '@alilc/lowcode-shared'; import { type Plugin, type PluginContext } from './plugin'; -import { IBoostsService } from './boosts'; +import { BoostsManager } from './boosts'; import { IPackageManagementService } from '../package'; import { ISchemaService } from '../schema'; import { ILifeCycleService } from '../lifeCycleService'; -import { KeyValueStore } from '../../utils/store'; - -interface IPluginRuntime extends Plugin { - status: 'setup' | 'ready'; -} +import { ICodeRuntimeService } from '../code-runtime'; +import { IRuntimeIntlService } from '../runtimeIntlService'; +import { IRuntimeUtilService } from '../runtimeUtilService'; export interface IExtensionHostService { + readonly boostsManager: BoostsManager; + registerPlugin(plugin: Plugin | Plugin[]): Promise; getPlugin(name: string): Plugin | undefined; - - dispose(): Promise; } export const IExtensionHostService = createDecorator('pluginManagementService'); -@Provide(IExtensionHostService) -export class ExtensionHostService implements IExtensionHostService { - private pluginRuntimes: IPluginRuntime[] = []; +export class ExtensionHostService extends Disposable implements IExtensionHostService { + boostsManager: BoostsManager; - private eventEmitter: EventEmitter; - - private pluginSetupContext: PluginContext; + private _activePlugins = new Set(); + private _pluginStore = new Map(); + private _pluginDependencyGraph = new Graph((name) => name); + private _pluginSetupContext: PluginContext; constructor( - @IPackageManagementService private packageManagementService: IPackageManagementService, - @IBoostsService private boostsService: IBoostsService, - @ISchemaService private schemaService: ISchemaService, - @ILifeCycleService private lifeCycleService: ILifeCycleService, + lifeCycleService: ILifeCycleService, + packageManagementService: IPackageManagementService, + schemaService: ISchemaService, + codeRuntimeService: ICodeRuntimeService, + runtimeIntlService: IRuntimeIntlService, + runtimeUtilService: IRuntimeUtilService, ) { - this.eventEmitter = new EventEmitter('ExtensionHost'); - this.pluginSetupContext = { - eventEmitter: this.eventEmitter, - globalState: new KeyValueStore(), - boosts: this.boostsService.toExpose(), - schema: this.schemaService, - packageManager: this.packageManagementService, + super(); - whenLifeCylePhaseChange: (phase, listener) => { - return this.lifeCycleService.when(phase, listener); + this.boostsManager = new BoostsManager( + codeRuntimeService, + runtimeIntlService, + runtimeUtilService, + ); + + this._pluginSetupContext = { + globalState: new Map(), + boosts: this.boostsManager.toExpose(), + schema: schemaService, + packageManager: packageManagementService, + + whenLifeCylePhaseChange: (phase) => { + return lifeCycleService.when(phase); }, }; } async registerPlugin(plugins: Plugin | Plugin[]) { - plugins = Array.isArray(plugins) ? plugins : [plugins]; + const items = (Array.isArray(plugins) ? plugins : [plugins]).filter( + (plugin) => !this._pluginStore.has(plugin.name), + ); + for (const item of items) { + this._pluginStore.set(item.name, item); + } + + await this._doRegisterPlugins(items); + } + + private async _doRegisterPlugins(plugins: Plugin[]) { for (const plugin of plugins) { - if (this.pluginRuntimes.find((item) => item.name === plugin.name)) { - console.warn(`${plugin.name} 插件已注册`); - continue; + this._pluginDependencyGraph.lookupOrInsertNode(plugin.name); + + if (plugin.dependsOn) { + for (const dependency of plugin.dependsOn) { + this._pluginDependencyGraph.insertEdge(plugin.name, dependency); + } + } + } + + while (true) { + const roots = this._pluginDependencyGraph.roots(); + + if (roots.length === 0 || roots.every((node) => !this._pluginStore.has(node.data))) { + if (this._pluginDependencyGraph.isEmpty()) { + throw new CyclicDependencyError(this._pluginDependencyGraph); + } + break; } - const pluginRuntime = plugin as IPluginRuntime; - - pluginRuntime.status = 'ready'; - this.pluginRuntimes.push(pluginRuntime); - - await this.doSetupPlugin(pluginRuntime); + for (const { data } of roots) { + const plugin = this._pluginStore.get(data); + if (plugin) { + await this._doSetupPlugin(plugin); + this._pluginDependencyGraph.removeNode(plugin.name); + } + } } } - private async doSetupPlugin(pluginRuntime: IPluginRuntime) { - if (pluginRuntime.status === 'setup') return; + private async _doSetupPlugin(plugin: Plugin) { + if (this._activePlugins.has(plugin.name)) return; - const isSetup = (name: string) => { - const setupPlugins = this.pluginRuntimes.filter((item) => item.status === 'setup'); - return setupPlugins.some((p) => p.name === name); - }; - - if (pluginRuntime.dependsOn?.some((dep) => !isSetup(dep))) { - return; - } - - await pluginRuntime.setup(this.pluginSetupContext); - pluginRuntime.status = 'setup'; - - // 遍历未安装的插件 寻找 dependsOn 的插件已安装完的插件进行安装 - const readyPlugins = this.pluginRuntimes.filter((item) => item.status === 'ready'); - const readyPlugin = readyPlugins.find((item) => item.dependsOn?.every((dep) => isSetup(dep))); - if (readyPlugin) { - await this.doSetupPlugin(readyPlugin); - } + await plugin.setup(this._pluginSetupContext); + this._activePlugins.add(plugin.name); + this.addDispose(plugin); } getPlugin(name: string): Plugin | undefined { - return this.pluginRuntimes.find((item) => item.name === name); - } - - async dispose(): Promise { - for (const plugin of this.pluginRuntimes) { - await plugin.destory?.(); - } + return this._pluginStore.get(name); } } diff --git a/packages/renderer-core/src/services/extension/plugin.ts b/packages/renderer-core/src/services/extension/plugin.ts index c4b017cbc..3e94310d5 100644 --- a/packages/renderer-core/src/services/extension/plugin.ts +++ b/packages/renderer-core/src/services/extension/plugin.ts @@ -1,4 +1,4 @@ -import { type EventEmitter, type StringDictionary } from '@alilc/lowcode-shared'; +import { type StringDictionary, type IDisposable } from '@alilc/lowcode-shared'; import { type IBoosts } from './boosts'; import { ILifeCycleService } from '../lifeCycleService'; import { type ISchemaService } from '../schema'; @@ -6,23 +6,29 @@ import { type IPackageManagementService } from '../package'; import { type IStore } from '../../utils/store'; export interface PluginContext { - eventEmitter: EventEmitter; globalState: IStore; + boosts: IBoosts; + schema: Pick; + packageManager: IPackageManagementService; - /** - * 生命周期变更事件 - */ + whenLifeCylePhaseChange: ILifeCycleService['when']; } -export interface Plugin { +export interface Plugin extends IDisposable { /** * 插件的 name 作为唯一标识,并不可重复。 */ name: string; + /** + * 插件启动函数 + * @param context 插件能力上下文 + */ setup(context: PluginContext): void | Promise; - destory?(): void | Promise; + /** + * 插件的依赖插件 + */ dependsOn?: string[]; } diff --git a/packages/renderer-core/src/services/extension/render.ts b/packages/renderer-core/src/services/extension/render.ts index 61a1bfa44..a6364a6ca 100644 --- a/packages/renderer-core/src/services/extension/render.ts +++ b/packages/renderer-core/src/services/extension/render.ts @@ -1,4 +1,4 @@ -import { type InstanceAccessor } from '@alilc/lowcode-shared'; +import { type IInstantiationService } from '@alilc/lowcode-shared'; export interface IRenderObject { mount: (containerOrId?: string | HTMLElement) => void | Promise; @@ -6,5 +6,5 @@ export interface IRenderObject { } export interface RenderAdapter { - (accessor: InstanceAccessor): Render | Promise; + (instantiationService: IInstantiationService): Render | Promise; } diff --git a/packages/renderer-core/src/services/lifeCycleService.ts b/packages/renderer-core/src/services/lifeCycleService.ts index c84d70e3b..146b830d9 100644 --- a/packages/renderer-core/src/services/lifeCycleService.ts +++ b/packages/renderer-core/src/services/lifeCycleService.ts @@ -1,4 +1,4 @@ -import { Provide, createDecorator, EventEmitter, EventDisposable } from '@alilc/lowcode-shared'; +import { createDecorator, Barrier } from '@alilc/lowcode-shared'; /** * 生命周期阶段 @@ -28,13 +28,15 @@ export interface ILifeCycleService { */ phase: LifecyclePhase; - setPhase(phase: LifecyclePhase): Promise; + setPhase(phase: LifecyclePhase): void; /** * Returns a promise that resolves when a certain lifecycle phase * has started. */ - when(phase: LifecyclePhase, listener: () => void | Promise): EventDisposable; + when(phase: LifecyclePhase): Promise; + + onWillDestory(): void; } export function LifecyclePhaseToString(phase: LifecyclePhase): string { @@ -52,9 +54,8 @@ export function LifecyclePhaseToString(phase: LifecyclePhase): string { export const ILifeCycleService = createDecorator('lifeCycleService'); -@Provide(ILifeCycleService) export class LifeCycleService implements ILifeCycleService { - private readonly phaseWhen = new EventEmitter(); + private readonly phaseWhen = new Map(); private _phase = LifecyclePhase.Starting; @@ -62,7 +63,7 @@ export class LifeCycleService implements ILifeCycleService { return this._phase; } - async setPhase(value: LifecyclePhase) { + setPhase(value: LifecyclePhase) { if (value < this._phase) { throw new Error('Lifecycle cannot go backwards'); } @@ -73,10 +74,26 @@ export class LifeCycleService implements ILifeCycleService { this._phase = value; - await this.phaseWhen.emit(LifecyclePhaseToString(value)); + const barrier = this.phaseWhen.get(this._phase); + if (barrier) { + barrier.open(); + this.phaseWhen.delete(this._phase); + } } - when(phase: LifecyclePhase, listener: () => void | Promise) { - return this.phaseWhen.on(LifecyclePhaseToString(phase), listener); + async when(phase: LifecyclePhase): Promise { + if (phase <= this._phase) { + return; + } + + let barrier = this.phaseWhen.get(phase); + if (!barrier) { + barrier = new Barrier(); + this.phaseWhen.set(phase, barrier); + } + + await barrier.wait(); } + + onWillDestory(): void {} } diff --git a/packages/renderer-core/src/services/model/componentTreeModelService.ts b/packages/renderer-core/src/services/model/componentTreeModelService.ts index 486efd4a4..2401ea5d0 100644 --- a/packages/renderer-core/src/services/model/componentTreeModelService.ts +++ b/packages/renderer-core/src/services/model/componentTreeModelService.ts @@ -1,6 +1,5 @@ import { createDecorator, - Provide, invariant, type ComponentTree, type StringDictionary, @@ -33,7 +32,6 @@ export const IComponentTreeModelService = createDecorator = {}; private packageStore: Map = ((window as any).__PACKAGE_STORE__ ??= new Map()); @@ -62,13 +60,18 @@ export class PackageManagementService implements IPackageManagementService { private packageLoaders: PackageLoader[] = []; - constructor( - @ISchemaService private schemaService: ISchemaService, - @ILifeCycleService private lifeCycleService: ILifeCycleService, - ) { - this.schemaService.onChange('componentsMap', (componentsMaps) => { - this.resolveComponentMaps(componentsMaps); - }); + constructor(@ISchemaService private schemaService: ISchemaService) { + super(); + + this.addDispose( + this.schemaService.onSchemaUpdate(({ key, previous, data }) => { + if (key === 'componentsMap') { + // todo: add remove ... + const diff = differenceWith(data, previous, isEqual); + if (diff.length > 0) this.resolveComponentMaps(diff); + } + }), + ); } async loadPackages(packages: Package[]) { diff --git a/packages/renderer-core/src/services/package/loader.ts b/packages/renderer-core/src/services/package/package.ts similarity index 100% rename from packages/renderer-core/src/services/package/loader.ts rename to packages/renderer-core/src/services/package/package.ts diff --git a/packages/renderer-core/src/services/runtimeIntlService.ts b/packages/renderer-core/src/services/runtimeIntlService.ts index 4979ffd37..551c89bc0 100644 --- a/packages/renderer-core/src/services/runtimeIntlService.ts +++ b/packages/renderer-core/src/services/runtimeIntlService.ts @@ -1,15 +1,12 @@ import { createDecorator, - Provide, Intl, type IntlApi, type Locale, type Translations, - Platform, + type LocaleTranslationsMap, } from '@alilc/lowcode-shared'; -import { ILifeCycleService, LifecyclePhase } from './lifeCycleService'; import { ICodeRuntimeService } from './code-runtime'; -import { ISchemaService } from './schema'; export interface MessageDescriptor { key: string; @@ -18,8 +15,6 @@ export interface MessageDescriptor { } export interface IRuntimeIntlService { - locale: string; - localize(descriptor: MessageDescriptor): string; setLocale(locale: Locale): void; @@ -31,32 +26,21 @@ export interface IRuntimeIntlService { export const IRuntimeIntlService = createDecorator('IRuntimeIntlService'); -@Provide(IRuntimeIntlService) export class RuntimeIntlService implements IRuntimeIntlService { private intl: Intl = new Intl(); - public locale: string = Platform.platformLocale; - constructor( - @ILifeCycleService private lifeCycleService: ILifeCycleService, + defaultLocale: string | undefined, + i18nTranslations: LocaleTranslationsMap, @ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService, - @ISchemaService private schemaService: ISchemaService, ) { - this.lifeCycleService.when(LifecyclePhase.OptionsResolved, () => { - const config = this.schemaService.get('config'); - const i18nTranslations = this.schemaService.get('i18n'); + if (defaultLocale) this.setLocale(defaultLocale); - if (config?.defaultLocale) { - this.setLocale(config.defaultLocale); - } - if (i18nTranslations) { - Object.keys(i18nTranslations).forEach((key) => { - this.addTranslations(key, i18nTranslations[key]); - }); - } + for (const key of Object.keys(i18nTranslations)) { + this.addTranslations(key, i18nTranslations[key]); + } - this.injectScope(); - }); + this._injectScope(); } localize(descriptor: MessageDescriptor): string { @@ -83,7 +67,7 @@ export class RuntimeIntlService implements IRuntimeIntlService { this.intl.addTranslations(locale, translations); } - private injectScope(): void { + private _injectScope(): void { const exposed: IntlApi = { i18n: (key, params) => { return this.localize({ key, params }); diff --git a/packages/renderer-core/src/services/runtimeUtilService.ts b/packages/renderer-core/src/services/runtimeUtilService.ts index b2b84eb12..73e36bcf0 100644 --- a/packages/renderer-core/src/services/runtimeUtilService.ts +++ b/packages/renderer-core/src/services/runtimeUtilService.ts @@ -2,14 +2,11 @@ import { type AnyFunction, type UtilDescription, createDecorator, - Provide, type StringDictionary, } from '@alilc/lowcode-shared'; import { isPlainObject } from 'lodash-es'; import { IPackageManagementService } from './package'; import { ICodeRuntimeService } from './code-runtime'; -import { ISchemaService } from './schema'; -import { ILifeCycleService, LifecyclePhase } from './lifeCycleService'; export interface IRuntimeUtilService { add(utilItem: UtilDescription, force?: boolean): void; @@ -20,25 +17,18 @@ export interface IRuntimeUtilService { export const IRuntimeUtilService = createDecorator('rendererUtilService'); -@Provide(IRuntimeUtilService) export class RuntimeUtilService implements IRuntimeUtilService { private utilsMap: Map = new Map(); constructor( + utils: UtilDescription[] = [], @ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService, @IPackageManagementService private packageManagementService: IPackageManagementService, - @ISchemaService private schemaService: ISchemaService, - @ILifeCycleService private lifeCycleService: ILifeCycleService, ) { - this.lifeCycleService.when(LifecyclePhase.OptionsResolved, () => { - this.injectScope(); - }); - - this.schemaService.onChange('utils', (utils = []) => { - for (const util of utils) { - this.add(util); - } - }); + for (const util of utils) { + this.add(util); + } + this.injectScope(); } add(utilItem: UtilDescription, force?: boolean): void; diff --git a/packages/renderer-core/src/services/schema/schemaService.ts b/packages/renderer-core/src/services/schema/schemaService.ts index 511123afb..6f4ba0daa 100644 --- a/packages/renderer-core/src/services/schema/schemaService.ts +++ b/packages/renderer-core/src/services/schema/schemaService.ts @@ -1,85 +1,63 @@ -import { - type Project, - createDecorator, - Provide, - EventEmitter, - type EventDisposable, -} from '@alilc/lowcode-shared'; -import { isObject } from 'lodash-es'; +import { Disposable, type Project, createDecorator, Events } from '@alilc/lowcode-shared'; +import { isObject, isEqual, get as lodashGet, set as lodashSet } from 'lodash-es'; import { schemaValidation } from './validation'; -import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService'; -import { ICodeRuntimeService } from '../code-runtime'; -import { type IStore, KeyValueStore } from '../../utils/store'; export interface NormalizedSchema extends Project {} export type NormalizedSchemaKey = keyof NormalizedSchema; +export type SchemaUpdateEvent = { key: string; previous: any; data: any }; + export interface ISchemaService { - initialize(schema: Project): void; + readonly onSchemaUpdate: Events.Event; - get(key: K): NormalizedSchema[K]; + get(key: string): T | undefined; + get(key: string, defaultValue?: T): T; - set(key: K, value: NormalizedSchema[K]): Promise; - - onChange( - key: K, - listener: (v: NormalizedSchema[K]) => void, - ): EventDisposable; + set(key: string, value: any): void; } export const ISchemaService = createDecorator('schemaService'); -@Provide(ISchemaService) -export class SchemaService implements ISchemaService { - private store: IStore = new KeyValueStore< - NormalizedSchema, - NormalizedSchemaKey - >({ - setterValidation: schemaValidation, - }); +export class SchemaService extends Disposable implements ISchemaService { + private store: NormalizedSchema; - private notifyEmiiter = new EventEmitter(); + private _observer = this.addDispose(new Events.Observable()); - constructor( - @ILifeCycleService private lifeCycleService: ILifeCycleService, - @ICodeRuntimeService private codeRuntimeService: ICodeRuntimeService, - ) { - this.onChange('constants', (value = {}) => { - this.codeRuntimeService.rootRuntime.getScope().set('constants', value); - }); + readonly onSchemaUpdate = this._observer.subscribe; - this.lifeCycleService.when(LifecyclePhase.Destroying, () => { - this.notifyEmiiter.removeAll(); - }); - } + constructor(schema: unknown) { + super(); - initialize(schema: unknown): void { if (!isObject(schema)) { throw Error('schema must a object'); } - Object.keys(schema).forEach((key) => { - // @ts-expect-error: ignore initialization - this.set(key, schema[key]); - }); - } + this.store = {} as any; + for (const key of Object.keys(schema)) { + const value = (schema as any)[key]; - async set(key: K, value: NormalizedSchema[K]): Promise { - if (value !== this.get(key)) { - this.store.set(key, value); - await this.notifyEmiiter.emit(key, value); + // todo: schemas validate + const valid = schemaValidation(key as any, value); + if (valid !== true) { + throw new Error( + `failed to config ${key.toString()}, validation error: ${valid ? valid : ''}`, + ); + } + + this.set(key, value); } } - get(key: K): NormalizedSchema[K] { - return this.store.get(key) as NormalizedSchema[K]; + set(key: string, value: any): void { + const previous = this.get(key); + if (!isEqual(previous, value)) { + lodashSet(this.store, key, value); + this._observer.notify({ key, previous, data: value }); + } } - onChange( - key: K, - listener: (v: NormalizedSchema[K]) => void | Promise, - ): EventDisposable { - return this.notifyEmiiter.on(key, listener); + get(key: string, defaultValue?: T): T { + return (lodashGet(this.store, key) ?? defaultValue) as T; } } diff --git a/packages/renderer-core/src/services/schema/validation.ts b/packages/renderer-core/src/services/schema/validation.ts index 705e1a535..d28bb231e 100644 --- a/packages/renderer-core/src/services/schema/validation.ts +++ b/packages/renderer-core/src/services/schema/validation.ts @@ -38,12 +38,12 @@ const SCHEMA_VALIDATIONS_OPTIONS: Partial = { }, }; -export function schemaValidation(key: K, value: Project[K]) { +export function schemaValidation(key: string, value: unknown) { if (!SCHEMA_KEYS.includes(key)) { return `schema 的字段名必须是${JSON.stringify(SCHEMA_KEYS)}中的一个`; } - const validOption = SCHEMA_VALIDATIONS_OPTIONS[key]; + const validOption = (SCHEMA_VALIDATIONS_OPTIONS as any)[key]; if (validOption) { const result = validOption.valid(value); diff --git a/packages/renderer-core/src/types.ts b/packages/renderer-core/src/types.ts index 151638e68..a2e00b598 100644 --- a/packages/renderer-core/src/types.ts +++ b/packages/renderer-core/src/types.ts @@ -9,10 +9,6 @@ export interface AppOptions { schema: Project; packages?: Package[]; plugins?: Plugin[]; - /** - * 运行模式 - */ - mode?: 'development' | 'production'; /** * code runtime 设置选项 */ @@ -32,5 +28,5 @@ export type RendererApplication = { use(plugin: Plugin): Promise; - destroy(): Promise; + destroy(): void; } & Render; diff --git a/packages/renderer-core/src/utils/store.ts b/packages/renderer-core/src/utils/store.ts deleted file mode 100644 index 34299f0ae..000000000 --- a/packages/renderer-core/src/utils/store.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { type StringDictionary } from '@alilc/lowcode-shared'; - -/** - * MapLike interface - */ -export interface IStore { - readonly size: number; - - get(key: K, defaultValue: O[K]): O[K]; - get(key: K, defaultValue?: O[K]): O[K] | undefined; - - set(key: K, value: O[K]): void; - - delete(key: K): void; - - clear(): void; -} - -export class KeyValueStore - implements IStore -{ - private readonly store = new Map(); - - private setterValidation: ((key: K, value: O[K]) => boolean | string) | undefined; - - constructor(options?: { setterValidation?: (key: K, value: O[K]) => boolean | string }) { - if (options?.setterValidation) { - this.setterValidation = options.setterValidation; - } - } - - get(key: K, defaultValue: O[K]): O[K]; - get(key: K, defaultValue?: O[K] | undefined): O[K] | undefined; - get(key: K, defaultValue?: O[K]): O[K] | undefined { - const value = this.store.get(key); - return value ?? defaultValue; - } - - set(key: K, value: O[K]): void { - if (this.setterValidation) { - const valid = this.setterValidation(key, value); - - if (valid !== true) { - console.warn(`failed to config ${key.toString()}, validation error: ${valid ? valid : ''}`); - return; - } - } - - this.store.set(key, value); - } - - delete(key: K): void { - this.store.delete(key); - } - - clear(): void { - this.store.clear(); - } - - get size(): number { - return this.store.size; - } -} diff --git a/packages/renderer-core/tsconfig.json b/packages/renderer-core/tsconfig.json index 5784de4ad..5348ce087 100644 --- a/packages/renderer-core/tsconfig.json +++ b/packages/renderer-core/tsconfig.json @@ -3,5 +3,4 @@ "compilerOptions": { "outDir": "dist" }, - "include": ["src", "../shared/src/utils/node.ts"] } diff --git a/packages/renderer-router/tsconfig.json b/packages/renderer-router/tsconfig.json index 039e0b4d1..b4e69ae1f 100644 --- a/packages/renderer-router/tsconfig.json +++ b/packages/renderer-router/tsconfig.json @@ -2,6 +2,5 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist" - }, - "include": ["src"] + } } diff --git a/packages/shared/__tests__/utils/async.spec.ts b/packages/shared/__tests__/utils/async.spec.ts deleted file mode 100644 index 8f5838909..000000000 --- a/packages/shared/__tests__/utils/async.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { Barrier } from '../../src'; - -describe('Barrier', () => { - it('waits for barrier to open', async () => { - const barrier = new Barrier(); - - setTimeout(() => { - barrier.open(); - }, 500); // Simulate opening the barrier after 500ms - - const start = Date.now(); - await barrier.wait(); // Async operation should await here - const duration = Date.now() - start; - - expect(barrier.isOpen()).toBeTruthy(); - expect(duration).toBeGreaterThanOrEqual(500); // Ensures we waited for at least 500ms - }); - - it('mutiple', async () => { - let result = ''; - const b1 = new Barrier(); - const b2 = new Barrier(); - - async function run1() { - await b1.wait(); - } - async function run2() { - await b2.wait(); - } - - run1().then(() => { - result += 1; - }); - run1().finally(() => { - result += 2; - }); - - run2().then(() => { - result += 3; - }); - run2().finally(() => { - result += 4; - }); - - b1.open(); - - await run1(); - - b2.open(); - - await run2(); - - expect(result).toBe('1234'); - }); -}); diff --git a/packages/shared/package.json b/packages/shared/package.json index dace9d2ab..87c0d64d6 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -24,12 +24,8 @@ "test:watch": "vitest" }, "dependencies": { - "@abraham/reflection": "^0.12.0", "@formatjs/intl": "^2.10.2", "@vue/reactivity": "^3.4.23", - "inversify": "^6.0.2", - "inversify-binding-decorators": "^4.0.0", - "hookable": "^5.5.3", "lodash-es": "^4.17.21", "store": "^2.0.12" }, diff --git a/packages/shared/src/common/disposable.ts b/packages/shared/src/common/disposable.ts new file mode 100644 index 000000000..88e71fca1 --- /dev/null +++ b/packages/shared/src/common/disposable.ts @@ -0,0 +1,208 @@ +import { createSingleCallFunction, Iterable } from '../utils'; + +/** + * An object that performs a cleanup operation when `.dispose()` is called. + * + * Some examples of how disposables are used: + * + * - An event listener that removes itself when `.dispose()` is called. + * - A resource such as a file system watcher that cleans up the resource when `.dispose()` is called. + * - The return value from registering a provider. When `.dispose()` is called, the provider is unregistered. + */ +export interface IDisposable { + dispose(): void; +} + +/** + * Check if `thing` is {@link IDisposable disposable}. + */ +export function isDisposable(thing: E): thing is E & IDisposable { + return ( + typeof thing === 'object' && + thing !== null && + typeof (thing as unknown as IDisposable).dispose === 'function' && + (thing as unknown as IDisposable).dispose.length === 0 + ); +} + +const noop = () => {}; + +export abstract class Disposable implements IDisposable { + static Noop: IDisposable = { dispose: noop }; + + private readonly _store = new DisposableStore(); + + dispose(): void { + this._store.dispose(); + } + + /** + * Adds `o` to the collection of disposables managed by this object. + */ + protected addDispose(o: T): T { + if ((o as unknown as Disposable) === this) { + throw new Error('Cannot register a disposable on itself!'); + } + return this._store.add(o); + } +} + +/** + * Manages a collection of disposable values. + * + * This is the preferred way to manage multiple disposables. A `DisposableStore` is safer to work with than an + * `IDisposable[]` as it considers edge cases, such as registering the same value multiple times or adding an item to a + * store that has already been disposed of. + */ +export class DisposableStore implements IDisposable { + static DISABLE_DISPOSED_WARNING = false; + + private readonly _toDispose = new Set(); + private _isDisposed = false; + + /** + * Dispose of all registered disposables and mark this object as disposed. + * + * Any future disposables added to this object will be disposed of on `add`. + */ + dispose(): void { + if (this._isDisposed) { + return; + } + + this._isDisposed = true; + this.clear(); + } + + /** + * @return `true` if this object has been disposed of. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * Dispose of all registered disposables but do not mark this object as disposed. + */ + clear(): void { + if (this._toDispose.size === 0) { + return; + } + + try { + dispose(this._toDispose); + } finally { + this._toDispose.clear(); + } + } + + /** + * Add a new {@link IDisposable disposable} to the collection. + */ + add(o: T): T { + if (!o) { + return o; + } + if ((o as unknown as DisposableStore) === this) { + throw new Error('Cannot register a disposable on itself!'); + } + + if (this._isDisposed) { + if (!DisposableStore.DISABLE_DISPOSED_WARNING) { + console.warn( + new Error( + 'Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!', + ).stack, + ); + } + } else { + this._toDispose.add(o); + } + + return o; + } + + /** + * Deletes a disposable from store and disposes of it. This will not throw or warn and proceed to dispose the + * disposable even when the disposable is not part in the store. + */ + delete(o: T): void { + if (!o) { + return; + } + if ((o as unknown as DisposableStore) === this) { + throw new Error('Cannot dispose a disposable on itself!'); + } + this._toDispose.delete(o); + o.dispose(); + } + + /** + * Deletes the value from the store, but does not dispose it. + */ + deleteAndLeak(o: T): void { + if (!o) { + return; + } + if (this._toDispose.has(o)) { + this._toDispose.delete(o); + } + } +} + +/** + * Disposes of the value(s) passed in. + */ +export function dispose(disposable: T): T; +export function dispose(disposable: T | undefined): T | undefined; +export function dispose = Iterable>( + disposables: A, +): A; +export function dispose(disposables: Array): Array; +export function dispose(disposables: ReadonlyArray): ReadonlyArray; +export function dispose(arg: T | Iterable | undefined): any { + if (Iterable.is(arg)) { + const errors: any[] = []; + + for (const d of arg) { + if (d) { + try { + d.dispose(); + } catch (e) { + errors.push(e); + } + } + } + + if (errors.length === 1) { + throw errors[0]; + } else if (errors.length > 1) { + throw new AggregateError(errors, 'Encountered errors while disposing of store'); + } + + return Array.isArray(arg) ? [] : arg; + } else if (arg) { + arg.dispose(); + return arg; + } +} + +/** + * Turn a function that implements dispose into an {@link IDisposable}. + * + * @param fn Clean up function, guaranteed to be called only **once**. + */ +export function toDisposable(fn: () => void): IDisposable { + return { + dispose: createSingleCallFunction(() => { + fn(); + }), + }; +} + +/** + * Combine multiple disposable values into a single {@link IDisposable}. + */ +export function combinedDisposable(...disposables: IDisposable[]): IDisposable { + return toDisposable(() => dispose(disposables)); +} diff --git a/packages/shared/src/common/errors.ts b/packages/shared/src/common/errors.ts new file mode 100644 index 000000000..dc3256423 --- /dev/null +++ b/packages/shared/src/common/errors.ts @@ -0,0 +1,7 @@ +export function illegalArgument(name?: string): Error { + if (name) { + return new Error(`Illegal argument: ${name}`); + } else { + return new Error('Illegal argument'); + } +} diff --git a/packages/shared/src/common/event.ts b/packages/shared/src/common/event.ts index 876729a45..1e8c58324 100644 --- a/packages/shared/src/common/event.ts +++ b/packages/shared/src/common/event.ts @@ -1,132 +1,59 @@ -import { Hookable, type HookKeys } from 'hookable'; +import { Disposable, IDisposable, toDisposable } from './disposable'; -type ArrayT = T extends any[] ? T : [T]; +export type Event = (listener: (arg: T, thisArg?: any) => any) => IDisposable; -export type Event = (listener: EventListener) => EventDisposable; -export type EventListener = (...arguments_: ArrayT) => Promise | void; -export type EventDisposable = () => void; +export class Observable { + private _isDisposed = false; -export interface IEmitter { - on: Event; - emit(...args: ArrayT): void; - emitAsync(...args: ArrayT): Promise; - clear(): void; -} + private _event?: Event; + private _listeners?: Set<(arg: T) => void>; -export class Emitter implements IEmitter { - private events: EventListener[] = []; + dispose(): void { + if (this._isDisposed) return; - on(fn: EventListener): EventDisposable { - this.events.push(fn); - - return () => { - this.events = this.events.filter((e) => e !== fn); - }; + this._listeners?.clear(); + this._listeners = undefined; + this._event = undefined; + this._isDisposed = true; } - emit(...args: ArrayT) { - for (const event of this.events) { - event.call(null, ...args); + notify(arg: T): void { + if (this._isDisposed) return; + + this._listeners?.forEach((listener) => listener(arg)); + } + + /** + * For the public to allow to subscribe to events from this Observable + */ + get subscribe(): Event { + if (!this._event) { + this._event = (listener: (arg: T) => void, thisArg?: any) => { + if (this._isDisposed) { + return Disposable.Noop; + } + + if (thisArg) { + listener = listener.bind(thisArg); + } + + if (!this._listeners) this._listeners = new Set(); + this._listeners.add(listener); + + return toDisposable(() => { + this._removeListener(listener); + }); + }; + } + + return this._event; + } + + private _removeListener(listener: (arg: T) => void) { + if (this._isDisposed) return; + + if (this._listeners?.has(listener)) { + this._listeners.delete(listener); } } - - async emitAsync(...args: ArrayT) { - for (const event of this.events) { - await event.call(null, ...args); - } - } - - clear() { - this.events.length = 0; - } -} - -export interface IEventEmitter< - EventT extends Record = Record, - EventNameT extends HookKeys = HookKeys, -> { - /** - * 监听事件 - * add monitor to a event - * @param event 事件名称 - * @param listener 事件回调 - */ - on(event: EventNameT, listener: EventT[EventNameT]): EventDisposable; - - /** - * 添加只运行一次的监听事件 - * @param event 事件名称 - * @param listener 事件回调 - */ - once(event: EventNameT, listener: EventT[EventNameT]): void; - - /** - * 触发事件 - * emit a message for a event - * @param event 事件名称 - * @param args 事件参数 - */ - emit(event: EventNameT, ...args: any): Promise; - - /** - * 取消监听事件 - * cancel a monitor from a event - * @param event 事件名称 - * @param listener 事件回调 - */ - off(event: EventNameT, listener: EventT[EventNameT]): void; - - /** - * 监听事件,会在其他回调函数之前执行 - * @param event 事件名称 - * @param listener 事件回调 - */ - prependListener(event: EventNameT, listener: EventT[EventNameT]): EventDisposable; - - /** - * 清除所有事件监听 - */ - removeAll(): void; -} - -export class EventEmitter< - EventT extends Record = Record>, - EventNameT extends HookKeys = HookKeys, -> implements IEventEmitter -{ - private namespace: string | undefined; - private hooks = new Hookable(); - - constructor(namespace?: string) { - this.namespace = namespace; - } - - on(event: EventNameT, listener: EventT[EventNameT]): EventDisposable { - return this.hooks.hook(event, listener); - } - - once(event: EventNameT, listener: EventT[EventNameT]): void { - this.hooks.hookOnce(event, listener); - } - - async emit(event: EventNameT, ...args: any) { - return this.hooks.callHook(event, ...args); - } - - off(event: EventNameT, listener: EventT[EventNameT]): void { - this.hooks.removeHook(event, listener); - } - - /** - * 监听事件,会在其他回调函数之前执行 - * @param event 事件名称 - * @param listener 事件回调 - */ - prependListener(event: EventNameT, listener: EventT[EventNameT]): EventDisposable { - return this.hooks.hook(`${event}:before` as EventNameT, listener); - } - - removeAll(): void { - this.hooks.removeAllHooks(); - } } diff --git a/packages/shared/src/common/graph.ts b/packages/shared/src/common/graph.ts new file mode 100644 index 000000000..226136b56 --- /dev/null +++ b/packages/shared/src/common/graph.ts @@ -0,0 +1,112 @@ +export class Node { + readonly incoming = new Map>(); + readonly outgoing = new Map>(); + + constructor( + readonly key: string, + readonly data: T, + ) {} +} + +/** + * 有向图 + */ +export class Graph { + private readonly _nodes = new Map>(); + + constructor(private readonly _hashFn: (element: T) => string) {} + + roots(): Node[] { + const ret: Node[] = []; + for (const node of this._nodes.values()) { + if (node.outgoing.size === 0) { + ret.push(node); + } + } + return ret; + } + + insertEdge(from: T, to: T): void { + const fromNode = this.lookupOrInsertNode(from); + const toNode = this.lookupOrInsertNode(to); + + fromNode.outgoing.set(toNode.key, toNode); + toNode.incoming.set(fromNode.key, fromNode); + } + + removeNode(data: T): void { + const key = this._hashFn(data); + this._nodes.delete(key); + for (const node of this._nodes.values()) { + node.outgoing.delete(key); + node.incoming.delete(key); + } + } + + lookupOrInsertNode(data: T): Node { + const key = this._hashFn(data); + let node = this._nodes.get(key); + + if (!node) { + node = new Node(key, data); + this._nodes.set(key, node); + } + + return node; + } + + lookup(data: T): Node | undefined { + return this._nodes.get(this._hashFn(data)); + } + + isEmpty(): boolean { + return this._nodes.size === 0; + } + + toString(): string { + const data: string[] = []; + for (const [key, value] of this._nodes) { + data.push( + `${key}\n\t(-> incoming)[${[...value.incoming.keys()].join(', ')}]\n\t(outgoing ->)[${[...value.outgoing.keys()].join(',')}]\n`, + ); + } + return data.join('\n'); + } + + /** + * 暴力搜索,仅用于排查故障 + */ + findCycleSlow() { + for (const [id, node] of this._nodes) { + const seen = new Set([id]); + const res = this._findCycle(node, seen); + if (res) { + return res; + } + } + return undefined; + } + + private _findCycle(node: Node, seen: Set): string | undefined { + for (const [id, outgoing] of node.outgoing) { + if (seen.has(id)) { + return [...seen, id].join(' -> '); + } + seen.add(id); + const value = this._findCycle(outgoing, seen); + if (value) { + return value; + } + seen.delete(id); + } + return undefined; + } +} + +export class CyclicDependencyError extends Error { + constructor(graph: Graph) { + super('cyclic dependency between services'); + this.message = + graph.findCycleSlow() ?? `UNABLE to detect cycle, dumping graph: \n${graph.toString()}`; + } +} diff --git a/packages/shared/src/common/index.ts b/packages/shared/src/common/index.ts index a7cbc2dc6..b888c4471 100644 --- a/packages/shared/src/common/index.ts +++ b/packages/shared/src/common/index.ts @@ -1,10 +1,16 @@ -import * as Platform from './platform'; +import * as Events from './event'; +import * as Signals from './signals'; -export { Platform }; +export { Events, Signals }; -export * from './event'; +export * from './platform'; export * from './logger'; export * from './intl'; export * from './instantiation'; -export * from './signals'; + +export * from './keyCodes'; +export * from './errors'; +export * from './disposable'; + export * from './linkedList'; +export * from './graph'; diff --git a/packages/shared/src/common/instantiation/container.ts b/packages/shared/src/common/instantiation/container.ts new file mode 100644 index 000000000..544def430 --- /dev/null +++ b/packages/shared/src/common/instantiation/container.ts @@ -0,0 +1,59 @@ +/** + * Identifies a bean of type `T`. + * The name Bean comes from Spring(Java) + */ +export interface BeanIdentifier { + (...args: any[]): void; + type: T; +} + +export class CtorDescriptor { + constructor( + readonly ctor: Constructor, + readonly staticArguments: any[] = [], + ) {} +} + +export class BeanContainer { + private _entries = new Map, any>(); + + constructor(...entries: [BeanIdentifier, any][]) { + for (const [id, instance] of entries) { + this.set(id, instance); + } + } + + set(id: BeanIdentifier, instance: T | CtorDescriptor): T | CtorDescriptor { + const result = this._entries.get(id); + this._entries.set(id, instance); + return result; + } + + has(id: BeanIdentifier): boolean { + return this._entries.has(id); + } + + get(id: BeanIdentifier): T | CtorDescriptor { + return this._entries.get(id); + } +} + +export type Constructor = new (...args: any[]) => T; + +const TARGET = '$TARGET$'; +const DEPENDENCIES = '$DEPENDENCIES$'; + +export function mapDepsToBeanId(beanId: BeanIdentifier, target: Constructor, index: number) { + if ((target as any)[TARGET] === target) { + (target as any)[DEPENDENCIES].push({ beanId, index }); + } else { + (target as any)[DEPENDENCIES] = [{ beanId, index }]; + (target as any)[TARGET] = target; + } +} + +export function getBeanDependecies( + target: Constructor, +): { id: BeanIdentifier; index: number }[] { + return (target as any)[DEPENDENCIES] || []; +} diff --git a/packages/shared/src/common/instantiation/decorators.ts b/packages/shared/src/common/instantiation/decorators.ts index 50a6d49e5..dae2142c2 100644 --- a/packages/shared/src/common/instantiation/decorators.ts +++ b/packages/shared/src/common/instantiation/decorators.ts @@ -1,34 +1,17 @@ -import { inject, injectable } from 'inversify'; -import { fluentProvide } from 'inversify-binding-decorators'; +import { type BeanIdentifier, type Constructor, mapDepsToBeanId } from './container'; -/** - * Identifies a service of type `T`. - */ -export interface ServiceIdentifier { - (...args: any[]): void; - type: T; -} +const idsMap = new Map>(); -export type Constructor = new (...args: any[]) => T; +export function createDecorator(beanId: string): BeanIdentifier { + if (idsMap.has(beanId)) { + return idsMap.get(beanId)!; + } -export function createDecorator(serviceId: string): ServiceIdentifier { - const id = ( - function (target: Constructor, targetKey: string, indexOrPropertyDescriptor: any): any { - return inject(serviceId)(target, targetKey, indexOrPropertyDescriptor); - } - ); - id.toString = () => serviceId; + const id = function (target: Constructor, _: string, indexOrPropertyDescriptor: any): any { + return mapDepsToBeanId(id, target, indexOrPropertyDescriptor); + }; + id.toString = () => beanId; + idsMap.set(beanId, id); return id; } - -export const Injectable = injectable; - -export function Provide(serviceId: ServiceIdentifier, isSingleTon?: boolean) { - const ret = fluentProvide(serviceId.toString()); - - if (isSingleTon) { - return ret.inSingletonScope().done(); - } - return ret.done(); -} diff --git a/packages/shared/src/common/instantiation/index.ts b/packages/shared/src/common/instantiation/index.ts index ed6f6797c..7afedffd4 100644 --- a/packages/shared/src/common/instantiation/index.ts +++ b/packages/shared/src/common/instantiation/index.ts @@ -1,2 +1,4 @@ export * from './instantiationService'; -export * from './decorators'; +export { createDecorator } from './decorators'; +export { CtorDescriptor, BeanContainer } from './container'; +export type { Constructor } from './container'; diff --git a/packages/shared/src/common/instantiation/instantiationService.ts b/packages/shared/src/common/instantiation/instantiationService.ts index a98a27f2a..4c4ea9de9 100644 --- a/packages/shared/src/common/instantiation/instantiationService.ts +++ b/packages/shared/src/common/instantiation/instantiationService.ts @@ -1,44 +1,41 @@ -import '@abraham/reflection'; -import { Container, interfaces, injectable } from 'inversify'; -import { buildProviderModule } from 'inversify-binding-decorators'; -import { ServiceIdentifier, Constructor, createDecorator } from './decorators'; +import { isDisposable } from '../disposable'; +import { Graph, CyclicDependencyError } from '../graph'; +import { + type BeanIdentifier, + BeanContainer, + type Constructor, + getBeanDependecies, + CtorDescriptor, +} from './container'; +import { createDecorator } from './decorators'; export interface InstanceAccessor { - get(id: ServiceIdentifier): T; + get(id: BeanIdentifier): T; } export interface IInstantiationService { - get(serviceIdentifier: ServiceIdentifier): T; + readonly container: BeanContainer; - bind(serviceIdentifier: ServiceIdentifier, constructor: Constructor): void; + createInstance(Ctor: T, ...args: any[]): InstanceType; - set(serviceIdentifier: ServiceIdentifier, instance: T): void; - - invokeFunction( - fn: (accessor: InstanceAccessor, ...args: TS) => R, - ...args: TS + invokeFunction( + fn: (accessor: InstanceAccessor, ...args: Args) => R, + ...args: Args ): R; - createInstance(App: T): InstanceType; + dispose(): void; } export const IInstantiationService = createDecorator('instantiationService'); export class InstantiationService implements IInstantiationService { - container: Container; + private _activeInstantiations = new Set>(); - constructor(options?: interfaces.ContainerOptions) { - this.container = new Container(options); - this.set(IInstantiationService, this); - this.container.load(buildProviderModule()); - } + private _isDisposed = false; + private readonly _beansToMaybeDispose = new Set(); - get(serviceIdentifier: ServiceIdentifier) { - return this.container.get(serviceIdentifier.toString()); - } - - set(serviceIdentifier: ServiceIdentifier, instance: T): void { - this.container.bind(serviceIdentifier).toConstantValue(instance); + constructor(public readonly container: BeanContainer = new BeanContainer()) { + this.container.set(IInstantiationService, this); } /** @@ -48,20 +45,166 @@ export class InstantiationService implements IInstantiationService { fn: (accessor: InstanceAccessor, ...args: TS) => R, ...args: TS ): R { + this._throwIfDisposed(); + const accessor: InstanceAccessor = { get: (id) => { - return this.get(id); + const result = this._getOrCreateInstance(id); + if (!result) { + throw new Error(`[invokeFunction] unknown service '${id}'`); + } + return result; }, }; return fn(accessor, ...args); } - bind(serviceIdentifier: ServiceIdentifier, constructor: Constructor) { - this.container.bind(serviceIdentifier).to(constructor); + /** + * 创建实例 + */ + createInstance(Ctor: T, ...args: any[]): InstanceType { + this._throwIfDisposed(); + + const beanDependencies = getBeanDependecies(Ctor).sort((a, b) => a.index - b.index); + const beanArgs = []; + + for (const dependency of beanDependencies) { + const instance = this._getOrCreateInstance(dependency.id); + if (!instance) { + throw new Error(`[createInstance] ${Ctor.name} depends on UNKNOWN bean ${dependency.id}.`); + } + + beanArgs.push(instance); + } + + // 检查传入参数的个数,进行参数微调 + const firstArgPos = beanDependencies.length > 0 ? beanDependencies[0].index : args.length; + if (args.length !== firstArgPos) { + const delta = firstArgPos - args.length; + if (delta > 0) { + args = args.concat(new Array(delta)); + } else { + args = args.slice(0, firstArgPos); + } + } + + return Reflect.construct>(Ctor, args.concat(beanArgs)); } - createInstance(App: T) { - injectable()(App); - return this.container.resolve>(App); + private _getOrCreateInstance(id: BeanIdentifier): T { + const thing = this.container.get(id); + if (thing instanceof CtorDescriptor) { + return this._safeCreateAndCacheInstance(id, thing); + } else { + return thing; + } + } + + private _safeCreateAndCacheInstance(id: BeanIdentifier, desc: CtorDescriptor): T { + if (this._activeInstantiations.has(id)) { + throw new Error(`[createInstance] illegal state - RECURSIVELY instantiating ${id}`); + } + this._activeInstantiations.add(id); + try { + return this._createAndCacheServiceInstance(id, desc); + } finally { + this._activeInstantiations.delete(id); + } + } + + private _createAndCacheServiceInstance(id: BeanIdentifier, desc: CtorDescriptor): T { + const graph = new Graph<{ id: BeanIdentifier; desc: CtorDescriptor }>((data) => + data.id.toString(), + ); + + let cycleCount = 0; + const stack = [{ id, desc }]; + const seen = new Set(); + + while (stack.length > 0) { + const item = stack.pop()!; + + if (seen.has(item.id.toString())) { + continue; + } + seen.add(item.id.toString()); + + graph.lookupOrInsertNode(item); + + // 一个较弱但有效的循环检查启发式方法 + if (cycleCount++ > 1000) { + throw new CyclicDependencyError(graph); + } + + // check all dependencies for existence and if they need to be created first + for (const dependency of getBeanDependecies(item.desc.ctor)) { + const instanceOrDesc = this.container.get(dependency.id); + if (!instanceOrDesc) { + throw new Error( + `[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`, + ); + } + + if (instanceOrDesc instanceof CtorDescriptor) { + const d = { + id: dependency.id, + desc: instanceOrDesc, + }; + graph.insertEdge(item, d); + stack.push(d); + } + } + + while (true) { + const roots = graph.roots(); + + // if there is no more roots but still + // nodes in the graph we have a cycle + if (roots.length === 0) { + if (!graph.isEmpty()) { + throw new CyclicDependencyError(graph); + } + break; + } + + for (const { data } of roots) { + // Repeat the check for this still being a service sync descriptor. That's because + // instantiating a dependency might have side-effect and recursively trigger instantiation + // so that some dependencies are now fullfilled already. + const instanceOrDesc = this.container.get(data.id); + if (instanceOrDesc instanceof CtorDescriptor) { + // create instance and overwrite the service collections + const instance = this.createInstance( + instanceOrDesc.ctor, + instanceOrDesc.staticArguments, + ); + this._beansToMaybeDispose.add(instance); + this.container.set(data.id, instance); + } + graph.removeNode(data); + } + } + } + + return this.container.get(id) as T; + } + + dispose(): void { + if (this._isDisposed) return; + + // dispose all services created by this service + for (const candidate of this._beansToMaybeDispose) { + if (isDisposable(candidate)) { + candidate.dispose(); + } + } + this._beansToMaybeDispose.clear(); + this._isDisposed = true; + } + + private _throwIfDisposed(): void { + if (this._isDisposed) { + throw new Error('InstantiationService has been disposed'); + } } } diff --git a/packages/shared/src/common/keyCodes.ts b/packages/shared/src/common/keyCodes.ts new file mode 100644 index 000000000..8436a2e6b --- /dev/null +++ b/packages/shared/src/common/keyCodes.ts @@ -0,0 +1,1302 @@ +/*--------------------------------------------------------------------------------------------- + * 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 + * But these are "more general", as they should work across browsers & OS`s. + */ +export const enum KeyCode { + DependsOnKbLayout = -1, + + /** + * Placed first to cover the 0 value of the enum. + */ + Unknown = 0, + + Backspace, + Tab, + Enter, + Shift, + Ctrl, + Alt, + PauseBreak, + CapsLock, + Escape, + Space, + PageUp, + PageDown, + End, + Home, + LeftArrow, + UpArrow, + RightArrow, + DownArrow, + Insert, + Delete, + + Digit0, + Digit1, + Digit2, + Digit3, + Digit4, + Digit5, + Digit6, + Digit7, + Digit8, + Digit9, + + 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, + + Meta, + ContextMenu, + + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + F21, + F22, + F23, + F24, + + NumLock, + ScrollLock, + + /** + * Used for miscellaneous characters; it can vary by keyboard. + * For the US standard keyboard, the ';:' key + */ + Semicolon, + /** + * For any country/region, the '+' key + * For the US standard keyboard, the '=+' key + */ + Equal, + /** + * For any country/region, the ',' key + * For the US standard keyboard, the ',<' key + */ + Comma, + /** + * For any country/region, the '-' key + * For the US standard keyboard, the '-_' key + */ + Minus, + /** + * For any country/region, the '.' key + * For the US standard keyboard, the '.>' key + */ + Period, + /** + * Used for miscellaneous characters; it can vary by keyboard. + * For the US standard keyboard, the '/?' key + */ + Slash, + /** + * Used for miscellaneous characters; it can vary by keyboard. + * For the US standard keyboard, the '`~' key + */ + Backquote, + /** + * Used for miscellaneous characters; it can vary by keyboard. + * For the US standard keyboard, the '[{' key + */ + BracketLeft, + /** + * Used for miscellaneous characters; it can vary by keyboard. + * For the US standard keyboard, the '\|' key + */ + Backslash, + /** + * Used for miscellaneous characters; it can vary by keyboard. + * For the US standard keyboard, the ']}' key + */ + BracketRight, + /** + * Used for miscellaneous characters; it can vary by keyboard. + * For the US standard keyboard, the ''"' key + */ + Quote, + /** + * Used for miscellaneous characters; it can vary by keyboard. + */ + OEM_8, + /** + * Either the angle bracket key or the backslash key on the RT 102-key keyboard. + */ + IntlBackslash, + + Numpad0, // VK_NUMPAD0, 0x60, Numeric keypad 0 key + Numpad1, // VK_NUMPAD1, 0x61, Numeric keypad 1 key + Numpad2, // VK_NUMPAD2, 0x62, Numeric keypad 2 key + Numpad3, // VK_NUMPAD3, 0x63, Numeric keypad 3 key + Numpad4, // VK_NUMPAD4, 0x64, Numeric keypad 4 key + Numpad5, // VK_NUMPAD5, 0x65, Numeric keypad 5 key + Numpad6, // VK_NUMPAD6, 0x66, Numeric keypad 6 key + Numpad7, // VK_NUMPAD7, 0x67, Numeric keypad 7 key + Numpad8, // VK_NUMPAD8, 0x68, Numeric keypad 8 key + Numpad9, // VK_NUMPAD9, 0x69, Numeric keypad 9 key + + NumpadMultiply, // VK_MULTIPLY, 0x6A, Multiply key + NumpadAdd, // VK_ADD, 0x6B, Add key + NUMPAD_SEPARATOR, // VK_SEPARATOR, 0x6C, Separator key + NumpadSubtract, // VK_SUBTRACT, 0x6D, Subtract key + NumpadDecimal, // VK_DECIMAL, 0x6E, Decimal key + NumpadDivide, // VK_DIVIDE, 0x6F, + + /** + * Cover all key codes when IME is processing input. + */ + KEY_IN_COMPOSITION, + + ABNT_C1, // Brazilian (ABNT) Keyboard + ABNT_C2, // Brazilian (ABNT) Keyboard + + AudioVolumeMute, + AudioVolumeUp, + AudioVolumeDown, + + BrowserSearch, + BrowserHome, + BrowserBack, + BrowserForward, + + MediaTrackNext, + MediaTrackPrevious, + MediaStop, + MediaPlayPause, + LaunchMediaPlayer, + LaunchMail, + LaunchApp2, + + /** + * VK_CLEAR, 0x0C, CLEAR key + */ + Clear, + + /** + * Placed last to cover the length of the enum. + * Please do not depend on this value! + */ + 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 namespace KeyCodeUtils { + export function toString(keyCode: KeyCode): string { + return uiMap.keyCodeToStr(keyCode); + } + export function fromString(key: string): KeyCode { + return uiMap.strToKeyCode(key); + } + + export function toUserSettingsUS(keyCode: KeyCode): string { + return userSettingsUSMap.keyCodeToStr(keyCode); + } + export function toUserSettingsGeneral(keyCode: KeyCode): string { + return userSettingsGeneralMap.keyCodeToStr(keyCode); + } + export function fromUserSettings(key: string): KeyCode { + return userSettingsUSMap.strToKeyCode(key) || userSettingsGeneralMap.strToKeyCode(key); + } + + export function toElectronAccelerator(keyCode: KeyCode): string | null { + if (keyCode >= KeyCode.Numpad0 && keyCode <= KeyCode.NumpadDivide) { + // [Electron Accelerators] Electron is able to parse numpad keys, but unfortunately it + // renders them just as regular keys in menus. For example, num0 is rendered as "0", + // numdiv is rendered as "/", numsub is rendered as "-". + // + // This can lead to incredible confusion, as it makes numpad based keybindings indistinguishable + // from keybindings based on regular keys. + // + // We therefore need to fall back to custom rendering for numpad keys. + return null; + } + + switch (keyCode) { + case KeyCode.UpArrow: + return 'Up'; + case KeyCode.DownArrow: + return 'Down'; + case KeyCode.LeftArrow: + return 'Left'; + case KeyCode.RightArrow: + return 'Right'; + } + + return uiMap.keyCodeToStr(keyCode); + } +} + +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/shared/src/common/platform.ts b/packages/shared/src/common/platform.ts index c71ca981f..420d47034 100644 --- a/packages/shared/src/common/platform.ts +++ b/packages/shared/src/common/platform.ts @@ -32,6 +32,13 @@ export function platformToString(platform: PlatformEnum): PlatformName { } } +export const enum OperatingSystem { + Windows = 1, + Macintosh = 2, + Linux = 3 +} +export const OS = (isMacintosh || isIOS ? OperatingSystem.Macintosh : (isWindows ? OperatingSystem.Windows : OperatingSystem.Linux)); + export let platform: PlatformEnum = PlatformEnum.Unknown; if (isMacintosh) { platform = PlatformEnum.Mac; diff --git a/packages/shared/src/utils/functional.ts b/packages/shared/src/utils/functional.ts new file mode 100644 index 000000000..d13827c37 --- /dev/null +++ b/packages/shared/src/utils/functional.ts @@ -0,0 +1,33 @@ +import { AnyFunction } from '../types'; + +/** + * Given a function, returns a function that is only calling that function once. + */ +export function createSingleCallFunction( + this: unknown, + fn: T, + fnDidRunCallback?: () => void, +): T { + const _this = this; + let didCall = false; + let result: unknown; + + return function (...args: any[]) { + if (didCall) { + return result; + } + + didCall = true; + if (fnDidRunCallback) { + try { + result = fn.apply(_this, args); + } finally { + fnDidRunCallback(); + } + } else { + result = fn.apply(_this, args); + } + + return result; + } as unknown as T; +} diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 361899979..659c24545 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -8,3 +8,4 @@ export * from './types'; export * from './async'; export * from './node'; export * from './resource'; +export * from './functional'; diff --git a/packages/shared/src/utils/iterable.ts b/packages/shared/src/utils/iterable.ts index 0667ba909..313603691 100644 --- a/packages/shared/src/utils/iterable.ts +++ b/packages/shared/src/utils/iterable.ts @@ -5,3 +5,7 @@ export function first(iterable: Iterable): T | undefined { export function isEmpty(iterable: Iterable | undefined | null): boolean { return !iterable || iterable[Symbol.iterator]().next().done === true; } + +export function is(thing: any): thing is Iterable { + return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function'; +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 15e38d973..178f4d698 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -4,5 +4,4 @@ "allowJs": true, "outDir": "dist" }, - "include": ["src"] }