feat: add keybind services
This commit is contained in:
parent
9e08c2a224
commit
46139fc155
|
@ -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);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export interface IContentEditor {
|
||||
load(content: string): Promise<void>;
|
||||
|
||||
export(): Promise<string>;
|
||||
|
||||
close(): void;
|
||||
|
||||
send(channel: string, ...args: any[]): void;
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './extension';
|
||||
export * from './extensionService';
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ export * from './resource';
|
|||
export * from './command';
|
||||
export * from './workspace';
|
||||
export * from './common';
|
||||
export * from './keybinding';
|
||||
|
||||
// test
|
||||
export * from './main';
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './jsonSchema';
|
||||
export * from './jsonSchemaRegistry';
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -0,0 +1,2 @@
|
|||
export * from './keybinding';
|
||||
export * from './keybindingService';
|
|
@ -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');
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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`;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: '+',
|
||||
},
|
||||
);
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export interface IWindowManagemant {}
|
|
@ -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 {}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -11,6 +11,7 @@ export class CtorDescriptor<T> {
|
|||
constructor(
|
||||
readonly ctor: Constructor<T>,
|
||||
readonly staticArguments: any[] = [],
|
||||
readonly supportsDelayedInstantiation: boolean = false,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
export * from './specs';
|
||||
export * from './json';
|
||||
export * from './common';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
* 应用协议
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,3 +9,4 @@ export * from './async';
|
|||
export * from './node';
|
||||
export * from './resource';
|
||||
export * from './functional';
|
||||
export * from './timer';
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue