feat: add keybind services

This commit is contained in:
1ncounter 2024-08-21 16:09:42 +08:00
parent 9e08c2a224
commit 46139fc155
49 changed files with 2838 additions and 1650 deletions

View File

@ -0,0 +1,118 @@
import { type IDisposable, isWebKit } from '@alilc/lowcode-shared';
export const DOMEventType = {
// Mouse
CLICK: 'click',
AUXCLICK: 'auxclick',
DBLCLICK: 'dblclick',
MOUSE_UP: 'mouseup',
MOUSE_DOWN: 'mousedown',
MOUSE_OVER: 'mouseover',
MOUSE_MOVE: 'mousemove',
MOUSE_OUT: 'mouseout',
MOUSE_ENTER: 'mouseenter',
MOUSE_LEAVE: 'mouseleave',
MOUSE_WHEEL: 'wheel',
POINTER_UP: 'pointerup',
POINTER_DOWN: 'pointerdown',
POINTER_MOVE: 'pointermove',
POINTER_LEAVE: 'pointerleave',
CONTEXT_MENU: 'contextmenu',
WHEEL: 'wheel',
// Keyboard
KEY_DOWN: 'keydown',
KEY_PRESS: 'keypress',
KEY_UP: 'keyup',
// HTML Document
LOAD: 'load',
BEFORE_UNLOAD: 'beforeunload',
UNLOAD: 'unload',
PAGE_SHOW: 'pageshow',
PAGE_HIDE: 'pagehide',
PASTE: 'paste',
ABORT: 'abort',
ERROR: 'error',
RESIZE: 'resize',
SCROLL: 'scroll',
FULLSCREEN_CHANGE: 'fullscreenchange',
WK_FULLSCREEN_CHANGE: 'webkitfullscreenchange',
// Form
SELECT: 'select',
CHANGE: 'change',
SUBMIT: 'submit',
RESET: 'reset',
FOCUS: 'focus',
FOCUS_IN: 'focusin',
FOCUS_OUT: 'focusout',
BLUR: 'blur',
INPUT: 'input',
// Local Storage
STORAGE: 'storage',
// Drag
DRAG_START: 'dragstart',
DRAG: 'drag',
DRAG_ENTER: 'dragenter',
DRAG_LEAVE: 'dragleave',
DRAG_OVER: 'dragover',
DROP: 'drop',
DRAG_END: 'dragend',
// Animation
ANIMATION_START: isWebKit ? 'webkitAnimationStart' : 'animationstart',
ANIMATION_END: isWebKit ? 'webkitAnimationEnd' : 'animationend',
ANIMATION_ITERATION: isWebKit ? 'webkitAnimationIteration' : 'animationiteration',
} as const;
class DomListener implements IDisposable {
private _handler: (e: any) => void;
private _node: EventTarget;
private readonly _type: string;
private readonly _options: boolean | AddEventListenerOptions;
constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) {
this._node = node;
this._type = type;
this._handler = handler;
this._options = options || false;
this._node.addEventListener(this._type, this._handler, this._options);
}
dispose(): void {
if (!this._handler) {
// Already disposed
return;
}
this._node.removeEventListener(this._type, this._handler, this._options);
// Prevent leakers from holding on to the dom or handler func
this._node = null!;
this._handler = null!;
}
}
export function addDisposableListener<K extends keyof GlobalEventHandlersEventMap>(
node: EventTarget,
type: K,
handler: (event: GlobalEventHandlersEventMap[K]) => void,
useCapture?: boolean,
): IDisposable;
export function addDisposableListener(
node: EventTarget,
type: string,
handler: (event: any) => void,
useCapture?: boolean,
): IDisposable;
export function addDisposableListener(
node: EventTarget,
type: string,
handler: (event: any) => void,
options: AddEventListenerOptions,
): IDisposable;
export function addDisposableListener(
node: EventTarget,
type: string,
handler: (event: any) => void,
useCaptureOrOptions?: boolean | AddEventListenerOptions,
): IDisposable {
return new DomListener(node, type, handler, useCaptureOrOptions);
}

View File

@ -3,8 +3,10 @@ import * as Schemas from './schemas';
export { Schemas };
export * from './charCode';
export * from './dom';
export * from './glob';
export * from './keyCodes';
export * from './keyCodeUtils';
export * from './path';
export * from './strings';
export * from './ternarySearchTree';

View File

@ -0,0 +1,242 @@
import { KeyCode } from './keyCodes';
class KeyCodeStrMap {
public _keyCodeToStr: string[];
public _strToKeyCode: { [str: string]: KeyCode };
constructor() {
this._keyCodeToStr = [];
this._strToKeyCode = Object.create(null);
}
define(keyCode: KeyCode, str: string): void {
this._keyCodeToStr[keyCode] = str;
this._strToKeyCode[str.toLowerCase()] = keyCode;
}
keyCodeToStr(keyCode: KeyCode): string {
return this._keyCodeToStr[keyCode];
}
strToKeyCode(str: string): KeyCode {
return this._strToKeyCode[str.toLowerCase()] || KeyCode.Unknown;
}
}
const uiMap = new KeyCodeStrMap();
const userSettingsUSMap = new KeyCodeStrMap();
const userSettingsGeneralMap = new KeyCodeStrMap();
export const KeyCodeUtils = {
toString(keyCode: KeyCode): string {
return uiMap.keyCodeToStr(keyCode);
},
fromString(key: string): KeyCode {
return uiMap.strToKeyCode(key);
},
toUserSettingsUS(keyCode: KeyCode): string {
return userSettingsUSMap.keyCodeToStr(keyCode);
},
toUserSettingsGeneral(keyCode: KeyCode): string {
return userSettingsGeneralMap.keyCodeToStr(keyCode);
},
fromUserSettings(key: string): KeyCode {
return userSettingsUSMap.strToKeyCode(key) || userSettingsGeneralMap.strToKeyCode(key);
},
};
export const EVENT_CODE_TO_KEY_CODE_MAP: { [keyCode: string]: KeyCode } = {};
(function () {
// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
// see https://www.toptal.com/developers/keycode/table
const empty = '';
type IMappingEntry = [string, KeyCode, string, string, string];
const mappings: IMappingEntry[] = [
// scanCode, keyCode, keyCodeStr, usUserSettingsLabel, generalUserSettingsLabel
['Unidentified', KeyCode.Unknown, 'unknown', empty, empty],
['KeyA', KeyCode.KeyA, 'A', empty, empty],
['KeyB', KeyCode.KeyB, 'B', empty, empty],
['KeyC', KeyCode.KeyC, 'C', empty, empty],
['KeyD', KeyCode.KeyD, 'D', empty, empty],
['KeyE', KeyCode.KeyE, 'E', empty, empty],
['KeyF', KeyCode.KeyF, 'F', empty, empty],
['KeyG', KeyCode.KeyG, 'G', empty, empty],
['KeyH', KeyCode.KeyH, 'H', empty, empty],
['KeyI', KeyCode.KeyI, 'I', empty, empty],
['KeyJ', KeyCode.KeyJ, 'J', empty, empty],
['KeyK', KeyCode.KeyK, 'K', empty, empty],
['KeyL', KeyCode.KeyL, 'L', empty, empty],
['KeyM', KeyCode.KeyM, 'M', empty, empty],
['KeyN', KeyCode.KeyN, 'N', empty, empty],
['KeyO', KeyCode.KeyO, 'O', empty, empty],
['KeyP', KeyCode.KeyP, 'P', empty, empty],
['KeyQ', KeyCode.KeyQ, 'Q', empty, empty],
['KeyR', KeyCode.KeyR, 'R', empty, empty],
['KeyS', KeyCode.KeyS, 'S', empty, empty],
['KeyT', KeyCode.KeyT, 'T', empty, empty],
['KeyU', KeyCode.KeyU, 'U', empty, empty],
['KeyV', KeyCode.KeyV, 'V', empty, empty],
['KeyW', KeyCode.KeyW, 'W', empty, empty],
['KeyX', KeyCode.KeyX, 'X', empty, empty],
['KeyY', KeyCode.KeyY, 'Y', empty, empty],
['KeyZ', KeyCode.KeyZ, 'Z', empty, empty],
['Digit1', KeyCode.Digit1, '1', empty, empty],
['Digit2', KeyCode.Digit2, '2', empty, empty],
['Digit3', KeyCode.Digit3, '3', empty, empty],
['Digit4', KeyCode.Digit4, '4', empty, empty],
['Digit5', KeyCode.Digit5, '5', empty, empty],
['Digit6', KeyCode.Digit6, '6', empty, empty],
['Digit7', KeyCode.Digit7, '7', empty, empty],
['Digit8', KeyCode.Digit8, '8', empty, empty],
['Digit9', KeyCode.Digit9, '9', empty, empty],
['Digit0', KeyCode.Digit0, '0', empty, empty],
['Enter', KeyCode.Enter, 'Enter', empty, empty],
['Escape', KeyCode.Escape, 'Escape', empty, empty],
['Backspace', KeyCode.Backspace, 'Backspace', empty, empty],
['Tab', KeyCode.Tab, 'Tab', empty, empty],
['Space', KeyCode.Space, 'Space', empty, empty],
['Minus', KeyCode.Minus, '-', '-', 'OEM_MINUS'],
['Equal', KeyCode.Equal, '=', '=', 'OEM_PLUS'],
['BracketLeft', KeyCode.BracketLeft, '[', '[', 'OEM_4'],
['BracketRight', KeyCode.BracketRight, ']', ']', 'OEM_6'],
['Backslash', KeyCode.Backslash, '\\', '\\', 'OEM_5'],
['Semicolon', KeyCode.Semicolon, ';', ';', 'OEM_1'],
['Quote', KeyCode.Quote, `'`, `'`, 'OEM_7'],
['Backquote', KeyCode.Backquote, '`', '`', 'OEM_3'],
['Comma', KeyCode.Comma, ',', ',', 'OEM_COMMA'],
['Period', KeyCode.Period, '.', '.', 'OEM_PERIOD'],
['Slash', KeyCode.Slash, '/', '/', 'OEM_2'],
['CapsLock', KeyCode.CapsLock, 'CapsLock', empty, empty],
['F1', KeyCode.F1, 'F1', empty, empty],
['F2', KeyCode.F2, 'F2', empty, empty],
['F3', KeyCode.F3, 'F3', empty, empty],
['F4', KeyCode.F4, 'F4', empty, empty],
['F5', KeyCode.F5, 'F5', empty, empty],
['F6', KeyCode.F6, 'F6', empty, empty],
['F7', KeyCode.F7, 'F7', empty, empty],
['F8', KeyCode.F8, 'F8', empty, empty],
['F9', KeyCode.F9, 'F9', empty, empty],
['F10', KeyCode.F10, 'F10', empty, empty],
['F11', KeyCode.F11, 'F11', empty, empty],
['F12', KeyCode.F12, 'F12', empty, empty],
['PrintScreen', KeyCode.Unknown, empty, empty, empty],
['ScrollLock', KeyCode.ScrollLock, 'ScrollLock', empty, empty],
['Pause', KeyCode.PauseBreak, 'PauseBreak', empty, empty],
['Insert', KeyCode.Insert, 'Insert', empty, empty],
['Home', KeyCode.Home, 'Home', empty, empty],
['PageUp', KeyCode.PageUp, 'PageUp', empty, empty],
['Delete', KeyCode.Delete, 'Delete', empty, empty],
['End', KeyCode.End, 'End', empty, empty],
['PageDown', KeyCode.PageDown, 'PageDown', empty, empty],
['ArrowRight', KeyCode.RightArrow, 'RightArrow', 'Right', empty],
['ArrowLeft', KeyCode.LeftArrow, 'LeftArrow', 'Left', empty],
['ArrowDown', KeyCode.DownArrow, 'DownArrow', 'Down', empty],
['ArrowUp', KeyCode.UpArrow, 'UpArrow', 'Up', empty],
['NumLock', KeyCode.NumLock, 'NumLock', empty, empty],
['NumpadDivide', KeyCode.NumpadDivide, 'NumPad_Divide', empty, empty],
['NumpadMultiply', KeyCode.NumpadMultiply, 'NumPad_Multiply', empty, empty],
['NumpadSubtract', KeyCode.NumpadSubtract, 'NumPad_Subtract', empty, empty],
['NumpadAdd', KeyCode.NumpadAdd, 'NumPad_Add', empty, empty],
['NumpadEnter', KeyCode.Enter, empty, empty, empty],
['Numpad1', KeyCode.Numpad1, 'NumPad1', empty, empty],
['Numpad2', KeyCode.Numpad2, 'NumPad2', empty, empty],
['Numpad3', KeyCode.Numpad3, 'NumPad3', empty, empty],
['Numpad4', KeyCode.Numpad4, 'NumPad4', empty, empty],
['Numpad5', KeyCode.Numpad5, 'NumPad5', empty, empty],
['Numpad6', KeyCode.Numpad6, 'NumPad6', empty, empty],
['Numpad7', KeyCode.Numpad7, 'NumPad7', empty, empty],
['Numpad8', KeyCode.Numpad8, 'NumPad8', empty, empty],
['Numpad9', KeyCode.Numpad9, 'NumPad9', empty, empty],
['Numpad0', KeyCode.Numpad0, 'NumPad0', empty, empty],
['NumpadDecimal', KeyCode.NumpadDecimal, 'NumPad_Decimal', empty, empty],
['IntlBackslash', KeyCode.IntlBackslash, 'OEM_102', empty, empty],
['ContextMenu', KeyCode.ContextMenu, 'ContextMenu', empty, empty],
['Power', KeyCode.Unknown, empty, empty, empty],
['NumpadEqual', KeyCode.Unknown, empty, empty, empty],
['F13', KeyCode.F13, 'F13', empty, empty],
['F14', KeyCode.F14, 'F14', empty, empty],
['F15', KeyCode.F15, 'F15', empty, empty],
['F16', KeyCode.F16, 'F16', empty, empty],
['F17', KeyCode.F17, 'F17', empty, empty],
['F18', KeyCode.F18, 'F18', empty, empty],
['F19', KeyCode.F19, 'F19', empty, empty],
['F20', KeyCode.F20, 'F20', empty, empty],
['F21', KeyCode.F21, 'F21', empty, empty],
['F22', KeyCode.F22, 'F22', empty, empty],
['F23', KeyCode.F23, 'F23', empty, empty],
['F24', KeyCode.F24, 'F24', empty, empty],
['AudioVolumeMute', KeyCode.AudioVolumeMute, 'AudioVolumeMute', empty, empty],
['AudioVolumeUp', KeyCode.AudioVolumeUp, 'AudioVolumeUp', empty, empty],
['AudioVolumeDown', KeyCode.AudioVolumeDown, 'AudioVolumeDown', empty, empty],
['NumpadComma', KeyCode.NUMPAD_SEPARATOR, 'NumPad_Separator', empty, empty],
['IntlRo', KeyCode.ABNT_C1, 'ABNT_C1', empty, empty],
['NumpadClear', KeyCode.Clear, 'Clear', empty, empty],
[empty, KeyCode.Ctrl, 'Ctrl', empty, empty],
[empty, KeyCode.Shift, 'Shift', empty, empty],
[empty, KeyCode.Alt, 'Alt', empty, empty],
[empty, KeyCode.Meta, 'Meta', empty, empty],
['ControlLeft', KeyCode.Ctrl, empty, empty, empty],
['ShiftLeft', KeyCode.Shift, empty, empty, empty],
['AltLeft', KeyCode.Alt, empty, empty, empty],
['MetaLeft', KeyCode.Meta, empty, empty, empty],
['ControlRight', KeyCode.Ctrl, empty, empty, empty],
['ShiftRight', KeyCode.Shift, empty, empty, empty],
['AltRight', KeyCode.Alt, empty, empty, empty],
['MetaRight', KeyCode.Meta, empty, empty, empty],
['MediaTrackNext', KeyCode.MediaTrackNext, 'MediaTrackNext', empty, empty],
['MediaTrackPrevious', KeyCode.MediaTrackPrevious, 'MediaTrackPrevious', empty, empty],
['MediaStop', KeyCode.MediaStop, 'MediaStop', empty, empty],
['MediaPlayPause', KeyCode.MediaPlayPause, 'MediaPlayPause', empty, empty],
['MediaSelect', KeyCode.LaunchMediaPlayer, 'LaunchMediaPlayer', empty, empty],
['LaunchMail', KeyCode.LaunchMail, 'LaunchMail', empty, empty],
['LaunchApp2', KeyCode.LaunchApp2, 'LaunchApp2', empty, empty],
['LaunchScreenSaver', KeyCode.Unknown, empty, empty, empty],
['BrowserSearch', KeyCode.BrowserSearch, 'BrowserSearch', empty, empty],
['BrowserHome', KeyCode.BrowserHome, 'BrowserHome', empty, empty],
['BrowserBack', KeyCode.BrowserBack, 'BrowserBack', empty, empty],
['BrowserForward', KeyCode.BrowserForward, 'BrowserForward', empty, empty],
// See https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
// If an Input Method Editor is processing key input and the event is keydown, return 229.
[empty, KeyCode.KEY_IN_COMPOSITION, 'KeyInComposition', empty, empty],
[empty, KeyCode.ABNT_C2, 'ABNT_C2', empty, empty],
[empty, KeyCode.OEM_8, 'OEM_8', empty, empty],
];
const seenKeyCode: boolean[] = [];
for (const mapping of mappings) {
const [scanCode, keyCode, keyCodeStr, usUserSettingsLabel, generalUserSettingsLabel] = mapping;
if (!seenKeyCode[keyCode]) {
seenKeyCode[keyCode] = true;
if (!keyCodeStr) {
throw new Error(`String representation missing for key code ${keyCode} around scan code ${scanCode}`);
}
uiMap.define(keyCode, keyCodeStr);
userSettingsUSMap.define(keyCode, usUserSettingsLabel || keyCodeStr);
userSettingsGeneralMap.define(keyCode, generalUserSettingsLabel || usUserSettingsLabel || keyCodeStr);
}
if (scanCode) {
EVENT_CODE_TO_KEY_CODE_MAP[scanCode] = keyCode;
}
}
console.log(
'%c [ IMMUTABLE_KEY_CODE_TO_CODE ]-500',
'font-size:13px; background:pink; color:#bf2c9f;',
uiMap,
userSettingsUSMap,
userSettingsGeneralMap,
EVENT_CODE_TO_KEY_CODE_MAP,
);
})();
export function KeyChord(firstPart: number, secondPart: number): number {
const chordPart = ((secondPart & 0x0000ffff) << 16) >>> 0;
return (firstPart | chordPart) >>> 0;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,8 @@
import {
Events,
type StringDictionary,
type JSONSchemaType,
jsonTypes,
IJSONSchema,
types,
Disposable,
} from '@alilc/lowcode-shared';
import { Events, type StringDictionary, jsonTypes, types, Disposable } from '@alilc/lowcode-shared';
import { isUndefined, isObject } from 'lodash-es';
import { Extensions, Registry } from '../extension/registry';
import { OVERRIDE_PROPERTY_REGEX, overrideIdentifiersFromKey } from './configuration';
import { type IJSONSchema, type JSONSchemaType } from '../schema';
export interface IConfigurationRegistry {
/**
@ -20,10 +13,7 @@ export interface IConfigurationRegistry {
/**
* Register multiple configurations to the registry.
*/
registerConfigurations(
configurations: IConfigurationNode[],
validate?: boolean,
): ReadonlySet<string>;
registerConfigurations(configurations: IConfigurationNode[], validate?: boolean): ReadonlySet<string>;
/**
* Deregister multiple configurations from the registry.
@ -101,7 +91,6 @@ export interface IConfigurationPropertySchema extends IJSONSchema {
*/
export interface IExtensionInfo {
id: string;
displayName?: string;
version?: string;
}
@ -167,10 +156,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat
this.registerConfigurations([configuration], validate);
}
registerConfigurations(
configurations: IConfigurationNode[],
validate: boolean = true,
): ReadonlySet<string> {
registerConfigurations(configurations: IConfigurationNode[], validate: boolean = true): ReadonlySet<string> {
const properties = new Set<string>();
this.doRegisterConfigurations(configurations, validate, properties);
@ -179,18 +165,9 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat
return properties;
}
private doRegisterConfigurations(
configurations: IConfigurationNode[],
validate: boolean,
bucket: Set<string>,
): void {
private doRegisterConfigurations(configurations: IConfigurationNode[], validate: boolean, bucket: Set<string>): void {
configurations.forEach((configuration) => {
this.validateAndRegisterProperties(
configuration,
validate,
configuration.extensionInfo,
bucket,
);
this.validateAndRegisterProperties(configuration, validate, configuration.extensionInfo, bucket);
this.registerJSONConfiguration(configuration);
});
@ -250,10 +227,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat
return null;
}
private updatePropertyDefaultValue(
key: string,
property: IRegisteredConfigurationPropertySchema,
): void {
private updatePropertyDefaultValue(key: string, property: IRegisteredConfigurationPropertySchema): void {
let defaultValue = undefined;
let defaultSource = undefined;
@ -286,10 +260,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat
this._onDidUpdateConfiguration.notify({ properties });
}
private doDeregisterConfigurations(
configurations: IConfigurationNode[],
bucket: Set<string>,
): void {
private doDeregisterConfigurations(configurations: IConfigurationNode[], bucket: Set<string>): void {
const deregisterConfiguration = (configuration: IConfigurationNode) => {
if (configuration.properties) {
for (const key of Object.keys(configuration.properties)) {
@ -313,10 +284,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat
this._onDidUpdateConfiguration.notify({ properties, defaultsOverrides: true });
}
private doRegisterDefaultConfigurations(
configurationDefaults: IConfigurationDefaults[],
bucket: Set<string>,
) {
private doRegisterDefaultConfigurations(configurationDefaults: IConfigurationDefaults[], bucket: Set<string>) {
this.registeredConfigurationDefaults.push(...configurationDefaults);
const overrideIdentifiers: string[] = [];
@ -327,9 +295,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat
const configurationDefaultOverridesForKey =
this.configurationDefaultsOverrides.get(key) ??
this.configurationDefaultsOverrides
.set(key, { configurationDefaultOverrides: [] })
.get(key)!;
this.configurationDefaultsOverrides.set(key, { configurationDefaultOverrides: [] }).get(key)!;
const value = overrides[key];
configurationDefaultOverridesForKey.configurationDefaultOverrides.push({ value, source });
@ -346,8 +312,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat
continue;
}
configurationDefaultOverridesForKey.configurationDefaultOverrideValue =
newDefaultOverride;
configurationDefaultOverridesForKey.configurationDefaultOverrideValue = newDefaultOverride;
this.updateDefaultOverrideProperty(key, newDefaultOverride, source);
overrideIdentifiers.push(...overrideIdentifiersFromKey(key));
}
@ -364,8 +329,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat
continue;
}
configurationDefaultOverridesForKey.configurationDefaultOverrideValue =
newDefaultOverride;
configurationDefaultOverridesForKey.configurationDefaultOverrideValue = newDefaultOverride;
const property = this.configurationProperties[key];
if (property) {
this.updatePropertyDefaultValue(key, property);
@ -453,8 +417,7 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat
const isObjectSetting =
isObject(value) &&
((property !== undefined && property.type === 'object') ||
(property === undefined &&
(isUndefined(existingDefaultValue) || isObject(existingDefaultValue))));
(property === undefined && (isUndefined(existingDefaultValue) || isObject(existingDefaultValue))));
// If the default value is an object, merge the objects and store the source of each keys
if (isObjectSetting) {
@ -522,21 +485,16 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat
// configuration override defaults - merges defaults
for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) {
configurationDefaultOverrideValue =
this.mergeDefaultConfigurationsForOverrideIdentifier(
key,
configurationDefaultOverride.value,
configurationDefaultOverride.source,
configurationDefaultOverrideValue,
);
configurationDefaultOverrideValue = this.mergeDefaultConfigurationsForOverrideIdentifier(
key,
configurationDefaultOverride.value,
configurationDefaultOverride.source,
configurationDefaultOverrideValue,
);
}
if (
configurationDefaultOverrideValue &&
!types.isEmptyObject(configurationDefaultOverrideValue.value)
) {
configurationDefaultOverridesForKey.configurationDefaultOverrideValue =
configurationDefaultOverrideValue;
if (configurationDefaultOverrideValue && !types.isEmptyObject(configurationDefaultOverrideValue.value)) {
configurationDefaultOverridesForKey.configurationDefaultOverrideValue = configurationDefaultOverrideValue;
this.updateDefaultOverrideProperty(key, configurationDefaultOverrideValue, source);
} else {
this.configurationDefaultsOverrides.delete(key);
@ -547,17 +505,15 @@ export class ConfigurationRegistryImpl extends Disposable implements IConfigurat
// configuration override defaults - merges defaults
for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) {
configurationDefaultOverrideValue =
this.mergeDefaultConfigurationsForConfigurationProperty(
key,
configurationDefaultOverride.value,
configurationDefaultOverride.source,
configurationDefaultOverrideValue,
);
configurationDefaultOverrideValue = this.mergeDefaultConfigurationsForConfigurationProperty(
key,
configurationDefaultOverride.value,
configurationDefaultOverride.source,
configurationDefaultOverrideValue,
);
}
configurationDefaultOverridesForKey.configurationDefaultOverrideValue =
configurationDefaultOverrideValue;
configurationDefaultOverridesForKey.configurationDefaultOverrideValue = configurationDefaultOverrideValue;
const property = this.configurationProperties[key];
if (property) {

View File

@ -0,0 +1,9 @@
export interface IContentEditor {
load(content: string): Promise<void>;
export(): Promise<string>;
close(): void;
send(channel: string, ...args: any[]): void;
}

View File

@ -0,0 +1,48 @@
import { toDisposable, type IDisposable } from '@alilc/lowcode-shared';
import { IContentEditor } from './contentEditor';
import { Registry, Extensions } from '../extension/registry';
export interface IContentEditorRegistry {
registerContentEditor(contentType: string, windowContent: IContentEditor, options?: IRegisterOptions): IDisposable;
getContentEditor(contentType: string): IContentEditor | undefined;
getContentTypeByExt(ext: string): string | undefined;
}
export interface IRegisterOptions {
ext?: string;
}
class ContentEditorRegistryImpl implements IContentEditorRegistry {
private readonly _contentEditors = new Map<string, IContentEditor>();
private readonly _mapExtToType = new Map<string, string>();
registerContentEditor(
contentType: string,
contentEditor: IContentEditor,
options: IRegisterOptions = {},
): IDisposable {
const { ext = contentType } = options;
this._contentEditors.set(contentType, contentEditor);
this._mapExtToType.set(ext, contentType);
return toDisposable(() => {
this._contentEditors.delete(contentType);
this._mapExtToType.delete(contentType);
});
}
getContentEditor(contentType: string): IContentEditor | undefined {
return this._contentEditors.get(contentType);
}
getContentTypeByExt(ext: string): string | undefined {
return this._mapExtToType.get(ext);
}
}
export const ContentEditorRegistry = new ContentEditorRegistryImpl();
Registry.add(Extensions.ContentEditor, ContentEditorRegistry);

View File

@ -1,14 +1,13 @@
import { StringDictionary } from '@alilc/lowcode-shared';
import { IConfigurationNode } from '../configuration';
export type ExtensionInitializer = <Context = any>(ctx: Context) => IExtensionInstance;
export type ExtensionStarter = <Context = any>(ctx: Context) => IExtensionInstance;
/**
*
*/
export interface IFunctionExtension extends ExtensionInitializer {
id: string;
displayName?: string;
export interface IFunctionExtension extends ExtensionStarter {
id?: string;
version: string;
metadata?: IExtensionMetadata;
}

View File

@ -1,59 +1,59 @@
import { ConfigurationRegistry, type IConfigurationNode } from '../configuration';
import { type ExtensionInitializer, type IExtensionInstance } from './extension';
import { invariant } from '@alilc/lowcode-shared';
import { type ExtensionStarter, type IExtensionInstance } from './extension';
export type ExtensionExportsAccessor = {
[key: string]: any;
};
export class ExtensionHost {
private isInited = false;
private _isInited = false;
private instance: IExtensionInstance;
private _instance: IExtensionInstance;
private configurationProperties: ReadonlySet<string>;
private _exports: ExtensionExportsAccessor | undefined;
get exports(): ExtensionExportsAccessor | undefined {
if (!this._isInited) return;
if (!this._exports) {
const exports = this._instance.exports?.();
if (!exports) return;
this._exports = new Proxy(Object.create(null), {
get(target, prop, receiver) {
if (Reflect.has(exports, prop)) {
return exports?.[prop as string];
}
return Reflect.get(target, prop, receiver);
},
});
}
return this._exports;
}
constructor(
public name: string,
initializer: ExtensionInitializer,
preferenceConfigurations: IConfigurationNode[],
public id: string,
starter: ExtensionStarter,
) {
this.configurationProperties =
ConfigurationRegistry.registerConfigurations(preferenceConfigurations);
// context will be provide in
this._instance = starter({});
}
this.instance = initializer({});
dispose(): void {
this.destroy();
}
async init(): Promise<void> {
if (this.isInited) return;
if (this._isInited) return;
await this.instance.init();
this.isInited = true;
await this._instance.init();
this._isInited = true;
}
async destroy(): Promise<void> {
if (!this.isInited) return;
if (!this._isInited) return;
await this.instance.destroy();
this.isInited = false;
}
toProxy(): ExtensionExportsAccessor | undefined {
invariant(this.isInited, 'Could not call toProxy before init');
const exports = this.instance.exports?.();
if (!exports) return;
return new Proxy(Object.create(null), {
get(target, prop, receiver) {
if (Reflect.has(exports, prop)) {
return exports?.[prop as string];
}
return Reflect.get(target, prop, receiver);
},
});
await this._instance.destroy();
this._isInited = false;
}
}

View File

@ -1,92 +0,0 @@
import { type Reference } from '@alilc/lowcode-shared';
import { type IFunctionExtension } from './extension';
import { type IConfigurationNode } from '../configuration';
import { ExtensionHost } from './extensionHost';
export interface IExtensionGallery {
name: string;
version: string;
reference: Reference | undefined;
dependencies: string[] | undefined;
engineVerison: string | undefined;
preferenceConfigurations: IConfigurationNode[] | undefined;
}
export interface IExtensionRegisterOptions {
/**
* Will enable plugin registered with auto-initialization immediately
* other than plugin-manager init all plugins at certain time.
* It is helpful when plugin register is later than plugin-manager initialization.
*/
autoInit?: boolean;
/**
* allow overriding existing plugin with same name when override === true
*/
override?: boolean;
}
export class ExtensionManagement {
private extensionGalleryMap: Map<string, IExtensionGallery> = new Map();
private extensionHosts: Map<string, ExtensionHost> = new Map();
constructor() {}
async register(
extension: IFunctionExtension,
{ autoInit = false, override = false }: IExtensionRegisterOptions = {},
): Promise<void> {
if (!this.validateExtension(extension, override)) return;
const metadata = extension.metadata ?? {};
const host = new ExtensionHost(
extension.name,
extension,
metadata.preferenceConfigurations ?? [],
);
if (autoInit) {
await host.init();
}
this.extensionHosts.set(extension.name, host);
const gallery: IExtensionGallery = {
name: extension.name,
version: extension.version,
reference: undefined,
dependencies: metadata.dependencies,
engineVerison: metadata.engineVerison,
preferenceConfigurations: metadata.preferenceConfigurations,
};
this.extensionGalleryMap.set(gallery.name, gallery);
}
private validateExtension(extension: IFunctionExtension, override: boolean): boolean {
if (!override && this.has(extension.name)) return false;
return true;
}
async deregister(name: string): Promise<void> {
if (this.has(name)) {
const host = this.extensionHosts.get(name)!;
await host.destroy();
this.extensionGalleryMap.delete(name);
this.extensionHosts.delete(name);
}
}
has(name: string): boolean {
return this.extensionGalleryMap.has(name);
}
getExtensionGallery(name: string): IExtensionGallery | undefined {
return this.extensionGalleryMap.get(name);
}
getExtensionHost(name: string): ExtensionHost | undefined {
return this.extensionHosts.get(name);
}
}

View File

@ -0,0 +1,160 @@
import { CyclicDependencyError, Disposable, Graph, type Reference } from '@alilc/lowcode-shared';
import { type IFunctionExtension } from './extension';
import { type IConfigurationNode, type IConfigurationRegistry } from '../configuration';
import { ExtensionHost } from './extensionHost';
import { Registry, Extensions } from './registry';
export interface IExtensionGallery {
id: string;
name: string;
version: string;
reference: Reference | undefined;
dependencies: string[] | undefined;
engineVerison: string | undefined;
preferenceConfigurations: IConfigurationNode[] | undefined;
registerOptions: IExtensionRegisterOptions;
}
export interface IExtensionRegisterOptions {
/**
* Will enable plugin registered with auto-initialization immediately
* other than plugin-manager init all plugins at certain time.
* It is helpful when plugin register is later than plugin-manager initialization.
*/
autoInit?: boolean;
/**
* allow overriding existing plugin with same name when override === true
*/
override?: boolean;
}
export class ExtensionManager extends Disposable {
private _extensionGalleryMap: Map<string, IExtensionGallery> = new Map();
private _extensionHosts: Map<string, ExtensionHost> = new Map();
private _extensionStore: Map<string, IFunctionExtension> = new Map();
private _extensionDependencyGraph = new Graph<string>((name) => name);
constructor() {
super();
}
async register(extension: IFunctionExtension, options: IExtensionRegisterOptions = {}): Promise<void> {
extension.id = extension.id ?? extension.name;
if (!this._validateExtension(extension, options.override)) return;
this._extensionStore.set(extension.id!, extension);
const metadata = extension.metadata ?? {};
const gallery: IExtensionGallery = {
id: extension.id!,
name: extension.name,
version: extension.version,
reference: undefined,
dependencies: metadata.dependencies,
engineVerison: metadata.engineVerison,
preferenceConfigurations: metadata.preferenceConfigurations?.map((config) => {
return {
...config,
extensionInfo: {
id: extension.id!,
version: extension.version,
},
};
}),
registerOptions: options,
};
this._extensionGalleryMap.set(gallery.id, gallery);
await this._installExtension(extension);
}
private _validateExtension(extension: IFunctionExtension, override: boolean = false): boolean {
if (!override && this.has(extension.id!)) return false;
return true;
}
private async _installExtension(extension: IFunctionExtension): Promise<void> {
const { dependencies = [] } = extension.metadata ?? {};
this._extensionDependencyGraph.lookupOrInsertNode(extension.id!);
if (dependencies.length > 0) {
for (const dep of dependencies) {
this._extensionDependencyGraph.insertEdge(extension.id!, dep);
}
}
while (true) {
const roots = this._extensionDependencyGraph.roots();
if (roots.length === 0 || roots.every((node) => !this.isExtensionActivated(node.data))) {
if (this._extensionDependencyGraph.isEmpty()) {
throw new CyclicDependencyError(this._extensionDependencyGraph);
}
break;
}
for (const { data } of roots) {
const extensionFunction = this.getExtension(data);
const gallery = this.getExtensionGallery(data);
if (extensionFunction) {
const host = new ExtensionHost(data, extensionFunction);
if (gallery!.preferenceConfigurations) {
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfigurations(
gallery!.preferenceConfigurations,
);
}
this._addDispose(host);
this._extensionHosts.set(extension.name, host);
this._extensionDependencyGraph.removeNode(extensionFunction.id!);
try {
if (gallery!.registerOptions.autoInit) {
await host.init();
}
} catch (e) {
console.log(`The extension [${data}] init failed: `, e);
}
}
}
}
}
async deregister(id: string): Promise<void> {
if (this.has(id)) {
const host = this._extensionHosts.get(id)!;
await host.destroy();
this._extensionGalleryMap.delete(id);
this._extensionHosts.delete(id);
}
}
has(id: string): boolean {
return this._extensionGalleryMap.has(id);
}
getExtension(id: string): IFunctionExtension | undefined {
return this._extensionStore.get(id);
}
getExtensionGallery(id: string): IExtensionGallery | undefined {
return this._extensionGalleryMap.get(id);
}
getExtensionHost(id: string): ExtensionHost | undefined {
return this._extensionHosts.get(id);
}
isExtensionActivated(id: string): boolean {
return this._extensionHosts.has(id);
}
}

View File

@ -1,5 +1,5 @@
import { createDecorator } from '@alilc/lowcode-shared';
import { ExtensionManagement, type IExtensionRegisterOptions } from './extensionManagement';
import { ExtensionManager, type IExtensionRegisterOptions } from './extensionManager';
import { type IFunctionExtension } from './extension';
import { ExtensionHost } from './extensionHost';
@ -11,26 +11,32 @@ export interface IExtensionService {
has(name: string): boolean;
getExtensionHost(name: string): ExtensionHost | undefined;
dispose(): void;
}
export const IExtensionService = createDecorator<IExtensionService>('extensionService');
export class ExtensionService implements IExtensionService {
private extensionManagement = new ExtensionManagement();
private _manager = new ExtensionManager();
dispose(): void {
this._manager.dispose();
}
register(extension: IFunctionExtension, options?: IExtensionRegisterOptions): Promise<void> {
return this.extensionManagement.register(extension, options);
return this._manager.register(extension, options);
}
deregister(name: string): Promise<void> {
return this.extensionManagement.deregister(name);
return this._manager.deregister(name);
}
has(name: string): boolean {
return this.extensionManagement.has(name);
return this._manager.has(name);
}
getExtensionHost(name: string): ExtensionHost | undefined {
return this.extensionManagement.getExtensionHost(name);
return this._manager.getExtensionHost(name);
}
}

View File

@ -0,0 +1,2 @@
export * from './extension';
export * from './extensionService';

View File

@ -39,8 +39,10 @@ class RegistryImpl implements IRegistry {
export const Registry: IRegistry = new RegistryImpl();
export const Extensions = {
JSONContribution: 'base.contributions.jsonContribution',
Configuration: 'base.contributions.configuration',
Command: 'base.contributions.command',
Keybinding: 'base.contributions.keybinding',
Widget: 'base.contributions.widget',
ContentEditor: 'base.contributions.contentEditor',
};

View File

@ -199,20 +199,20 @@ export enum FsContants {
}
export interface IFileSystemProvider {
watch(resource: URI, opts: IWatchOptions): IFileSystemWatcher;
watch(resource: URI, opts?: IWatchOptions): IFileSystemWatcher;
chmod(resource: URI, mode: number): Promise<void>;
access(resource: URI, mode?: number): Promise<void>;
stat(resource: URI): Promise<IFileStat>;
mkdir(resource: URI, opts: IFileWriteOptions): Promise<void>;
mkdir(resource: URI, opts?: IFileWriteOptions): Promise<void>;
readdir(resource: URI): Promise<[string, FileType][]>;
delete(resource: URI, opts: IFileDeleteOptions): Promise<void>;
delete(resource: URI, opts?: IFileDeleteOptions): Promise<void>;
rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void>;
rename(from: URI, to: URI, opts?: IFileOverwriteOptions): Promise<void>;
// copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void>;
readFile(resource: URI): Promise<string>;
writeFile(resource: URI, content: string, opts: IFileWriteOptions): Promise<void>;
writeFile(resource: URI, content: string, opts?: IFileWriteOptions): Promise<void>;
}
export enum FileSystemErrorCode {

View File

@ -104,17 +104,17 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider {
};
}
watch(resource: URI, opts: IWatchOptions): IFileSystemWatcher {
watch(resource: URI, opts?: IWatchOptions): IFileSystemWatcher {
return { resource, opts } as any as IFileSystemWatcher;
}
async mkdir(resource: URI, opts: IFileWriteOptions): Promise<void> {
async mkdir(resource: URI, opts?: IFileWriteOptions): Promise<void> {
const base = basename(resource.path);
const dir = dirname(resource.path);
const parent = this._lookupAsDirectory(dir, true);
if (parent) {
if (!opts.overwrite && parent.entries.has(base)) {
if (!opts?.overwrite && parent.entries.has(base)) {
throw FileSystemError.create('directory exists', FileSystemErrorCode.FileExists);
}
@ -126,7 +126,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider {
{ resource, type: FileChangeType.ADDED },
);
} else {
if (!opts.recursive) {
if (!opts?.recursive) {
throw FileSystemError.create('parent directory not found', FileSystemErrorCode.FileNotFound);
}
@ -154,14 +154,14 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider {
return file.data;
}
async writeFile(resource: URI, content: string, opts: IFileWriteOptions): Promise<void> {
async writeFile(resource: URI, content: string, opts?: IFileWriteOptions): Promise<void> {
const base = basename(resource.path);
const dir = dirname(resource.path);
const dirUri = resource.with({ path: dir });
let parent = this._lookupAsDirectory(dir, true);
if (!parent) {
if (!opts.recursive) {
if (!opts?.recursive) {
throw FileSystemError.create('file not found', FileSystemErrorCode.FileNotFound);
}
parent = await this._mkdirRecursive(dirUri);
@ -174,7 +174,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider {
if (entry && entry.permission < FilePermission.Writable) {
throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable);
}
if (entry && !opts.overwrite) {
if (entry && !opts?.overwrite) {
throw FileSystemError.create('file exists already', FileSystemErrorCode.FileExists);
}
@ -189,7 +189,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider {
this._fireSoon({ resource, type: FileChangeType.UPDATED });
}
async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {
async delete(resource: URI, opts?: IFileDeleteOptions): Promise<void> {
const dir = dirname(resource.path);
const base = basename(resource.path);
const parent = this._lookupAsDirectory(dir, false);
@ -206,7 +206,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider {
}
if (entry instanceof Directory) {
if (opts.recursive) {
if (opts?.recursive) {
parent.entries.delete(base);
parent.mtime = Date.now();
} else {
@ -224,7 +224,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider {
}
}
async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {
async rename(from: URI, to: URI, opts?: IFileOverwriteOptions): Promise<void> {
if (from.path === to.path) return;
const entry = this._lookup(from.path, false);
@ -232,7 +232,7 @@ export class InMemoryFileSystemProvider implements IFileSystemProvider {
if (entry.permission < FilePermission.Writable) {
throw FileSystemError.create('Permission denied', FileSystemErrorCode.FileNotWritable);
}
if (!opts.overwrite) {
if (!opts?.overwrite) {
throw FileSystemError.create('file exists already', FileSystemErrorCode.FileExists);
}

View File

@ -4,6 +4,7 @@ export * from './resource';
export * from './command';
export * from './workspace';
export * from './common';
export * from './keybinding';
// test
export * from './main';

View File

@ -0,0 +1,2 @@
export * from './jsonSchema';
export * from './jsonSchemaRegistry';

View File

@ -0,0 +1,272 @@
export type JSONSchemaType = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object';
/**
* fork from vscode
*/
export interface IJSONSchema {
id?: string;
$id?: string;
$schema?: string;
type?: JSONSchemaType | JSONSchemaType[];
title?: string;
default?: any;
definitions?: IJSONSchemaMap;
description?: string;
properties?: IJSONSchemaMap;
patternProperties?: IJSONSchemaMap;
additionalProperties?: boolean | IJSONSchema;
minProperties?: number;
maxProperties?: number;
dependencies?: IJSONSchemaMap | { [prop: string]: string[] };
items?: IJSONSchema | IJSONSchema[];
minItems?: number;
maxItems?: number;
uniqueItems?: boolean;
additionalItems?: boolean | IJSONSchema;
pattern?: string;
minLength?: number;
maxLength?: number;
minimum?: number;
maximum?: number;
exclusiveMinimum?: boolean | number;
exclusiveMaximum?: boolean | number;
multipleOf?: number;
required?: string[];
$ref?: string;
anyOf?: IJSONSchema[];
allOf?: IJSONSchema[];
oneOf?: IJSONSchema[];
not?: IJSONSchema;
enum?: any[];
format?: string;
// schema draft 06
const?: any;
contains?: IJSONSchema;
propertyNames?: IJSONSchema;
examples?: any[];
// schema draft 07
$comment?: string;
if?: IJSONSchema;
then?: IJSONSchema;
else?: IJSONSchema;
// schema 2019-09
unevaluatedProperties?: boolean | IJSONSchema;
unevaluatedItems?: boolean | IJSONSchema;
minContains?: number;
maxContains?: number;
deprecated?: boolean;
dependentRequired?: { [prop: string]: string[] };
dependentSchemas?: IJSONSchemaMap;
$defs?: { [name: string]: IJSONSchema };
$anchor?: string;
$recursiveRef?: string;
$recursiveAnchor?: string;
$vocabulary?: any;
// schema 2020-12
prefixItems?: IJSONSchema[];
$dynamicRef?: string;
$dynamicAnchor?: string;
// internal extensions
errorMessage?: string;
}
export interface IJSONSchemaMap {
[name: string]: IJSONSchema;
}
export interface IJSONSchemaSnippet {
label?: string;
description?: string;
body?: any; // a object that will be JSON stringified
bodyText?: string; // an already stringified JSON object that can contain new lines (\n) and tabs (\t)
}
/**
* Converts a basic JSON schema to a TypeScript type.
*
* TODO: only supports basic schemas. Doesn't support all JSON schema features.
*/
export type SchemaToType<T> = T extends { type: 'string' }
? string
: T extends { type: 'number' }
? number
: T extends { type: 'boolean' }
? boolean
: T extends { type: 'null' }
? null
: T extends { type: 'object'; properties: infer P }
? { [K in keyof P]: SchemaToType<P[K]> }
: T extends { type: 'array'; items: infer I }
? Array<SchemaToType<I>>
: never;
interface Equals {
schemas: IJSONSchema[];
id?: string;
}
export function getCompressedContent(schema: IJSONSchema): string {
let hasDups = false;
// visit all schema nodes and collect the ones that are equal
const equalsByString = new Map<string, Equals>();
const nodeToEquals = new Map<IJSONSchema, Equals>();
const visitSchemas = (next: IJSONSchema) => {
if (schema === next) {
return true;
}
const val = JSON.stringify(next);
if (val.length < 30) {
// the $ref takes around 25 chars, so we don't save anything
return true;
}
const eq = equalsByString.get(val);
if (!eq) {
const newEq = { schemas: [next] };
equalsByString.set(val, newEq);
nodeToEquals.set(next, newEq);
return true;
}
eq.schemas.push(next);
nodeToEquals.set(next, eq);
hasDups = true;
return false;
};
traverseNodes(schema, visitSchemas);
equalsByString.clear();
if (!hasDups) {
return JSON.stringify(schema);
}
let defNodeName = '$defs';
while (Reflect.has(schema, defNodeName)) {
defNodeName += '_';
}
// used to collect all schemas that are later put in `$defs`. The index in the array is the id of the schema.
const definitions: IJSONSchema[] = [];
function stringify(root: IJSONSchema): string {
return JSON.stringify(root, (_key: string, value: any) => {
if (value !== root) {
const eq = nodeToEquals.get(value);
if (eq && eq.schemas.length > 1) {
if (!eq.id) {
eq.id = `_${definitions.length}`;
definitions.push(eq.schemas[0]);
}
return { $ref: `#/${defNodeName}/${eq.id}` };
}
}
return value;
});
}
// stringify the schema and replace duplicate subtrees with $ref
// this will add new items to the definitions array
const str = stringify(schema);
// now stringify the definitions. Each invication of stringify cann add new items to the definitions array, so the length can grow while we iterate
const defStrings: string[] = [];
for (let i = 0; i < definitions.length; i++) {
defStrings.push(`"_${i}":${stringify(definitions[i])}`);
}
if (defStrings.length) {
return `${str.substring(0, str.length - 1)},"${defNodeName}":{${defStrings.join(',')}}}`;
}
return str;
}
type IJSONSchemaRef = IJSONSchema | boolean;
function isObject(thing: any): thing is object {
return typeof thing === 'object' && thing !== null;
}
/*
* Traverse a JSON schema and visit each schema node
*/
function traverseNodes(root: IJSONSchema, visit: (schema: IJSONSchema) => boolean) {
if (!root || typeof root !== 'object') {
return;
}
const collectEntries = (...entries: (IJSONSchemaRef | undefined)[]) => {
for (const entry of entries) {
if (isObject(entry)) {
toWalk.push(entry);
}
}
};
const collectMapEntries = (...maps: (IJSONSchemaMap | undefined)[]) => {
for (const map of maps) {
if (isObject(map)) {
for (const key in map) {
const entry = map[key];
if (isObject(entry)) {
toWalk.push(entry);
}
}
}
}
};
const collectArrayEntries = (...arrays: (IJSONSchemaRef[] | undefined)[]) => {
for (const array of arrays) {
if (Array.isArray(array)) {
for (const entry of array) {
if (isObject(entry)) {
toWalk.push(entry);
}
}
}
}
};
const collectEntryOrArrayEntries = (items: IJSONSchemaRef[] | IJSONSchemaRef | undefined) => {
if (Array.isArray(items)) {
for (const entry of items) {
if (isObject(entry)) {
toWalk.push(entry);
}
}
} else if (isObject(items)) {
toWalk.push(items);
}
};
const toWalk: IJSONSchema[] = [root];
let next = toWalk.pop();
while (next) {
const visitChildern = visit(next);
if (visitChildern) {
collectEntries(
next.additionalItems,
next.additionalProperties,
next.not,
next.contains,
next.propertyNames,
next.if,
next.then,
next.else,
next.unevaluatedItems,
next.unevaluatedProperties,
);
collectMapEntries(
next.definitions,
next.$defs,
next.properties,
next.patternProperties,
<IJSONSchemaMap>next.dependencies,
next.dependentSchemas,
);
collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.prefixItems);
collectEntryOrArrayEntries(next.items);
}
next = toWalk.pop();
}
}

View File

@ -0,0 +1,78 @@
import { Events } from '@alilc/lowcode-shared';
import { Registry, Extensions } from '../extension/registry';
import { getCompressedContent, type IJSONSchema } from './jsonSchema';
export interface ISchemaContributions {
schemas: { [id: string]: IJSONSchema };
}
export interface IJSONContributionRegistry {
readonly onDidChangeSchema: Events.Event<string>;
/**
* Register a schema to the registry.
*/
registerSchema(uri: string, unresolvedSchemaContent: IJSONSchema): void;
/**
* Notifies all listeners that the content of the given schema has changed.
* @param uri The id of the schema
*/
notifySchemaChanged(uri: string): void;
/**
* Get all schemas
*/
getSchemaContributions(): ISchemaContributions;
/**
* Gets the (compressed) content of the schema with the given schema ID (if any)
* @param uri The id of the schema
*/
getSchemaContent(uri: string): string | undefined;
/**
* Returns true if there's a schema that matches the given schema ID
* @param uri The id of the schema
*/
hasSchemaContent(uri: string): boolean;
}
class JSONContributionRegistryImpl implements IJSONContributionRegistry {
private _onDidChangeSchema = new Events.Emitter<string>();
onDidChangeSchema = this._onDidChangeSchema.event;
private schemasById: { [id: string]: IJSONSchema };
constructor() {
this.schemasById = {};
}
registerSchema(uri: string, unresolvedSchemaContent: IJSONSchema): void {
this.schemasById[uri] = unresolvedSchemaContent;
this._onDidChangeSchema.notify(uri);
}
notifySchemaChanged(uri: string): void {
this._onDidChangeSchema.notify(uri);
}
getSchemaContributions(): ISchemaContributions {
return {
schemas: this.schemasById,
};
}
getSchemaContent(uri: string): string | undefined {
const schema = this.schemasById[uri];
return schema ? getCompressedContent(schema) : undefined;
}
hasSchemaContent(uri: string): boolean {
return !!this.schemasById[uri];
}
}
export const JSONContributionRegistry = new JSONContributionRegistryImpl();
Registry.add(Extensions.JSONContribution, JSONContributionRegistry);

View File

@ -0,0 +1,2 @@
export * from './keybinding';
export * from './keybindingService';

View File

@ -0,0 +1,63 @@
import { Events, createDecorator } from '@alilc/lowcode-shared';
import { Keybinding, ResolvedKeybinding } from './keybindings';
import { ResolvedKeybindingItem } from './keybindingResolver';
import { type IKeyboardEvent } from './keybindingEvent';
export interface IUserFriendlyKeybinding {
key: string;
command: string;
args?: any;
}
export interface IKeybindingService {
readonly inChordMode: boolean;
onDidUpdateKeybindings: Events.Event<void>;
/**
* 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<void> | undefined;
dispatchByUserSettingsLabel(userSettingsLabel: string, target: any): void;
/**
* Look up keybindings for a command.
* Use `lookupKeybinding` if you are interested in the preferred keybinding.
*/
lookupKeybindings(commandId: string): ResolvedKeybinding[];
/**
* Look up the preferred (last defined) keybinding for a command.
* @returns The preferred keybinding or null if the command is not bound.
*/
// lookupKeybinding(commandId: string, context?: any): ResolvedKeybinding | undefined;
getDefaultKeybindings(): readonly ResolvedKeybindingItem[];
getKeybindings(): readonly ResolvedKeybindingItem[];
}
export const IKeybindingService = createDecorator<IKeybindingService>('keybindingService');

View File

@ -0,0 +1,145 @@
import { isMacintosh, isLinux, isFirefox } from '@alilc/lowcode-shared';
import { EVENT_CODE_TO_KEY_CODE_MAP, KeyCode } from '../common';
import { KeyCodeChord, BinaryKeybindingsMask } from './keybindings';
const ctrlKeyMod = isMacintosh ? BinaryKeybindingsMask.WinCtrl : BinaryKeybindingsMask.CtrlCmd;
const altKeyMod = BinaryKeybindingsMask.Alt;
const shiftKeyMod = BinaryKeybindingsMask.Shift;
const metaKeyMod = isMacintosh ? BinaryKeybindingsMask.CtrlCmd : BinaryKeybindingsMask.WinCtrl;
export interface IKeyboardEvent {
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly keyCode: KeyCode;
readonly code: string;
preventDefault(): void;
stopPropagation(): void;
toKeyCodeChord(): KeyCodeChord;
equals(keybinding: number): boolean;
}
export class StandardKeyboardEvent implements IKeyboardEvent {
public readonly navtiveEvent: KeyboardEvent;
public readonly target: HTMLElement;
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly keyCode: KeyCode;
public readonly code: string;
private _asKeybinding: number;
private _asKeyCodeChord: KeyCodeChord;
constructor(source: KeyboardEvent) {
const e = source;
this.navtiveEvent = e;
this.target = <HTMLElement>e.target;
this.ctrlKey = e.ctrlKey;
this.shiftKey = e.shiftKey;
this.altKey = e.altKey;
this.metaKey = e.metaKey;
this.keyCode = extractKeyCode(e);
this.code = e.code;
this.ctrlKey = this.ctrlKey || this.keyCode === KeyCode.Ctrl;
this.altKey = this.altKey || this.keyCode === KeyCode.Alt;
this.shiftKey = this.shiftKey || this.keyCode === KeyCode.Shift;
this.metaKey = this.metaKey || this.keyCode === KeyCode.Meta;
this._asKeybinding = this._computeKeybinding();
this._asKeyCodeChord = this._computeKeyCodeChord();
}
preventDefault(): void {
if (this.navtiveEvent && this.navtiveEvent.preventDefault) {
this.navtiveEvent.preventDefault();
}
}
stopPropagation(): void {
if (this.navtiveEvent && this.navtiveEvent.stopPropagation) {
this.navtiveEvent.stopPropagation();
}
}
toKeyCodeChord(): KeyCodeChord {
return this._asKeyCodeChord;
}
equals(other: number): boolean {
return this._asKeybinding === other;
}
private _computeKeybinding(): number {
let key = KeyCode.Unknown;
if (
this.keyCode !== KeyCode.Ctrl &&
this.keyCode !== KeyCode.Shift &&
this.keyCode !== KeyCode.Alt &&
this.keyCode !== KeyCode.Meta
) {
key = this.keyCode;
}
let result = 0;
if (this.ctrlKey) {
result |= ctrlKeyMod;
}
if (this.altKey) {
result |= altKeyMod;
}
if (this.shiftKey) {
result |= shiftKeyMod;
}
if (this.metaKey) {
result |= metaKeyMod;
}
result |= key;
return result;
}
private _computeKeyCodeChord(): KeyCodeChord {
let key = KeyCode.Unknown;
if (
this.keyCode !== KeyCode.Ctrl &&
this.keyCode !== KeyCode.Shift &&
this.keyCode !== KeyCode.Alt &&
this.keyCode !== KeyCode.Meta
) {
key = this.keyCode;
}
return new KeyCodeChord(this.ctrlKey, this.shiftKey, this.altKey, this.metaKey, key);
}
}
function extractKeyCode(e: KeyboardEvent): KeyCode {
const code = e.code;
// browser quirks
if (isFirefox) {
switch (code) {
case 'Backquote':
if (isLinux) {
return KeyCode.IntlBackslash;
}
break;
case 'OSLeft':
if (isMacintosh) {
return KeyCode.Meta;
}
break;
}
}
// cross browser keycodes:
return EVENT_CODE_TO_KEY_CODE_MAP[code] || KeyCode.Unknown;
}

View File

@ -1,5 +1,5 @@
import { KeyCodeUtils, ScanCodeUtils } from '@alilc/lowcode-shared';
import { KeyCodeChord, ScanCodeChord, Keybinding, Chord } from './keybindings';
import { KeyCodeUtils } from '../common';
import { KeyCodeChord, Keybinding } from './keybindings';
export class KeybindingParser {
private static _readModifiers(input: string) {
@ -67,17 +67,8 @@ export class KeybindingParser {
};
}
private static parseChord(input: string): [Chord, string] {
private static parseChord(input: string): [KeyCodeChord, string] {
const mods = this._readModifiers(input);
const scanCodeMatch = mods.key.match(/^\[([^\]]+)\]$/);
if (scanCodeMatch) {
const strScanCode = scanCodeMatch[1];
const scanCode = ScanCodeUtils.lowerCaseToEnum(strScanCode);
return [
new ScanCodeChord(mods.ctrl, mods.shift, mods.alt, mods.meta, scanCode),
mods.remains,
];
}
const keyCode = KeyCodeUtils.fromUserSettings(mods.key);
return [new KeyCodeChord(mods.ctrl, mods.shift, mods.alt, mods.meta, keyCode), mods.remains];
}
@ -87,8 +78,8 @@ export class KeybindingParser {
return null;
}
const chords: Chord[] = [];
let chord: Chord;
const chords: KeyCodeChord[] = [];
let chord: KeyCodeChord;
while (input.length > 0) {
[chord, input] = this.parseChord(input);

View File

@ -1,18 +1,16 @@
import { OperatingSystem, OS } from '@alilc/lowcode-shared';
import {
combinedDisposable,
DisposableStore,
IDisposable,
LinkedList,
OperatingSystem,
OS,
toDisposable,
} from '@alilc/lowcode-shared';
import { ICommandHandler, ICommandMetadata, CommandsRegistry } from '../command';
import { Keybinding } from './keybindings';
import { decodeKeybinding, Keybinding } from './keybindings';
import { Extensions, Registry } from '../extension/registry';
export interface IKeybindingItem {
keybinding: Keybinding | null;
command: string | null;
commandArgs?: any;
weight1: number;
weight2: number;
extensionId: string | null;
isBuiltinExtension: boolean;
}
export interface IKeybindings {
primary?: number;
secondary?: number[];
@ -46,8 +44,16 @@ export interface IExtensionKeybindingRule {
id: string;
args?: any;
weight: number;
extensionId?: string;
isBuiltinExtension?: boolean;
extensionId: string;
}
export interface IKeybindingItem {
keybinding: Keybinding | null;
command: string | null;
commandArgs?: any;
weight1: number;
weight2: number;
extensionId: string | null;
}
export const enum KeybindingWeight {
@ -59,9 +65,12 @@ export const enum KeybindingWeight {
}
export interface IKeybindingsRegistry {
registerKeybindingRule(rule: IKeybindingRule): void;
registerKeybindingRule(rule: IKeybindingRule): IDisposable;
setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void;
registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void;
registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): IDisposable;
getDefaultKeybindings(): IKeybindingItem[];
}
@ -90,18 +99,110 @@ export class KeybindingsRegistryImpl implements IKeybindingsRegistry {
return kb;
}
registerKeybindingRule(rule: IKeybindingRule): void {
private _coreKeybindings: LinkedList<IKeybindingItem>;
private _extensionKeybindings: IKeybindingItem[];
private _cachedMergedKeybindings: IKeybindingItem[] | null;
constructor() {
this._coreKeybindings = new LinkedList();
this._extensionKeybindings = [];
this._cachedMergedKeybindings = null;
}
registerKeybindingRule(rule: IKeybindingRule): IDisposable {
const actualKb = KeybindingsRegistryImpl.bindToCurrentPlatform(rule);
const result = new DisposableStore();
if (actualKb && actualKb.primary) {
const kk = decodeKeybinding(actualKb.primary, OS);
if (kk) {
result.add(this._registerDefaultKeybinding(kk, rule.id, rule.args, rule.weight, 0));
}
}
if (actualKb && Array.isArray(actualKb.secondary)) {
for (let i = 0, len = actualKb.secondary.length; i < len; i++) {
const k = actualKb.secondary[i];
const kk = decodeKeybinding(k, OS);
if (kk) {
result.add(this._registerDefaultKeybinding(kk, rule.id, rule.args, rule.weight, -i - 1));
}
}
}
return result;
}
registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): void {
this.registerKeybindingRule(desc);
CommandsRegistry.registerCommand(desc);
private _registerDefaultKeybinding(
keybinding: Keybinding,
commandId: string,
commandArgs: any,
weight1: number,
weight2: number,
): IDisposable {
const remove = this._coreKeybindings.push({
keybinding: keybinding,
command: commandId,
commandArgs: commandArgs,
weight1: weight1,
weight2: weight2,
extensionId: null,
});
this._cachedMergedKeybindings = null;
return toDisposable(() => {
remove();
this._cachedMergedKeybindings = null;
});
}
setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void {}
registerCommandAndKeybindingRule(desc: ICommandAndKeybindingRule): IDisposable {
return combinedDisposable(this.registerKeybindingRule(desc), CommandsRegistry.registerCommand(desc));
}
getDefaultKeybindings(): IKeybindingItem[] {}
setExtensionKeybindings(rules: IExtensionKeybindingRule[]): void {
const result: IKeybindingItem[] = [];
for (const rule of rules) {
if (rule.keybinding) {
result.push({
keybinding: rule.keybinding,
command: rule.id,
commandArgs: rule.args,
weight1: rule.weight,
weight2: 0,
extensionId: rule.extensionId || null,
});
}
}
this._extensionKeybindings = result;
this._cachedMergedKeybindings = null;
}
getDefaultKeybindings(): IKeybindingItem[] {
if (!this._cachedMergedKeybindings) {
this._cachedMergedKeybindings = Array.from(this._coreKeybindings).concat(this._extensionKeybindings);
this._cachedMergedKeybindings.sort(sorter);
}
return this._cachedMergedKeybindings.slice(0);
}
}
function sorter(a: IKeybindingItem, b: IKeybindingItem): number {
if (a.weight1 !== b.weight1) {
return a.weight1 - b.weight1;
}
if (a.command && b.command) {
if (a.command < b.command) {
return -1;
}
if (a.command > b.command) {
return 1;
}
}
return a.weight2 - b.weight2;
}
export const KeybindingsRegistry = new KeybindingsRegistryImpl();

View File

@ -1,3 +1,48 @@
import { CharCode } from '../common';
import { ResolvedKeybinding } from './keybindings';
export class ResolvedKeybindingItem {
public readonly resolvedKeybinding: ResolvedKeybinding | undefined;
public readonly chords: string[];
public readonly bubble: boolean;
public readonly command: string | null;
public readonly commandArgs: any;
public readonly isDefault: boolean;
public readonly extensionId: string | null;
constructor(
resolvedKeybinding: ResolvedKeybinding | undefined,
command: string | null,
commandArgs: any,
isDefault: boolean,
extensionId: string | null,
) {
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.isDefault = isDefault;
this.extensionId = extensionId;
}
}
export function toEmptyArrayIfContainsNull<T>(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;
}
export const enum ResultKind {
/** No keybinding found this sequence of chords */
NoMatchingKb,
@ -22,58 +67,304 @@ function KbFound(commandId: string | null, commandArgs: any, isBubble: boolean):
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;
/**
* Stores mappings from keybindings to commands and from commands to keybindings.
* Given a sequence of chords, `resolve`s which keybinding it matches
*/
export class KeybindingResolver {
private readonly _log: (str: string) => void;
private readonly _defaultKeybindings: ResolvedKeybindingItem[];
private readonly _keybindings: ResolvedKeybindingItem[];
private readonly _defaultBoundCommands: Map</* commandId */ string, boolean>;
private readonly _map: Map</* 1st chord's keypress */ string, ResolvedKeybindingItem[]>;
private readonly _lookupMap: Map</* commandId */ string, ResolvedKeybindingItem[]>;
constructor(
resolvedKeybinding: ResolvedKeybinding | undefined,
command: string | null,
commandArgs: any,
when: ContextKeyExpression | undefined,
isDefault: boolean,
extensionId: string | null,
isBuiltinExtension: boolean,
/** built-in and extension-provided keybindings */
defaultKeybindings: ResolvedKeybindingItem[],
/** user's keybindings */
overrides: ResolvedKeybindingItem[],
log: (str: string) => void,
) {
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._log = log;
this._defaultKeybindings = defaultKeybindings;
this._defaultBoundCommands = new Map<string, boolean>();
for (const defaultKeybinding of defaultKeybindings) {
const command = defaultKeybinding.command;
if (command && command.charAt(0) !== '-') {
this._defaultBoundCommands.set(command, true);
}
}
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;
this._map = new Map<string, ResolvedKeybindingItem[]>();
this._lookupMap = new Map<string, ResolvedKeybindingItem[]>();
this._keybindings = KeybindingResolver.handleRemovals(
([] as ResolvedKeybindingItem[]).concat(defaultKeybindings).concat(overrides),
);
for (let i = 0, len = this._keybindings.length; i < len; i++) {
const k = this._keybindings[i];
if (k.chords.length === 0) {
// unbound
continue;
}
this._addKeyPress(k.chords[0], k);
}
}
private static _isTargetedForRemoval(defaultKb: ResolvedKeybindingItem, keypress: string[] | null): boolean {
if (keypress) {
for (let i = 0; i < keypress.length; i++) {
if (keypress[i] !== defaultKb.chords[i]) {
return false;
}
}
}
return true;
}
/**
* Looks for rules containing "-commandId" and removes them.
*/
public static handleRemovals(rules: ResolvedKeybindingItem[]): ResolvedKeybindingItem[] {
// Do a first pass and construct a hash-map for removals
const removals = new Map</* commandId */ string, ResolvedKeybindingItem[]>();
for (let i = 0, len = rules.length; i < len; i++) {
const rule = rules[i];
if (rule.command && rule.command.charAt(0) === '-') {
const command = rule.command.substring(1);
if (!removals.has(command)) {
removals.set(command, [rule]);
} else {
removals.get(command)!.push(rule);
}
}
}
if (removals.size === 0) {
// There are no removals
return rules;
}
// Do a second pass and keep only non-removed keybindings
const result: ResolvedKeybindingItem[] = [];
for (let i = 0, len = rules.length; i < len; i++) {
const rule = rules[i];
if (!rule.command || rule.command.length === 0) {
result.push(rule);
continue;
}
if (rule.command.charAt(0) === '-') {
continue;
}
const commandRemovals = removals.get(rule.command);
if (!commandRemovals || !rule.isDefault) {
result.push(rule);
continue;
}
let isRemoved = false;
for (const commandRemoval of commandRemovals) {
if (this._isTargetedForRemoval(rule, commandRemoval.chords)) {
isRemoved = true;
break;
}
}
if (!isRemoved) {
result.push(rule);
continue;
}
}
return result;
}
private _addKeyPress(keypress: string, item: ResolvedKeybindingItem): void {
const conflicts = this._map.get(keypress);
if (typeof conflicts === 'undefined') {
// There is no conflict so far
this._map.set(keypress, [item]);
this._addToLookupMap(item);
return;
}
for (let i = conflicts.length - 1; i >= 0; i--) {
const conflict = conflicts[i];
if (conflict.command === item.command) {
continue;
}
// Test if the shorter keybinding is a prefix of the longer one.
// If the shorter keybinding is a prefix, it effectively will shadow the longer one and is considered a conflict.
let isShorterKbPrefix = true;
for (let i = 1; i < conflict.chords.length && i < item.chords.length; i++) {
if (conflict.chords[i] !== item.chords[i]) {
// The ith step does not conflict
isShorterKbPrefix = false;
break;
}
}
if (!isShorterKbPrefix) {
continue;
}
}
conflicts.push(item);
this._addToLookupMap(item);
}
private _addToLookupMap(item: ResolvedKeybindingItem): void {
if (!item.command) {
return;
}
let arr = this._lookupMap.get(item.command);
if (typeof arr === 'undefined') {
arr = [item];
this._lookupMap.set(item.command, arr);
} else {
arr.push(item);
}
}
private _removeFromLookupMap(item: ResolvedKeybindingItem): void {
if (!item.command) {
return;
}
const arr = this._lookupMap.get(item.command);
if (typeof arr === 'undefined') {
return;
}
for (let i = 0, len = arr.length; i < len; i++) {
if (arr[i] === item) {
arr.splice(i, 1);
return;
}
}
}
public getDefaultBoundCommands(): Map<string, boolean> {
return this._defaultBoundCommands;
}
public getDefaultKeybindings(): readonly ResolvedKeybindingItem[] {
return this._defaultKeybindings;
}
public getKeybindings(): readonly ResolvedKeybindingItem[] {
return this._keybindings;
}
public lookupKeybindings(commandId: string): ResolvedKeybindingItem[] {
const items = this._lookupMap.get(commandId);
if (typeof items === 'undefined' || items.length === 0) {
return [];
}
// Reverse to get the most specific item first
const result: ResolvedKeybindingItem[] = [];
let resultLen = 0;
for (let i = items.length - 1; i >= 0; i--) {
result[resultLen++] = items[i];
}
return result;
}
public lookupPrimaryKeybinding(commandId: string): ResolvedKeybindingItem | null {
const items = this._lookupMap.get(commandId);
if (typeof items === 'undefined' || items.length === 0) {
return null;
}
if (items.length === 1) {
return items[0];
}
return items[items.length - 1];
}
/**
* Looks up a keybinding trigged as a result of pressing a sequence of chords - `[...currentChords, keypress]`
*
* Example: resolving 3 chords pressed sequentially - `cmd+k cmd+p cmd+i`:
* `currentChords = [ 'cmd+k' , 'cmd+p' ]` and `keypress = `cmd+i` - last pressed chord
*/
public resolve(currentChords: string[], keypress: string): ResolutionResult {
const pressedChords = [...currentChords, keypress];
this._log(`| Resolving ${pressedChords}`);
const kbCandidates = this._map.get(pressedChords[0]);
if (kbCandidates === undefined) {
// No bindings with such 0-th chord
this._log(`\\ No keybinding entries.`);
return NoMatchingKb;
}
let lookupMap: ResolvedKeybindingItem[] | null = null;
if (pressedChords.length < 2) {
lookupMap = kbCandidates;
} else {
// Fetch all chord bindings for `currentChords`
lookupMap = [];
for (let i = 0, len = kbCandidates.length; i < len; i++) {
const candidate = kbCandidates[i];
if (pressedChords.length > candidate.chords.length) {
// # of pressed chords can't be less than # of chords in a keybinding to invoke
continue;
}
let prefixMatches = true;
for (let i = 1; i < pressedChords.length; i++) {
if (candidate.chords[i] !== pressedChords[i]) {
prefixMatches = false;
break;
}
}
if (prefixMatches) {
lookupMap.push(candidate);
}
}
}
// check there's a keybinding with a matching when clause
const result = this._findCommand(lookupMap);
if (!result) {
this._log(`\\ From ${lookupMap.length} keybinding entries, no when clauses matched the context.`);
return NoMatchingKb;
}
// check we got all chords necessary to be sure a particular keybinding needs to be invoked
if (pressedChords.length < result.chords.length) {
// The chord sequence is not complete
this._log(
`\\ From ${lookupMap.length} keybinding entries, awaiting ${result.chords.length - pressedChords.length} more chord(s), source: ${printSourceExplanation(result)}.`,
);
return MoreChordsNeeded;
}
this._log(
`\\ From ${lookupMap.length} keybinding entries, matched ${result.command}, source: ${printSourceExplanation(result)}.`,
);
return KbFound(result.command, result.commandArgs, result.bubble);
}
private _findCommand(matches: ResolvedKeybindingItem[]): ResolvedKeybindingItem | null {
for (let i = matches.length - 1; i >= 0; i--) {
const k = matches[i];
return k;
}
return null;
}
}
export function toEmptyArrayIfContainsNull<T>(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;
function printSourceExplanation(kb: ResolvedKeybindingItem): string {
return kb.extensionId ? `user extension ${kb.extensionId}` : kb.isDefault ? `built-in` : `user`;
}

View File

@ -1,100 +1,468 @@
import { createDecorator, type IJSONSchema, KeyCode, type Event } from '@alilc/lowcode-shared';
import { Keybinding, ResolvedKeybinding } from './keybindings';
import { ResolutionResult, ResolvedKeybindingItem } from './keybindingResolver';
import {
DeferredPromise,
Disposable,
DisposableStore,
Events,
IDisposable,
illegalState,
IntervalTimer,
OperatingSystem,
OS,
TimeoutTimer,
} from '@alilc/lowcode-shared';
import { IKeybindingService } from './keybinding';
import {
BinaryKeybindingsMask,
Keybinding,
KeyCodeChord,
ResolvedChord,
ResolvedKeybinding,
SingleModifierChord,
} from './keybindings';
import { type IKeyboardEvent, StandardKeyboardEvent } from './keybindingEvent';
import { addDisposableListener, DOMEventType, KeyCode } from '../common';
import { IKeyboardMapper, USLayoutKeyboardMapper } from './keyboardMapper';
import { KeybindingParser } from './keybindingParser';
import { KeybindingResolver, ResolvedKeybindingItem, ResultKind } from './keybindingResolver';
import { IKeybindingItem, KeybindingsRegistry } from './keybindingRegistry';
import { ICommandService } from '../command';
export interface IUserFriendlyKeybinding {
key: string;
command: string;
args?: any;
when?: string;
interface CurrentChord {
keypress: string;
label: string | null;
}
export interface IKeyboardEvent {
readonly _standardKeyboardEventBrand: true;
export class KeybindingService extends Disposable implements IKeybindingService {
private _onDidUpdateKeybindings = this._addDispose(new Events.Emitter<void>());
onDidUpdateKeybindings = this._onDidUpdateKeybindings.event;
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly altGraphKey: boolean;
readonly keyCode: KeyCode;
readonly code: string;
}
private _keyboardMapper: IKeyboardMapper = new USLayoutKeyboardMapper();
export interface KeybindingsSchemaContribution {
readonly onDidChange?: Event<void>;
private _currentlyDispatchingCommandId: string | null = null;
private _isCompostion = false;
private _ignoreSingleModifiers: KeybindingModifierSet = KeybindingModifierSet.EMPTY;
private _currentSingleModifier: SingleModifierChord | null = null;
private _cachedResolver: KeybindingResolver | null = null;
getSchemaAdditions(): IJSONSchema[];
}
export interface IKeybindingService {
readonly _serviceBrand: undefined;
readonly inChordMode: boolean;
onDidUpdateKeybindings: Event<void>;
private _keybindingHoldMode: DeferredPromise<void> | null = null;
private _currentSingleModifierClearTimeout: TimeoutTimer = new TimeoutTimer();
private _currentChordChecker: IntervalTimer = new IntervalTimer();
/**
* 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.
* recently recorded keypresses that can trigger a keybinding;
*
* @returns A promise that resolves when hold stops, returns undefined if hold mode could not be enabled.
* example: say, there's "cmd+k cmd+i" keybinding;
* the user pressed "cmd+k" (before they press "cmd+i")
* "cmd+k" would be stored in this array, when on pressing "cmd+i", the service
* would invoke the command bound by the keybinding
*/
enableKeybindingHoldMode(commandId: string): Promise<void> | undefined;
private _currentChords: CurrentChord[] = [];
dispatchByUserSettingsLabel(userSettingsLabel: string, target: any): void;
get inChordMode(): boolean {
return this._currentChords.length > 0;
}
/**
* Look up keybindings for a command.
* Use `lookupKeybinding` if you are interested in the preferred keybinding.
*/
lookupKeybindings(commandId: string): ResolvedKeybinding[];
constructor(@ICommandService private readonly commandService: ICommandService) {
super();
/**
* 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;
this._addDispose(this._registerKeyboardEvent());
}
getDefaultKeybindingsContent(): string;
private _getResolver(): KeybindingResolver {
if (!this._cachedResolver) {
const defaults = this._resolveKeybindingItems(KeybindingsRegistry.getDefaultKeybindings(), true);
// const overrides = this._resolveUserKeybindingItems(this.userKeybindings.keybindings, false);
this._cachedResolver = new KeybindingResolver(defaults, [], console.log);
}
return this._cachedResolver;
}
getDefaultKeybindings(): readonly ResolvedKeybindingItem[];
private _resolveKeybindingItems(items: IKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] {
const result: ResolvedKeybindingItem[] = [];
getKeybindings(): readonly ResolvedKeybindingItem[];
let resultLen = 0;
for (const item of items) {
const keybinding = item.keybinding;
if (!keybinding) {
// This might be a removal keybinding item in user settings => accept it
result[resultLen++] = new ResolvedKeybindingItem(
undefined,
item.command,
item.commandArgs,
isDefault,
item.extensionId,
);
} else {
if (this._assertBrowserConflicts(keybinding)) {
continue;
}
customKeybindingsCount(): number;
const resolvedKeybindings = this._keyboardMapper.resolveKeybinding(keybinding);
for (let i = resolvedKeybindings.length - 1; i >= 0; i--) {
const resolvedKeybinding = resolvedKeybindings[i];
result[resultLen++] = new ResolvedKeybindingItem(
resolvedKeybinding,
item.command,
item.commandArgs,
isDefault,
item.extensionId,
);
}
}
}
/**
* 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;
return result;
}
registerSchemaContribution(contribution: KeybindingsSchemaContribution): void;
private _assertBrowserConflicts(keybinding: Keybinding): boolean {
for (const chord of keybinding.chords) {
if (!chord.metaKey && !chord.altKey && !chord.ctrlKey && !chord.shiftKey) {
continue;
}
toggleLogging(): boolean;
const modifiersMask = BinaryKeybindingsMask.CtrlCmd | BinaryKeybindingsMask.Alt | BinaryKeybindingsMask.Shift;
_dumpDebugInfo(): string;
_dumpDebugInfoJSON(): string;
let partModifiersMask = 0;
if (chord.metaKey) {
partModifiersMask |= BinaryKeybindingsMask.CtrlCmd;
}
if (chord.shiftKey) {
partModifiersMask |= BinaryKeybindingsMask.Shift;
}
if (chord.altKey) {
partModifiersMask |= BinaryKeybindingsMask.Alt;
}
if (chord.ctrlKey && OS === OperatingSystem.Macintosh) {
partModifiersMask |= BinaryKeybindingsMask.WinCtrl;
}
if ((partModifiersMask & modifiersMask) === (BinaryKeybindingsMask.CtrlCmd | BinaryKeybindingsMask.Alt)) {
if (chord.keyCode === KeyCode.LeftArrow || chord.keyCode === KeyCode.RightArrow) {
// console.warn('Ctrl/Cmd+Arrow keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
return true;
}
}
if ((partModifiersMask & modifiersMask) === BinaryKeybindingsMask.CtrlCmd) {
if (chord instanceof KeyCodeChord && chord.keyCode >= KeyCode.Digit0 && chord.keyCode <= KeyCode.Digit9) {
// console.warn('Ctrl/Cmd+Num keybindings should not be used by default in web. Offender: ', kb.getHashCode(), ' for ', commandId);
return true;
}
}
}
return false;
}
private _registerKeyboardEvent(): IDisposable {
const disposables = new DisposableStore();
disposables.add(
addDisposableListener(document, DOMEventType.KEY_DOWN, (e) => {
if (this._keybindingHoldMode) {
return;
}
this._isCompostion = e.isComposing;
const keyEvent = new StandardKeyboardEvent(e);
const shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target);
if (shouldPreventDefault) {
keyEvent.preventDefault();
}
this._isCompostion = false;
}),
);
disposables.add(
addDisposableListener(document, DOMEventType.KEY_UP, (e) => {
this._resetKeybindingHoldMode();
this._isCompostion = e.isComposing;
const keyEvent = new StandardKeyboardEvent(e);
const shouldPreventDefault = this._singleModifierDispatch(keyEvent, keyEvent.target);
if (shouldPreventDefault) {
keyEvent.preventDefault();
}
this._isCompostion = false;
}),
);
return disposables;
}
private _dispatch(e: IKeyboardEvent, target: HTMLElement): boolean {
return this._doDispatch(this.resolveKeyboardEvent(e), target, false);
}
private _doDispatch(userKeypress: ResolvedKeybinding, target: HTMLElement, isSingleModiferChord = false): boolean {
let shouldPreventDefault = false;
if (userKeypress.hasMultipleChords()) {
// warn - because user can press a single chord at a time
console.warn('Unexpected keyboard event mapped to multiple chords');
return false;
}
let userPressedChord: string | null = null;
let currentChords: string[] | null = null;
if (isSingleModiferChord) {
// The keybinding is the second keypress of a single modifier chord, e.g. "shift shift".
// A single modifier can only occur when the same modifier is pressed in short sequence,
// hence we disregard `_currentChord` and use the same modifier instead.
const [dispatchKeyname] = userKeypress.getSingleModifierDispatchChords();
userPressedChord = dispatchKeyname;
currentChords = dispatchKeyname ? [dispatchKeyname] : []; // TODO@ulugbekna: in the `else` case we assign an empty array - make sure `resolve` can handle an empty array well
} else {
[userPressedChord] = userKeypress.getDispatchChords();
currentChords = this._currentChords.map(({ keypress }) => keypress);
}
if (userPressedChord === null) {
console.log(`\\ Keyboard event cannot be dispatched in keydown phase.`);
// cannot be dispatched, probably only modifier keys
return shouldPreventDefault;
}
const keypressLabel = userKeypress.getLabel();
const resolveResult = this._getResolver().resolve(currentChords, userPressedChord);
switch (resolveResult.kind) {
case ResultKind.NoMatchingKb: {
if (this.inChordMode) {
const currentChordsLabel = this._currentChords.map(({ label }) => label).join(', ');
console.log(`+ Leaving multi-chord mode: Nothing bound to "${currentChordsLabel}, ${keypressLabel}".`);
this._leaveChordMode();
shouldPreventDefault = true;
}
return shouldPreventDefault;
}
case ResultKind.MoreChordsNeeded: {
shouldPreventDefault = true;
this._expectAnotherChord(userPressedChord, keypressLabel);
console.log(
this._currentChords.length === 1 ? `+ Entering multi-chord mode...` : `+ Continuing multi-chord mode...`,
);
return shouldPreventDefault;
}
case ResultKind.KbFound: {
if (resolveResult.commandId === null || resolveResult.commandId === '') {
if (this.inChordMode) {
const currentChordsLabel = this._currentChords.map(({ label }) => label).join(', ');
console.log(`+ Leaving chord mode: Nothing bound to "${currentChordsLabel}, ${keypressLabel}".`);
this._leaveChordMode();
shouldPreventDefault = true;
}
} else {
if (this.inChordMode) {
this._leaveChordMode();
}
if (!resolveResult.isBubble) {
shouldPreventDefault = true;
}
console.log(`+ Invoking command ${resolveResult.commandId}.`);
this._currentlyDispatchingCommandId = resolveResult.commandId;
try {
this.commandService
.executeCommand(
resolveResult.commandId,
typeof resolveResult.commandArgs === 'undefined' ? undefined : resolveResult.commandArgs,
)
.then(undefined, (err) => console.warn(err));
} finally {
this._currentlyDispatchingCommandId = null;
}
}
return shouldPreventDefault;
}
}
}
private _leaveChordMode(): void {
this._currentChordChecker.cancel();
this._currentChords = [];
}
private _expectAnotherChord(firstChord: string, keypressLabel: string | null): void {
this._currentChords.push({ keypress: firstChord, label: keypressLabel });
switch (this._currentChords.length) {
case 0:
throw illegalState('impossible');
case 1:
// TODO@ulugbekna: revise this message and the one below (at least, fix terminology)
// this._currentChordStatusMessage = this._notificationService.status(
// nls.localize('first.chord', '({0}) was pressed. Waiting for second key of chord...', keypressLabel),
// );
break;
default: {
// const fullKeypressLabel = this._currentChords.map(({ label }) => label).join(', ');
// this._currentChordStatusMessage = this._notificationService.status(
// nls.localize('next.chord', '({0}) was pressed. Waiting for next key of chord...', fullKeypressLabel),
// );
}
}
this._scheduleLeaveChordMode();
}
private _scheduleLeaveChordMode(): void {
const chordLastInteractedTime = Date.now();
this._currentChordChecker.cancelAndSet(() => {
if (Date.now() - chordLastInteractedTime > 5000) {
// 5 seconds elapsed => leave chord mode
this._leaveChordMode();
}
}, 500);
}
private _singleModifierDispatch(e: IKeyboardEvent, target: HTMLElement): boolean {
const keybinding = this.resolveKeyboardEvent(e);
const [singleModifier] = keybinding.getSingleModifierDispatchChords();
if (singleModifier) {
if (this._ignoreSingleModifiers.has(singleModifier)) {
console.log(`+ Ignoring single modifier ${singleModifier} due to it being pressed together with other keys.`);
this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY;
this._currentSingleModifierClearTimeout.cancel();
this._currentSingleModifier = null;
return false;
}
this._ignoreSingleModifiers = KeybindingModifierSet.EMPTY;
if (this._currentSingleModifier === null) {
// we have a valid `singleModifier`, store it for the next keyup, but clear it in 300ms
console.log(`+ Storing single modifier for possible chord ${singleModifier}.`);
this._currentSingleModifier = singleModifier;
this._currentSingleModifierClearTimeout.cancelAndSet(() => {
console.log(`+ Clearing single modifier due to 300ms elapsed.`);
this._currentSingleModifier = null;
}, 300);
return false;
}
if (singleModifier === this._currentSingleModifier) {
// bingo!
console.log(`/ Dispatching single modifier chord ${singleModifier} ${singleModifier}`);
this._currentSingleModifierClearTimeout.cancel();
this._currentSingleModifier = null;
return this._doDispatch(keybinding, target, /*isSingleModiferChord*/ true);
}
console.log(
`+ Clearing single modifier due to modifier mismatch: ${this._currentSingleModifier} ${singleModifier}`,
);
this._currentSingleModifierClearTimeout.cancel();
this._currentSingleModifier = null;
return false;
}
// When pressing a modifier and holding it pressed with any other modifier or key combination,
// the pressed modifiers should no longer be considered for single modifier dispatch.
const [firstChord] = keybinding.getChords();
this._ignoreSingleModifiers = new KeybindingModifierSet(firstChord);
if (this._currentSingleModifier !== null) {
console.log(`+ Clearing single modifier due to other key up.`);
}
this._currentSingleModifierClearTimeout.cancel();
this._currentSingleModifier = null;
return false;
}
enableKeybindingHoldMode(commandId: string): Promise<void> | undefined {
if (this._currentlyDispatchingCommandId !== commandId) {
return undefined;
}
this._keybindingHoldMode = new DeferredPromise<void>();
return this._keybindingHoldMode.p;
}
private _resetKeybindingHoldMode(): void {
if (this._keybindingHoldMode) {
this._keybindingHoldMode?.complete();
this._keybindingHoldMode = null;
}
}
resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[] {
return this._keyboardMapper.resolveKeybinding(keybinding);
}
resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {
return this._keyboardMapper.resolveKeyboardEvent(keyboardEvent);
}
resolveUserBinding(userBinding: string): ResolvedKeybinding[] {
const keybinding = KeybindingParser.parseKeybinding(userBinding);
return keybinding ? this._keyboardMapper.resolveKeybinding(keybinding) : [];
}
dispatchEvent(e: IKeyboardEvent, target: HTMLElement): boolean {
return this._dispatch(e, target);
}
dispatchByUserSettingsLabel(userSettingsLabel: string, target: HTMLElement): void {
const keybindings = this.resolveUserBinding(userSettingsLabel);
if (keybindings.length === 0) {
console.log(`\\ Could not resolve - ${userSettingsLabel}`);
} else {
this._doDispatch(keybindings[0], target, /*isSingleModiferChord*/ false);
}
}
lookupKeybindings(commandId: string): ResolvedKeybinding[] {
return this._getResolver()
.lookupKeybindings(commandId)
.map((item) => item.resolvedKeybinding)
.filter(Boolean) as ResolvedKeybinding[];
}
getDefaultKeybindings(): readonly ResolvedKeybindingItem[] {
return this._getResolver().getDefaultKeybindings();
}
getKeybindings(): readonly ResolvedKeybindingItem[] {
return this._getResolver().getKeybindings();
}
}
export const IKeybindingService = createDecorator<IKeybindingService>('keybindingService');
class KeybindingModifierSet {
public static EMPTY = new KeybindingModifierSet(null);
private readonly _ctrlKey: boolean;
private readonly _shiftKey: boolean;
private readonly _altKey: boolean;
private readonly _metaKey: boolean;
constructor(source: ResolvedChord | null) {
this._ctrlKey = source ? source.ctrlKey : false;
this._shiftKey = source ? source.shiftKey : false;
this._altKey = source ? source.altKey : false;
this._metaKey = source ? source.metaKey : false;
}
has(modifier: SingleModifierChord) {
switch (modifier) {
case 'ctrl':
return this._ctrlKey;
case 'shift':
return this._shiftKey;
case 'alt':
return this._altKey;
case 'meta':
return this._metaKey;
}
}
}

View File

@ -1,5 +1,6 @@
import { illegalArgument, OperatingSystem } from '@alilc/lowcode-shared';
import { KeyCode, ScanCode } from '../common/keyCodes';
import { KeyCode } from '../common/keyCodes';
import { AriaLabelProvider, UILabelProvider, UserSettingsLabelProvider } from './keybingdingLabels';
/**
* Binary encoding strategy:
@ -14,7 +15,7 @@ import { KeyCode, ScanCode } from '../common/keyCodes';
* K = bits 0-7 = key code
* ```
*/
const enum BinaryKeybindingsMask {
export const enum BinaryKeybindingsMask {
CtrlCmd = (1 << 11) >>> 0,
Shift = (1 << 10) >>> 0,
Alt = (1 << 9) >>> 0,
@ -22,28 +23,27 @@ const enum BinaryKeybindingsMask {
KeyCode = 0x000000ff,
}
export function decodeKeybinding(
keybinding: number | number[],
OS: OperatingSystem,
): Keybinding | null {
export function decodeKeybinding(keybinding: number | number[], OS: OperatingSystem): Keybinding | null {
if (typeof keybinding === 'number') {
if (keybinding === 0) {
return null;
}
const firstChord = (keybinding & 0x0000ffff) >>> 0;
const secondChord = (keybinding & 0xffff0000) >>> 16;
if (secondChord !== 0) {
return new Keybinding([
createSimpleKeybinding(firstChord, OS),
createSimpleKeybinding(secondChord, OS),
]);
return new Keybinding([createSimpleKeybinding(firstChord, OS), createSimpleKeybinding(secondChord, OS)]);
}
return new Keybinding([createSimpleKeybinding(firstChord, OS)]);
} else {
const chords = [];
for (let i = 0; i < keybinding.length; i++) {
chords.push(createSimpleKeybinding(keybinding[i], OS));
}
return new Keybinding(chords);
}
}
@ -81,7 +81,7 @@ export class KeyCodeChord implements Modifiers {
public readonly keyCode: KeyCode,
) {}
equals(other: Chord): boolean {
equals(other: KeyCodeChord): boolean {
return (
other instanceof KeyCodeChord &&
this.ctrlKey === other.ctrlKey &&
@ -127,64 +127,13 @@ export class KeyCodeChord implements Modifiers {
}
}
/**
* Represents a chord which uses the `code` field of keyboard events.
* A chord is a combination of keys pressed simultaneously.
*/
export class ScanCodeChord implements Modifiers {
constructor(
public readonly ctrlKey: boolean,
public readonly shiftKey: boolean,
public readonly altKey: boolean,
public readonly metaKey: boolean,
public readonly scanCode: ScanCode,
) {}
equals(other: Chord): boolean {
return (
other instanceof ScanCodeChord &&
this.ctrlKey === other.ctrlKey &&
this.shiftKey === other.shiftKey &&
this.altKey === other.altKey &&
this.metaKey === other.metaKey &&
this.scanCode === other.scanCode
);
}
getHashCode(): string {
const ctrl = this.ctrlKey ? '1' : '0';
const shift = this.shiftKey ? '1' : '0';
const alt = this.altKey ? '1' : '0';
const meta = this.metaKey ? '1' : '0';
return `S${ctrl}${shift}${alt}${meta}${this.scanCode}`;
}
/**
* Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
*/
isDuplicateModifierCase(): boolean {
return (
(this.ctrlKey &&
(this.scanCode === ScanCode.ControlLeft || this.scanCode === ScanCode.ControlRight)) ||
(this.shiftKey &&
(this.scanCode === ScanCode.ShiftLeft || this.scanCode === ScanCode.ShiftRight)) ||
(this.altKey &&
(this.scanCode === ScanCode.AltLeft || this.scanCode === ScanCode.AltRight)) ||
(this.metaKey &&
(this.scanCode === ScanCode.MetaLeft || this.scanCode === ScanCode.MetaRight))
);
}
}
export type Chord = KeyCodeChord | ScanCodeChord;
/**
* A keybinding is a sequence of chords.
*/
export class Keybinding {
readonly chords: Chord[];
readonly chords: KeyCodeChord[];
constructor(chords: Chord[]) {
constructor(chords: KeyCodeChord[]) {
if (chords.length === 0) {
throw illegalArgument(`chords`);
}
@ -243,11 +192,6 @@ export abstract class ResolvedKeybinding {
* This prints the binding in a format suitable for ARIA.
*/
public abstract getAriaLabel(): string | null;
/**
* This prints the binding in a format suitable for electron's accelerators.
* See https://github.com/electron/electron/blob/master/docs/api/accelerator.md
*/
public abstract getElectronAccelerator(): string | null;
/**
* This prints the binding in a format suitable for user settings.
*/
@ -279,3 +223,65 @@ export abstract class ResolvedKeybinding {
*/
public abstract getSingleModifierDispatchChords(): (SingleModifierChord | null)[];
}
export abstract class BaseResolvedKeybinding extends ResolvedKeybinding {
protected readonly _chords: readonly KeyCodeChord[];
constructor(chords: readonly KeyCodeChord[]) {
super();
if (chords.length === 0) {
throw illegalArgument(`chords`);
}
this._chords = chords;
}
public getLabel(): string | null {
return UILabelProvider.toLabel(this._chords, (keybinding) => this._getLabel(keybinding));
}
public getAriaLabel(): string | null {
return AriaLabelProvider.toLabel(this._chords, (keybinding) => this._getAriaLabel(keybinding));
}
public getUserSettingsLabel(): string | null {
return UserSettingsLabelProvider.toLabel(this._chords, (keybinding) => this._getUserSettingsLabel(keybinding));
}
public isWYSIWYG(): boolean {
return this._chords.every((keybinding) => this._isWYSIWYG(keybinding));
}
public hasMultipleChords(): boolean {
return this._chords.length > 1;
}
public getChords(): ResolvedChord[] {
return this._chords.map((keybinding) => this._getChord(keybinding));
}
private _getChord(keybinding: KeyCodeChord): ResolvedChord {
return new ResolvedChord(
keybinding.ctrlKey,
keybinding.shiftKey,
keybinding.altKey,
keybinding.metaKey,
this._getLabel(keybinding),
this._getAriaLabel(keybinding),
);
}
public getDispatchChords(): (string | null)[] {
return this._chords.map((keybinding) => this._getChordDispatch(keybinding));
}
public getSingleModifierDispatchChords(): (SingleModifierChord | null)[] {
return this._chords.map((keybinding) => this._getSingleModifierChordDispatch(keybinding));
}
protected abstract _getLabel(keybinding: KeyCodeChord): string | null;
protected abstract _getAriaLabel(keybinding: KeyCodeChord): string | null;
protected abstract _getUserSettingsLabel(keybinding: KeyCodeChord): string | null;
protected abstract _isWYSIWYG(keybinding: KeyCodeChord): boolean;
protected abstract _getChordDispatch(keybinding: KeyCodeChord): string | null;
protected abstract _getSingleModifierChordDispatch(keybinding: KeyCodeChord): SingleModifierChord | null;
}

View File

@ -0,0 +1,156 @@
import { OperatingSystem, OS } from '@alilc/lowcode-shared';
import { Modifiers } from './keybindings';
export interface ModifierLabels {
readonly ctrlKey: string;
readonly shiftKey: string;
readonly altKey: string;
readonly metaKey: string;
readonly separator: string;
}
export interface KeyLabelProvider<T extends Modifiers> {
(keybinding: T): string | null;
}
export class ModifierLabelProvider {
public readonly modifierLabels: ModifierLabels[];
constructor(mac: ModifierLabels, windows: ModifierLabels, linux: ModifierLabels = windows) {
this.modifierLabels = [null!]; // index 0 will never me accessed.
this.modifierLabels[OperatingSystem.Macintosh] = mac;
this.modifierLabels[OperatingSystem.Windows] = windows;
this.modifierLabels[OperatingSystem.Linux] = linux;
}
public toLabel<T extends Modifiers>(chords: readonly T[], keyLabelProvider: KeyLabelProvider<T>): string | null {
if (chords.length === 0) {
return null;
}
const result: string[] = [];
for (let i = 0, len = chords.length; i < len; i++) {
const chord = chords[i];
const keyLabel = keyLabelProvider(chord);
if (keyLabel === null) {
// this keybinding cannot be expressed...
return null;
}
result[i] = _simpleAsString(chord, keyLabel, this.modifierLabels[OS]);
}
return result.join(' ');
}
}
function _simpleAsString(modifiers: Modifiers, key: string, labels: ModifierLabels): string {
if (key === null) {
return '';
}
const result: string[] = [];
// translate modifier keys: Ctrl-Shift-Alt-Meta
if (modifiers.ctrlKey) {
result.push(labels.ctrlKey);
}
if (modifiers.shiftKey) {
result.push(labels.shiftKey);
}
if (modifiers.altKey) {
result.push(labels.altKey);
}
if (modifiers.metaKey) {
result.push(labels.metaKey);
}
// the actual key
if (key !== '') {
result.push(key);
}
return result.join(labels.separator);
}
/**
* A label provider that prints modifiers in a suitable format for displaying in the UI.
*/
export const UILabelProvider = new ModifierLabelProvider(
{
ctrlKey: '\u2303',
shiftKey: '⇧',
altKey: '⌥',
metaKey: '⌘',
separator: '',
},
{
ctrlKey: 'Ctrl',
shiftKey: 'Shift',
altKey: 'Alt',
metaKey: 'Windows',
separator: '+',
},
{
ctrlKey: 'Ctrl',
shiftKey: 'Shift',
altKey: 'Alt',
metaKey: 'Super',
separator: '+',
},
);
/**
* A label provider that prints modifiers in a suitable format for ARIA.
*/
export const AriaLabelProvider = new ModifierLabelProvider(
{
ctrlKey: 'Control',
shiftKey: 'Shift',
altKey: 'Option',
metaKey: 'Command',
separator: '+',
},
{
ctrlKey: 'Control',
shiftKey: 'Shift',
altKey: 'Alt',
metaKey: 'Windows',
separator: '+',
},
{
ctrlKey: 'Control',
shiftKey: 'Shift',
altKey: 'Alt',
metaKey: 'Super',
separator: '+',
},
);
/**
* A label provider that prints modifiers in a suitable format for user settings.
*/
export const UserSettingsLabelProvider = new ModifierLabelProvider(
{
ctrlKey: 'ctrl',
shiftKey: 'shift',
altKey: 'alt',
metaKey: 'cmd',
separator: '+',
},
{
ctrlKey: 'ctrl',
shiftKey: 'shift',
altKey: 'alt',
metaKey: 'win',
separator: '+',
},
{
ctrlKey: 'ctrl',
shiftKey: 'shift',
altKey: 'alt',
metaKey: 'meta',
separator: '+',
},
);

View File

@ -0,0 +1,31 @@
import { IKeyboardEvent } from './keybindingEvent';
import { Keybinding, KeyCodeChord, ResolvedKeybinding } from './keybindings';
import { USLayoutResolvedKeybinding } from './usLayoutResolvedKeybinding';
export interface IKeyboardMapper {
resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding;
resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[];
}
/**
* A keyboard mapper to be used when reading the keymap.
*/
export class USLayoutKeyboardMapper implements IKeyboardMapper {
public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {
const ctrlKey = keyboardEvent.ctrlKey;
const altKey = keyboardEvent.altKey;
const chord = new KeyCodeChord(
ctrlKey,
keyboardEvent.shiftKey,
altKey,
keyboardEvent.metaKey,
keyboardEvent.keyCode,
);
const result = this.resolveKeybinding(new Keybinding([chord]));
return result[0];
}
public resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[] {
return USLayoutResolvedKeybinding.resolveKeybinding(keybinding);
}
}

View File

@ -0,0 +1,103 @@
import { OperatingSystem, OS } from '@alilc/lowcode-shared';
import { BaseResolvedKeybinding, Keybinding, KeyCodeChord, SingleModifierChord } from './keybindings';
import { KeyCode, KeyCodeUtils } from '../common';
import { toEmptyArrayIfContainsNull } from './keybindingResolver';
export class USLayoutResolvedKeybinding extends BaseResolvedKeybinding {
public static resolveKeybinding(keybinding: Keybinding): USLayoutResolvedKeybinding[] {
const chords: KeyCodeChord[] = toEmptyArrayIfContainsNull(keybinding.chords);
if (chords.length > 0) {
return [new USLayoutResolvedKeybinding(chords)];
}
return [];
}
constructor(chords: KeyCodeChord[]) {
super(chords);
}
private _keyCodeToUILabel(keyCode: KeyCode): string {
if (OS === OperatingSystem.Macintosh) {
switch (keyCode) {
case KeyCode.LeftArrow:
return '←';
case KeyCode.UpArrow:
return '↑';
case KeyCode.RightArrow:
return '→';
case KeyCode.DownArrow:
return '↓';
}
}
return KeyCodeUtils.toString(keyCode);
}
protected _getLabel(chord: KeyCodeChord): string | null {
if (chord.isDuplicateModifierCase()) {
return '';
}
return this._keyCodeToUILabel(chord.keyCode);
}
protected _getAriaLabel(chord: KeyCodeChord): string | null {
if (chord.isDuplicateModifierCase()) {
return '';
}
return KeyCodeUtils.toString(chord.keyCode);
}
protected _getUserSettingsLabel(chord: KeyCodeChord): string | null {
if (chord.isDuplicateModifierCase()) {
return '';
}
const result = KeyCodeUtils.toUserSettingsUS(chord.keyCode);
return result ? result.toLowerCase() : result;
}
protected _isWYSIWYG(): boolean {
return true;
}
protected _getChordDispatch(chord: KeyCodeChord): string | null {
return USLayoutResolvedKeybinding.getDispatchStr(chord);
}
public static getDispatchStr(chord: KeyCodeChord): string | null {
if (chord.isModifierKey()) {
return null;
}
let result = '';
if (chord.ctrlKey) {
result += 'ctrl+';
}
if (chord.shiftKey) {
result += 'shift+';
}
if (chord.altKey) {
result += 'alt+';
}
if (chord.metaKey) {
result += 'meta+';
}
result += KeyCodeUtils.toString(chord.keyCode);
return result;
}
protected _getSingleModifierChordDispatch(keybinding: KeyCodeChord): SingleModifierChord | null {
if (keybinding.keyCode === KeyCode.Ctrl && !keybinding.shiftKey && !keybinding.altKey && !keybinding.metaKey) {
return 'ctrl';
}
if (keybinding.keyCode === KeyCode.Shift && !keybinding.ctrlKey && !keybinding.altKey && !keybinding.metaKey) {
return 'shift';
}
if (keybinding.keyCode === KeyCode.Alt && !keybinding.ctrlKey && !keybinding.shiftKey && !keybinding.metaKey) {
return 'alt';
}
if (keybinding.keyCode === KeyCode.Meta && !keybinding.ctrlKey && !keybinding.shiftKey && !keybinding.altKey) {
return 'meta';
}
return null;
}
}

View File

@ -1,10 +1,15 @@
import { InstantiationService, BeanContainer, CtorDescriptor } from '@alilc/lowcode-shared';
import { URI } from './common/uri';
import * as Schemas from './common/schemas';
import { CommandService, ICommandService } from './command';
import { IKeybindingService, KeybindingService } from './keybinding';
import { ConfigurationService, IConfigurationService } from './configuration';
import { IExtensionService, ExtensionService } from './extension';
import { IWorkspaceService, WorkspaceService, toWorkspaceIdentifier } from './workspace';
import { IWindowService, WindowService } from './window';
import { IFileService, FileService, InMemoryFileSystemProvider } from './file';
import { URI } from './common/uri';
import * as Schemas from './common/schemas';
import { IResourceService, ResourceService } from './resource';
class TestMainApplication {
instantiationService: InstantiationService;
@ -25,11 +30,14 @@ class TestMainApplication {
fileService.registerProvider(Schemas.file, new InMemoryFileSystemProvider());
try {
const uri = URI.from({ path: '/Desktop' });
const root = URI.from({ scheme: Schemas.file, path: '/' });
await workspaceService.enterWorkspace(toWorkspaceIdentifier(uri.path));
// empty or mutiple files
// 展示目录结构
const workspace = await workspaceService.enterWorkspace(toWorkspaceIdentifier(root.path));
const fileUri = URI.joinPath(uri, 'test.lc');
// 打开页面 or 保留空白页
const fileUri = URI.joinPath(workspace.uri, 'test.lc');
await windowService.open({
urisToOpen: [{ fileUri }],
@ -46,6 +54,13 @@ class TestMainApplication {
const configurationService = new ConfigurationService();
container.set(IConfigurationService, configurationService);
const resourceService = new ResourceService();
container.set(IResourceService, resourceService);
container.set(ICommandService, new CtorDescriptor(CommandService));
container.set(IKeybindingService, new CtorDescriptor(KeybindingService));
container.set(IExtensionService, new CtorDescriptor(ExtensionService));
const workspaceService = new WorkspaceService();
container.set(IWorkspaceService, workspaceService);

View File

@ -40,7 +40,7 @@ export interface IPath<T = IEditOptions> {
* file exists, if false it does not. with
* `undefined` the state is unknown.
*/
readonly exists?: boolean;
readonly exists: boolean;
/**
* Optional editor options to apply in the file
@ -52,6 +52,8 @@ export interface IWindowConfiguration {
fileToOpenOrCreate: IPath;
workspace?: IWorkspaceIdentifier;
contentType: string;
}
export interface IEditWindow extends IDisposable {
@ -70,10 +72,11 @@ export interface IEditWindow extends IDisposable {
readonly isReady: boolean;
ready(): Promise<IEditWindow>;
load(config: IWindowConfiguration, options?: { isReload?: boolean }): void;
reload(): void;
load(config: IWindowConfiguration, options?: { isReload?: boolean }): Promise<void>;
reload(): Promise<void>;
close(): void;
destory(): Promise<void>;
sendWhenReady(channel: string, ...args: any[]): void;
}

View File

@ -1,6 +1,9 @@
import { Disposable, Events } from '@alilc/lowcode-shared';
import { IWindowState, IEditWindow, IWindowConfiguration } from './window';
import { IFileService } from '../file';
import { Registry, Extensions } from '../extension/registry';
import { IContentEditorRegistry } from '../contentEditor/contentEditorRegistry';
import { IContentEditor } from '../contentEditor/contentEditor';
export interface IWindowCreationOptions {
readonly state: IWindowState;
@ -76,18 +79,45 @@ export class EditWindow extends Disposable implements IEditWindow {
}
}
load(config: IWindowConfiguration): void {
async load(config: IWindowConfiguration): Promise<void> {
const { fileToOpenOrCreate, contentType } = config;
const { exists, fileUri } = fileToOpenOrCreate;
const contentEditor = Registry.as<IContentEditorRegistry>(Extensions.ContentEditor).getContentEditor(contentType);
if (!contentEditor) {
throw Error('content editor not found');
}
let content: string = '';
const fs = this.fileService.withProvider(fileToOpenOrCreate.fileUri)!;
if (!exists) {
await fs.writeFile(fileUri, content);
} else {
content = await fs.readFile(fileUri);
}
contentEditor.load(content);
this._onWillLoad.notify();
this._config = config;
this.setReady();
}
reload(): void {}
async reload(): Promise<void> {
await this.destory();
return this.load(this._config!);
}
focus(): void {}
close(): void {}
async destory(): Promise<void> {}
sendWhenReady(channel: string, ...args: any[]): void {
if (this.isReady) {
this.send(channel, ...args);

View File

@ -0,0 +1 @@
export interface IWindowManagemant {}

View File

@ -1,8 +1,10 @@
import { createDecorator, Disposable, Events, IInstantiationService } from '@alilc/lowcode-shared';
import { defaultWindowState, IEditWindow, IWindowConfiguration } from './window';
import { Schemas, URI } from '../common';
import { Schemas, URI, extname } from '../common';
import { EditWindow } from './windowImpl';
import { IFileService } from '../file';
import { Extensions, Registry } from '../extension/registry';
import { IContentEditorRegistry } from '../contentEditor/contentEditorRegistry';
export interface IOpenConfiguration {
readonly urisToOpen: IWindowOpenable[];
@ -30,9 +32,9 @@ export interface IWindowService {
open(openConfig: IOpenConfiguration): Promise<IEditWindow[]>;
// sendToFocused(channel: string, ...args: any[]): void;
// sendToOpeningWindow(channel: string, ...args: any[]): void;
// sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void;
sendToFocused(channel: string, ...args: any[]): void;
sendToOpeningWindow(channel: string, ...args: any[]): void;
sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void;
getWindows(): IEditWindow[];
getWindowCount(): number;
@ -103,16 +105,24 @@ export class WindowService extends Disposable implements IWindowService {
if (openOnlyIfExists) continue;
}
const config: IWindowConfiguration = {
fileToOpenOrCreate: {
fileUri: item.fileUri,
exists,
options: {},
},
};
const window = await this._openInEditWindow(config);
const fileExt = extname(item.fileUri.path);
const registeredContentType = Registry.as<IContentEditorRegistry>(Extensions.ContentEditor).getContentTypeByExt(
fileExt,
);
usedWindows.push(window);
if (registeredContentType) {
const config: IWindowConfiguration = {
fileToOpenOrCreate: {
fileUri: item.fileUri,
exists,
options: {},
},
contentType: registeredContentType,
};
const window = await this._openInEditWindow(config);
usedWindows.push(window);
}
}
return usedWindows;
@ -128,6 +138,19 @@ export class WindowService extends Disposable implements IWindowService {
newWindow.load(config);
newWindow.onDidDestroy(() => {
this.windows.delete(newWindow.id);
});
return newWindow;
}
sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void {}
sendToFocused(channel: string, ...args: any[]): void {
const focusedWindow = this.getLastActiveWindow();
focusedWindow?.sendWhenReady(channel, ...args);
}
sendToOpeningWindow(channel: string, ...args: any[]): void {}
}

View File

@ -1,22 +1,19 @@
import { createDecorator, Disposable } from '@alilc/lowcode-shared';
import { Workspace, type IWorkspaceIdentifier, isWorkspaceIdentifier } from './workspace';
import { toWorkspaceFolder, IWorkspaceFolder } from './workspaceFolder';
import { URI } from '../common';
import { toWorkspaceFolder } from './workspaceFolder';
export interface IWorkspaceService {
initialize(): Promise<void>;
enterWorkspace(identifier: IWorkspaceIdentifier): Promise<void>;
enterWorkspace(identifier: IWorkspaceIdentifier): Promise<Workspace>;
getWorkspace(): Workspace;
getWorkspaceFolder(resource: URI): IWorkspaceFolder | null;
getWorkspace(id: string | IWorkspaceIdentifier): Workspace | undefined;
}
export const IWorkspaceService = createDecorator<IWorkspaceService>('workspaceService');
export class WorkspaceService extends Disposable implements IWorkspaceService {
private _workspace: Workspace;
private _workspacesMap = new Map<string, Workspace>();
constructor() {
super();
@ -24,19 +21,23 @@ export class WorkspaceService extends Disposable implements IWorkspaceService {
async initialize() {}
async enterWorkspace(identifier: IWorkspaceIdentifier) {
async enterWorkspace(identifier: IWorkspaceIdentifier): Promise<Workspace> {
if (!isWorkspaceIdentifier(identifier)) {
throw new Error('Invalid workspace identifier');
}
this._workspace = new Workspace(identifier.id, identifier.uri, [toWorkspaceFolder(identifier.uri)]);
const workspace = new Workspace(identifier.id, identifier.uri, [toWorkspaceFolder(identifier.uri)]);
this._workspacesMap.set(identifier.id, workspace);
return workspace;
}
getWorkspace(): Workspace {
return this._workspace;
}
getWorkspace(identifier: string | IWorkspaceIdentifier): Workspace | undefined {
if (isWorkspaceIdentifier(identifier)) {
return this._workspacesMap.get(identifier.id);
}
getWorkspaceFolder(resource: URI) {
return this._workspace.getFolder(resource);
return this._workspacesMap.get(identifier);
}
}

View File

@ -15,8 +15,7 @@ export interface IExtensionHostService {
getPlugin(name: string): Plugin | undefined;
}
export const IExtensionHostService =
createDecorator<IExtensionHostService>('pluginManagementService');
export const IExtensionHostService = createDecorator<IExtensionHostService>('pluginManagementService');
export class ExtensionHostService extends Disposable implements IExtensionHostService {
boostsManager: BoostsManager;
@ -35,11 +34,7 @@ export class ExtensionHostService extends Disposable implements IExtensionHostSe
) {
super();
this.boostsManager = new BoostsManager(
codeRuntimeService,
runtimeIntlService,
runtimeUtilService,
);
this.boostsManager = new BoostsManager(codeRuntimeService, runtimeIntlService, runtimeUtilService);
this._pluginSetupContext = {
globalState: new Map(),

View File

@ -11,6 +11,7 @@ export class CtorDescriptor<T> {
constructor(
readonly ctor: Constructor<T>,
readonly staticArguments: any[] = [],
readonly supportsDelayedInstantiation: boolean = false,
) {}
}

View File

@ -194,7 +194,12 @@ export class InstantiationService implements IInstantiationService {
const instanceOrDesc = this._container.get(data.id);
if (instanceOrDesc instanceof CtorDescriptor) {
// create instance and overwrite the service collections
const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments);
const instance = this._createServiceInstanceWithOwner(
data.id,
data.desc.ctor,
data.desc.staticArguments,
data.desc.supportsDelayedInstantiation,
);
this._setCreatedServiceInstance(data.id, instance);
}
graph.removeNode(data);
@ -205,14 +210,23 @@ export class InstantiationService implements IInstantiationService {
return this._container.get(id) as T;
}
private _createServiceInstanceWithOwner<T>(id: BeanIdentifier<T>, ctor: any, args: any[]): T {
private _createServiceInstanceWithOwner<T>(
id: BeanIdentifier<T>,
ctor: any,
args: any[],
supportsDelayedInstantiation: boolean,
): T {
if (this._container.get(id) instanceof CtorDescriptor) {
if (supportsDelayedInstantiation) {
// todo
}
const instance = this.createInstance(ctor, args);
this._beansToMaybeDispose.add(instance);
return instance;
} else if (this._parent) {
return this._parent._createServiceInstanceWithOwner(id, ctor, args);
return this._parent._createServiceInstanceWithOwner(id, ctor, args, supportsDelayedInstantiation);
} else {
throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`);
}

View File

@ -35,9 +35,10 @@ export function platformToString(platform: PlatformEnum): PlatformName {
export const enum OperatingSystem {
Windows = 1,
Macintosh = 2,
Linux = 3
Linux = 3,
}
export const OS = (isMacintosh || isIOS ? OperatingSystem.Macintosh : (isWindows ? OperatingSystem.Windows : OperatingSystem.Linux));
export const OS =
isMacintosh || isIOS ? OperatingSystem.Macintosh : isWindows ? OperatingSystem.Windows : OperatingSystem.Linux;
export let platform: PlatformEnum = PlatformEnum.Unknown;
if (isMacintosh) {
@ -48,8 +49,9 @@ if (isMacintosh) {
platform = PlatformEnum.Linux;
}
export const isChrome = !!(userAgent && userAgent.indexOf('Chrome') >= 0);
export const isFirefox = !!(userAgent && userAgent.indexOf('Firefox') >= 0);
export const isSafari = !!(!isChrome && userAgent && userAgent.indexOf('Safari') >= 0);
export const isEdge = !!(userAgent && userAgent.indexOf('Edg/') >= 0);
export const isAndroid = !!(userAgent && userAgent.indexOf('Android') >= 0);
export const isChrome = userAgent && userAgent.indexOf('Chrome') >= 0;
export const isWebKit = userAgent.indexOf('AppleWebKit') >= 0;
export const isFirefox = userAgent && userAgent.indexOf('Firefox') >= 0;
export const isSafari = !isChrome && userAgent && userAgent.indexOf('Safari') >= 0;
export const isEdge = userAgent && userAgent.indexOf('Edg/') >= 0;
export const isAndroid = userAgent && userAgent.indexOf('Android') >= 0;

View File

@ -1,3 +1,2 @@
export * from './specs';
export * from './json';
export * from './common';

View File

@ -1,86 +0,0 @@
export type JSONValue = number | string | boolean | null;
export interface JSONObject {
[key: string]: JSONValue | JSONObject | JSONObject[];
}
export type JSONSchemaType = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object';
/**
* fork from vscode
*/
export interface IJSONSchema {
id?: string;
$id?: string;
$schema?: string;
type?: JSONSchemaType | JSONSchemaType[];
title?: string;
default?: any;
definitions?: IJSONSchemaMap;
description?: string;
properties?: IJSONSchemaMap;
patternProperties?: IJSONSchemaMap;
additionalProperties?: boolean | IJSONSchema;
minProperties?: number;
maxProperties?: number;
dependencies?: IJSONSchemaMap | { [prop: string]: string[] };
items?: IJSONSchema | IJSONSchema[];
minItems?: number;
maxItems?: number;
uniqueItems?: boolean;
additionalItems?: boolean | IJSONSchema;
pattern?: string;
minLength?: number;
maxLength?: number;
minimum?: number;
maximum?: number;
exclusiveMinimum?: boolean | number;
exclusiveMaximum?: boolean | number;
multipleOf?: number;
required?: string[];
$ref?: string;
anyOf?: IJSONSchema[];
allOf?: IJSONSchema[];
oneOf?: IJSONSchema[];
not?: IJSONSchema;
enum?: any[];
format?: string;
// schema draft 06
const?: any;
contains?: IJSONSchema;
propertyNames?: IJSONSchema;
examples?: any[];
// schema draft 07
$comment?: string;
if?: IJSONSchema;
then?: IJSONSchema;
else?: IJSONSchema;
// schema 2019-09
unevaluatedProperties?: boolean | IJSONSchema;
unevaluatedItems?: boolean | IJSONSchema;
minContains?: number;
maxContains?: number;
deprecated?: boolean;
dependentRequired?: { [prop: string]: string[] };
dependentSchemas?: IJSONSchemaMap;
$defs?: { [name: string]: IJSONSchema };
$anchor?: string;
$recursiveRef?: string;
$recursiveAnchor?: string;
$vocabulary?: any;
// schema 2020-12
prefixItems?: IJSONSchema[];
$dynamicRef?: string;
$dynamicAnchor?: string;
// internal extensions
errorMessage?: string;
}
export interface IJSONSchemaMap {
[name: string]: IJSONSchema;
}

View File

@ -2,9 +2,14 @@
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec
*
*/
import { JSONObject, JSONValue } from '../json';
import { Reference } from './material-spec';
export type JSONValue = number | string | boolean | null;
export interface JSONObject {
[key: string]: JSONValue | JSONObject | JSONObject[];
}
/**
* https://lowcode-engine.cn/site/docs/specs/lowcode-spec#2-%E5%8D%8F%E8%AE%AE%E7%BB%93%E6%9E%84
*

View File

@ -44,3 +44,83 @@ export class AutoOpenBarrier extends Barrier {
super.open();
}
}
const canceledName = 'Canceled';
/**
* Checks if the given error is a promise in canceled state
*/
export function isCancellationError(error: any): boolean {
if (error instanceof CancellationError) {
return true;
}
return error instanceof Error && error.name === canceledName && error.message === canceledName;
}
export class CancellationError extends Error {
constructor() {
super(canceledName);
this.name = this.message;
}
}
export type ValueCallback<T = unknown> = (value: T | Promise<T>) => void;
const enum DeferredOutcome {
Resolved,
Rejected,
}
/**
* Creates a promise whose resolution or rejection can be controlled imperatively.
*/
export class DeferredPromise<T> {
private completeCallback!: ValueCallback<T>;
private errorCallback!: (err: unknown) => void;
private outcome?: { outcome: DeferredOutcome.Rejected; value: any } | { outcome: DeferredOutcome.Resolved; value: T };
public get isRejected() {
return this.outcome?.outcome === DeferredOutcome.Rejected;
}
public get isResolved() {
return this.outcome?.outcome === DeferredOutcome.Resolved;
}
public get isSettled() {
return !!this.outcome;
}
public get value() {
return this.outcome?.outcome === DeferredOutcome.Resolved ? this.outcome?.value : undefined;
}
public readonly p: Promise<T>;
constructor() {
this.p = new Promise<T>((c, e) => {
this.completeCallback = c;
this.errorCallback = e;
});
}
public complete(value: T) {
return new Promise<void>((resolve) => {
this.completeCallback(value);
this.outcome = { outcome: DeferredOutcome.Resolved, value };
resolve();
});
}
public error(err: unknown) {
return new Promise<void>((resolve) => {
this.errorCallback(err);
this.outcome = { outcome: DeferredOutcome.Rejected, value: err };
resolve();
});
}
public cancel() {
return this.error(new CancellationError());
}
}

View File

@ -9,3 +9,4 @@ export * from './async';
export * from './node';
export * from './resource';
export * from './functional';
export * from './timer';

View File

@ -11,3 +11,11 @@ export function illegalArgument(name?: string): Error {
return new Error('Illegal argument');
}
}
export function illegalState(name?: string): Error {
if (name) {
return new Error(`Illegal state: ${name}`);
} else {
return new Error('Illegal state');
}
}

View File

@ -0,0 +1,86 @@
import { IDisposable, toDisposable } from '../common';
export class TimeoutTimer implements IDisposable {
private _token: any;
private _isDisposed = false;
constructor();
constructor(runner: () => void, timeout: number);
constructor(runner?: () => void, timeout?: number) {
this._token = -1;
if (typeof runner === 'function' && typeof timeout === 'number') {
this.setIfNotSet(runner, timeout);
}
}
dispose(): void {
this.cancel();
this._isDisposed = true;
}
cancel(): void {
if (this._token !== -1) {
clearTimeout(this._token);
this._token = -1;
}
}
cancelAndSet(runner: () => void, timeout: number): void {
if (this._isDisposed) {
throw Error(`Calling 'cancelAndSet' on a disposed TimeoutTimer`);
}
this.cancel();
this._token = setTimeout(() => {
this._token = -1;
runner();
}, timeout);
}
setIfNotSet(runner: () => void, timeout: number): void {
if (this._isDisposed) {
throw Error(`Calling 'setIfNotSet' on a disposed TimeoutTimer`);
}
if (this._token !== -1) {
// timer is already set
return;
}
this._token = setTimeout(() => {
this._token = -1;
runner();
}, timeout);
}
}
export class IntervalTimer implements IDisposable {
private disposable: IDisposable | undefined = undefined;
private isDisposed = false;
cancel(): void {
this.disposable?.dispose();
this.disposable = undefined;
}
cancelAndSet(runner: () => void, interval: number): void {
if (this.isDisposed) {
throw new Error(`Calling 'cancelAndSet' on a disposed IntervalTimer`);
}
this.cancel();
const handle = setInterval(() => {
runner();
}, interval);
this.disposable = toDisposable(() => {
clearInterval(handle);
this.disposable = undefined;
});
}
dispose(): void {
this.cancel();
this.isDisposed = true;
}
}