const {
	defaultKeymap,
	keymap,
	redo,
	redoSelection,
	undo,
	undoSelection,
	EditorView,
	Extension,
	KeyBinding,
	Prec,
	StateEffect
} = require( 'ext.CodeMirror.v6.lib' );

/**
 * Key bindings for CodeMirror.
 *
 * This class provides key bindings for CodeMirror, including a help dialog
 * that lists the available shortcuts, accessible via `Ctrl`-`Shift`-`/`.
 *
 * Additional {@link CodeMirrorKeyBinding key bindings} can be registered using
 * {@link CodeMirrorKeymap#registerKeyBinding registerKeyBinding()}. This will
 * take effect in the editor immediately.
 *
 * To document key bindings in the help dialog, use
 * {@link CodeMirrorKeymap#registerKeyBindingHelp registerKeyBindingHelp()}, which
 * can optionally also enable the key binding in the editor.
 *
 * Both methods require an {@link EditorView}, which is only accessible after
 * CodeMirror has been initialized. For dynamically added key bindings, use the
 * {@link event:'ext.CodeMirror.ready' ext.CodeMirror.ready} hook to have access to
 * the {@link CodeMirror} instance after initialization, which will have the
 * {@link CodeMirror#view CodeMirror.view} property set.
 *
 * @example
 * mw.hook( 'ext.CodeMirror.ready' ).add( ( $_dom, cm ) => {
 *   const myKeybinding = {
 *     key: 'F1',
 *     run: () => {
 *       // Do something when F1 is pressed.
 *     }
 *   };
 *
 *   // Register the key binding in the editor.
 *   cm.registerKeyBinding( myKeybinding, cm.view );
 *
 *   // Or, register only in the help dialog.
 *   cm.registerKeyBindingHelp( 'other', 'myKeybinding', myKeybinding );
 *
 *   // Or, both.
 *   cm.registerKeyBindingHelp( 'other', 'myKeybinding', myKeybinding, cm.view );
 * } );
 */
class CodeMirrorKeymap {

	constructor() {
		/** @type {HTMLDivElement} */
		this.dialog = null;

		/** @type {Function} */
		this.keydownListener = null;

		/**
		 * Platform name, used for platform-specific key bindings.
		 * This uses the same platform-detection logic as CodeMirror.
		 * One of `mac`, `win`, `linux`, or a blank string.
		 *
		 * @type {string}
		 */
		this.platform = '';
		const nav = typeof navigator !== 'undefined' ?
			navigator :
			{ platform: '', vendor: '', userAgent: '', maxTouchPoints: 0 };
		if ( nav.platform.match( /Mac/ ) || (
			/Apple Computer/.test( nav.vendor ) && (
				/Mobile\/\w+/.test( nav.userAgent ) || nav.maxTouchPoints > 2
			)
		) ) {
			this.platform = 'mac';
		} else if ( nav.platform.match( /Win/ ) ) {
			this.platform = 'win';
		} else if ( nav.platform.match( /Linux|X11/ ) ) {
			this.platform = 'linux';
		}

		/**
		 * {@link CodeMirrorPreferences} instance, once available.
		 * Used to dynamically show/hide key bindings based on user preferences.
		 *
		 * @type {CodeMirrorPreferences|null}
		 * @private
		 */
		this.preferences = null;
		mw.hook( 'ext.CodeMirror.preferences.ready' ).add( ( preferences ) => {
			this.preferences = preferences;
		} );

		/**
		 * Registry of key bindings we want to advertise in the help dialog.
		 * The outer keys are the section within the dialog. The objects therein are for
		 * each mapping (command) we want to show, keyed by tool. The value for each is
		 * a {@link CodeMirrorKeyBinding} object, or an array of them.
		 *
		 * @type {Object<Object<CodeMirrorKeyBinding>>|Object<Object<CodeMirrorKeyBinding[]>>}
		 * @property {Object<CodeMirrorKeyBinding>|Object<CodeMirrorKeyBinding[]>} textStyling
		 * @property {Object<CodeMirrorKeyBinding>|Object<CodeMirrorKeyBinding[]>} history
		 * @property {Object<CodeMirrorKeyBinding>|Object<CodeMirrorKeyBinding[]>} paragraph
		 * @property {Object<CodeMirrorKeyBinding>|Object<CodeMirrorKeyBinding[]>} search
		 * @property {Object<CodeMirrorKeyBinding>|Object<CodeMirrorKeyBinding[]>} insert
		 * @property {Object<CodeMirrorKeyBinding>|Object<CodeMirrorKeyBinding[]>} other
		 */
		this.keymapHelpRegistry = {
			// Empty values are placeholders for MW-specific key bindings
			// so that they appear in the correct order in the help dialog.
			textStyling: {},
			// Use our own history keymap since it differs greatly from stock CodeMirror.
			history: {
				undo: {
					key: 'Mod-z',
					run: undo,
					preventDefault: true
				},
				redo: [
					{
						key: 'Mod-y',
						mac: 'Mod-Shift-z',
						run: redo,
						preventDefault: true
					}, {
						linux: 'Ctrl-Shift-z',
						// T365072
						win: 'Ctrl-Shift-z',
						run: redo,
						preventDefault: true
					}
				],
				// TODO: Find something that works for Mac.
				undoSelection: {
					win: 'Meta-u',
					linux: 'Meta-u',
					run: undoSelection,
					preventDefault: true
				},
				redoSelection: {
					win: 'Meta-Shift-u',
					linux: 'Meta-Shift-u',
					run: redoSelection,
					preventDefault: true
				}
			},
			paragraph: {
				indent: { key: 'Mod-[' },
				outdent: { key: 'Mod-]' }
			},
			search: {
				find: { key: 'Mod-f' },
				findNext: {
					key: 'Mod-g',
					msg: mw.msg( 'codemirror-next' ),
					aliases: [ 'F3' ]
				},
				findPrev: { key: 'Shift-Mod-g', msg: mw.msg( 'codemirror-previous' ) },
				selectNext: { key: 'Mod-d' },
				gotoLine: { key: 'Mod-Alt-g', msg: mw.msg( 'codemirror-goto-line' ) }
			},
			insert: {
				blankLine: { key: 'Mod-Enter' }
			},
			codeFolding: {
				fold: {
					key: 'Ctrl-Shift-[',
					mac: 'Cmd-Alt-['
				},
				unfold: {
					key: 'Ctrl-Shift-]',
					mac: 'Cmd-Alt-]'
				},
				foldAll: { key: 'Ctrl-Alt-[' },
				unfoldAll: { key: 'Ctrl-Alt-]' }
			},
			autocomplete: {},
			other: {
				moveLine: {
					key: 'Alt-↑/↓',
					msg: mw.msg( 'codemirror-keymap-moveline' )
				},
				copyLine: {
					key: 'Alt-Shift-↑/↓',
					msg: mw.msg( 'codemirror-keymap-copyline' )
				},
				direction: { key: 'Mod-Shift-x' },
				preferences: {
					key: 'Mod-Shift-,',
					msg: mw.msg( 'codemirror-keymap-preferences' )
				},
				help: {
					key: 'Ctrl-Shift-/',
					run: this.showHelpDialog.bind( this ),
					preventDefault: true
				}
			}
		};

		/**
		 * Map of descriptions of cursor modifiers (e.g. multi-cursor, crosshair).
		 * (Un)set elements directly on this map to document new cursor modifiers.
		 *
		 * Keys are unique names, values are the descriptions.
		 *
		 * @type {Map<string, string>}
		 */
		this.cursorModifiers = new Map( [
			[ 'multiCursor', mw.msg( 'codemirror-keymap-multicursor', this.getShortcutHtml( 'Mod' ).outerHTML ) ],
			[ 'crosshair', mw.msg( 'codemirror-keymap-crosshair', this.getShortcutHtml( 'Alt' ).outerHTML ) ]
		] );

		// Use mw.hook to add a click listener to the keymap help button.
		mw.hook( 'ext.CodeMirror.preferences.display' ).add( ( container ) => {
			container.querySelector( '.cm-mw-panel--kbd-help' ).addEventListener( 'click',
				() => this.showHelpDialog()
			);
		} );
	}

	/**
	 * Show the keymap help dialog.
	 *
	 * This implements the Codex Dialog component. See https://w.wiki/CcWY
	 *
	 * @return {boolean}
	 */
	showHelpDialog() {
		/**
		 * Fired when the keymap help dialog is opened.
		 *
		 * @event CodeMirror~ext.CodeMirror.keymap
		 * @internal
		 */
		mw.hook( 'ext.CodeMirror.keymap' ).fire();

		if ( this.dialog ) {
			this.animateDialog( true );
			return true;
		}

		const backdrop = document.createElement( 'div' );
		backdrop.classList.add(
			'cdx-dialog-backdrop',
			// These classes are used by the fade animation.
			// We always want them enabled, since dialog content is not interactable
			// and thus we don't need to worry about conflicting styles.
			'cdx-dialog-fade-enter-active',
			'cm-mw-dialog-backdrop',
			'cm-mw-dialog--hidden'
		);
		const tabindex = document.createElement( 'div' );
		tabindex.tabIndex = 0;
		backdrop.appendChild( tabindex );

		const dialog = document.createElement( 'div' );
		dialog.classList.add( 'cdx-dialog', 'cm-mw-dialog', 'cm-mw-keymap-dialog' );
		backdrop.appendChild( dialog );
		backdrop.addEventListener( 'click', ( e ) => {
			if ( e.target === backdrop ) {
				this.animateDialog( false );
			}
		} );

		const header = document.createElement( 'header' );
		header.classList.add( 'cdx-dialog__header', 'cdx-dialog__header--default' );
		const headerTitleGroup = document.createElement( 'div' );
		headerTitleGroup.classList.add( 'cdx-dialog__header__title-group' );
		const h2 = document.createElement( 'h2' );
		h2.id = 'cdx-dialog__header__title-group';
		h2.classList.add( 'cdx-dialog__header__title' );
		h2.textContent = mw.msg( 'codemirror-keymap-help-title' );
		headerTitleGroup.appendChild( h2 );
		header.appendChild( headerTitleGroup );

		const closeBtn = document.createElement( 'button' );
		closeBtn.type = 'button';
		closeBtn.classList.add(
			'cdx-button',
			'cdx-button',
			'cdx-button--action-default',
			'cdx-button--weight-quiet',
			'cdx-button--size-medium',
			'cdx-button--icon-only',
			'cdx-dialog__header__close-button',
			'cdx-dialog__header__close'
		);
		closeBtn.setAttribute( 'aria-label', mw.msg( 'codemirror-keymap-help-close' ) );
		const cdxIcon = document.createElement( 'span' );
		cdxIcon.classList.add( 'cdx-button__icon', 'cm-mw-icon--close' );
		closeBtn.appendChild( cdxIcon );
		closeBtn.addEventListener( 'click', this.animateDialog.bind( this, false ) );
		header.appendChild( closeBtn );
		dialog.appendChild( header );

		const focusTrap = document.createElement( 'div' );
		focusTrap.tabIndex = -1;
		dialog.appendChild( focusTrap );

		const body = document.createElement( 'div' );
		body.classList.add( 'cdx-dialog__body' );
		this.setHelpDialogBody( body );
		dialog.appendChild( body );

		backdrop.appendChild( tabindex.cloneNode() );

		this.dialog = backdrop;

		document.body.appendChild( backdrop );
		this.animateDialog( true );

		return true;
	}

	/**
	 * Fade the dialog in or out, adjusting for scrollbar widths to prevent shifting of content.
	 * This almost fully mimics the way the Codex handles its Dialog component, with the exception
	 * that we don't force a focus trap, nor do we set aria-hidden on other elements in the DOM.
	 * This is to keep our implementation simple until something like T382532 is realized.
	 *
	 * @param {boolean} open
	 * @private
	 */
	animateDialog( open = false ) {
		document.activeElement.blur();
		// Must be unhidden in order to animate.
		this.dialog.classList.remove( 'cm-mw-dialog--hidden' );
		// When the transition ends, hide or show the dialog.
		this.dialog.addEventListener( 'transitionend', () => {
			this.dialog.classList.toggle( 'cm-mw-dialog--hidden', !open );
			if ( open ) {
				this.dialog.querySelector( '[tabindex="0"]' ).focus();
				// Determine the width of the scrollbar and compensate for it if necessary
				const scrollWidth = window.innerWidth - document.documentElement.clientWidth;
				document.documentElement.style.setProperty( 'margin-right', `${ scrollWidth }px` );
			} else {
				document.documentElement.style.removeProperty( 'margin-right' );
			}
			// Toggle a class on <body> to prevent scrolling
			document.body.classList.toggle( 'cdx-dialog-open', open );
		}, { once: true } );
		// Animates the dialog in or out.
		// Use setTimeout() with slight delay to allow rendering threads to catch up.
		setTimeout( () => {
			this.dialog.classList.toggle( 'cm-mw-dialog-animate-show', open );
		}, 50 );

		// Add or remove the keydown listener.
		if ( open && !this.keydownListener ) {
			this.keydownListener = ( e ) => {
				if ( e.key === 'Escape' && !this.dialog.classList.contains( 'cm-mw-dialog--hidden' ) ) {
					this.animateDialog( false );
				}
			};
			document.body.addEventListener( 'keydown', this.keydownListener );
		} else if ( !open && this.keydownListener ) {
			document.body.removeEventListener( 'keydown', this.keydownListener );
			this.keydownListener = null;
		}
	}

	/**
	 * @param {HTMLDivElement} body
	 * @private
	 */
	setHelpDialogBody( body ) {
		const keybindingsContainer = document.createElement( 'section' );
		keybindingsContainer.classList.add( 'cm-mw-keymap-dialog__keybindings' );
		const sections = Object.keys( this.keymapHelpRegistry );
		// Count of non-empty sections.
		let sectionCount = 0;
		for ( const section of sections ) {
			const commands = Object.keys( this.keymapHelpRegistry[ section ] );
			if ( !commands.length ) {
				continue;
			}
			sectionCount++;
			const sectionEl = document.createElement( 'div' );
			// CSS class names known to be used here include but are not limited to:
			// * cm-mw-keymap-section--autocomplete
			// * cm-mw-keymap-section--codefolding
			// * cm-mw-keymap-section--history
			// * cm-mw-keymap-section--insert
			// * cm-mw-keymap-section--other
			// * cm-mw-keymap-section--paragraph
			// * cm-mw-keymap-section--search
			// * cm-mw-keymap-section--textstyling
			sectionEl.className = `cm-mw-keymap-section cm-mw-keymap-section--${ section.toLowerCase() }`;
			this.setDisplayFromPreference( section, sectionEl );

			const heading = document.createElement( 'h4' );
			// Messages known to be used here include but are not limited to:
			// * codemirror-keymap-autocomplete
			// * codemirror-keymap-codefolding
			// * codemirror-keymap-history
			// * codemirror-keymap-insert
			// * codemirror-keymap-other
			// * codemirror-keymap-paragraph
			// * codemirror-keymap-search
			// * codemirror-keymap-textstyling
			heading.textContent = mw.msg( `codemirror-keymap-${ section.toLowerCase() }` );
			const dl = document.createElement( 'dl' );
			dl.classList.add( 'cm-mw-keymap-list' );
			for ( const command of commands ) {
				const keyBinding = this.reduceKeyBindings(
					this.keymapHelpRegistry[ section ][ command ]
				);

				// Skip if the binding has a null `msg` or if it doesn't apply to this platform.
				if ( keyBinding.msg === null ||
					( !keyBinding[ this.platform ] && !keyBinding.key )
				) {
					continue;
				}

				// Create <dt> element containing the key binding(s).
				const dt = document.createElement( 'dt' );
				const keys = [
					keyBinding[ this.platform ] || keyBinding.key,
					...( keyBinding.aliases || [] )
				];
				for ( const key of keys ) {
					dt.appendChild( this.getShortcutHtml( key ) );
				}
				dl.appendChild( dt );

				// Create <dd> element containing the command name/description.
				const dd = document.createElement( 'dd' );
				// Set the message for the CodeMirrorKeyBinding. If the 'msg' property
				// is set, use that, otherwise use the key 'codemirror-keymap-<command>'.
				dd.textContent = keyBinding.msg ||
					// Messages known to be used here include but are not limited to:
					// * codemirror-keymap-blockquote
					// * codemirror-keymap-bold
					// * codemirror-keymap-comment
					// * codemirror-keymap-computercode
					// * codemirror-keymap-find
					// * codemirror-keymap-findnext
					// * codemirror-keymap-findprev
					// * codemirror-keymap-gotoline
					// * codemirror-keymap-heading
					// * codemirror-keymap-indent
					// * codemirror-keymap-italic
					// * codemirror-keymap-link
					// * codemirror-keymap-outdent
					// * codemirror-keymap-preformatted
					// * codemirror-keymap-redo
					// * codemirror-keymap-redoselection
					// * codemirror-keymap-reference
					// * codemirror-keymap-selectnext
					// * codemirror-keymap-strikethrough
					// * codemirror-keymap-subscript
					// * codemirror-keymap-superscript
					// * codemirror-keymap-underline
					// * codemirror-keymap-undo
					// * codemirror-keymap-undoselection
					mw.msg( `codemirror-keymap-${ command.toLowerCase() }` );
				dl.appendChild( dd );

				this.setDisplayFromPreference( command, dt );
				this.setDisplayFromPreference( command, dd );
			}
			sectionEl.appendChild( heading );
			sectionEl.appendChild( dl );
			keybindingsContainer.appendChild( sectionEl );
		}

		// If there are no four or fewer sections, show only two columns.
		// This happens if the LanguageSupport extension did not
		// register any additional key bindings, throwing off the styling.
		if ( sectionCount <= 4 ) {
			keybindingsContainer.classList.add( 'cm-mw-keymap-dialog__keybindings--two-col' );
		}

		// Cursor modifiers.
		const cursorSection = document.createElement( 'section' );
		cursorSection.classList.add( 'cm-mw-keymap-dialog__cursor' );
		const h4 = document.createElement( 'h4' );
		h4.textContent = mw.msg( 'codemirror-keymap-cursor-modifiers' );
		cursorSection.appendChild( h4 );

		const ul = document.createElement( 'ul' );
		for ( const [ cursorModName, description ] of this.cursorModifiers ) {
			const li = document.createElement( 'li' );
			li.innerHTML = description;
			ul.appendChild( li );
			this.setDisplayFromPreference( cursorModName, li );
		}
		cursorSection.appendChild( ul );

		body.appendChild( keybindingsContainer );
		body.appendChild( cursorSection );
	}

	/**
	 * Reduce the given key bindings into a single CodeMirrorKeyBinding,
	 * with additional applicable keys under the aliases property.
	 *
	 * @param {CodeMirrorKeyBinding|CodeMirrorKeyBinding[]} given
	 * @return {CodeMirrorKeyBinding}
	 * @private
	 */
	reduceKeyBindings( given ) {
		/** @type {CodeMirrorKeyBinding[]} */
		const keyBindings = [].concat.apply( [], new Array( given ) );
		return keyBindings.reduce( ( acc, kb ) => {
			const relevantKey = kb[ this.platform ] || kb.key;
			if ( !acc ) {
				acc = kb;
			} else if ( relevantKey ) {
				acc.aliases = [ ...acc.aliases || [], relevantKey ];
			}
			return acc;
		} );
	}

	/**
	 * Set the display of an HTMLElement based on a preference. This uses the
	 * `ext.CodeMirror.preferences.change` hook to update the display when the preference changes.
	 *
	 * @param {string} prefName
	 * @param {HTMLElement} el
	 * @internal
	 * @private
	 */
	setDisplayFromPreference( prefName, el ) {
		const isRegistered = this.preferences && !!this.preferences.extensionRegistry[ prefName ];
		const currentlyEnabled = !isRegistered || this.preferences.getPreference( prefName );
		el.style.display = currentlyEnabled ? '' : 'none';

		if ( isRegistered ) {
			mw.hook( 'ext.CodeMirror.preferences.change' ).add( ( pref, enabled ) => {
				if ( pref === prefName ) {
					el.style.display = enabled ? '' : 'none';
				}
			} );
		}
	}

	/**
	 * Get the `<kbd>` HTML for a key binding sequence.
	 * This takes into account platform-specific key names.
	 *
	 * @param {string} keyBindingSequence
	 * @return {HTMLElement}
	 * @internal
	 */
	getShortcutHtml( keyBindingSequence ) {
		const outerKbd = document.createElement( 'kbd' );
		outerKbd.classList.add( 'cm-mw-keymap-key' );
		const keys = keyBindingSequence.split( '-' )
			.map( ( key ) => {
				// Normalize capitalized keys to include Shift in the help dialog.
				if ( key.length === 1 && key !== key.toLowerCase() ) {
					return `Shift-${ key.toLowerCase() }`;
				}
				return key;
			} )
			.join( '-' )
			.split( '-' );
		// Build the <kbd> elements.
		keys.forEach( ( key, index ) => {
			// Normalize Cmd to ⌘ Cmd on macOS, and Ctrl on other platforms.
			if ( key.toLowerCase() === 'mod' ) {
				key = this.platform === 'mac' ? '⌘ Cmd' : 'Ctrl';
			} else if ( key.toLowerCase() === 'cmd' ) {
				key = '⌘ Cmd';
			} else if ( key.toLowerCase() === 'alt' && this.platform === 'mac' ) {
				key = '⌥ Option';
			} else if ( key === 'ArrowUp' ) {
				key = '↑';
			} else if ( key === 'ArrowDown' ) {
				key = '↓';
			}
			const kbd = document.createElement( 'kbd' );
			kbd.textContent = key;
			outerKbd.appendChild( kbd );
			if ( index < keys.length - 1 ) {
				const plus = document.createTextNode( '+' );
				outerKbd.appendChild( plus );
			}
		} );
		return outerKbd;
	}

	/**
	 * @typedef {Object} CodeMirrorKeymap~CodeMirrorKeyBinding
	 * @extends KeyBinding
	 * @description Extends CodeMirror's {@link KeyBinding} interface with additional properties.
	 * @see https://codemirror.net/docs/ref/#view.KeyBinding
	 * @property {string} [key] The key binding sequence, i.e. `Mod-Shift-/`. Any applicable
	 *   platform-specific key bindings will take precedence over this.
	 * @property {string} [mac] The key binding sequence to use specifically on macOS.
	 * @property {string} [win] The key binding sequence to use specifically on Windows
	 * @property {string} [linux] The key binding sequence to use specifically on Linux.
	 * @property {string[]} [aliases] Additional key binding sequences that trigger the command.
	 * @property {Command} [run] The function to run when the key binding is triggered.
	 * @property {boolean} [preventDefault=false] Prevent the default behavior of the key binding.
	 * @property {string|null} [msg] Override the auto-generated message in the help dialog.
	 *   If not provided, a message key will be generated of the form `codemirror-keymap-<command>`
	 *   and rendered with {@link mw.msg}. Use `null` to exclude the command from the help dialog.
	 * @property {Function} [prec={@link Prec.default}] The precedence function to use for the key
	 *   binding. See {@link Prec} for details.
	 */

	/**
	 * Register a key binding in the help dialog. If a `view` is passed in and the key binding
	 * has a `run` function, the key binding will also be enabled in the editor.
	 * If no `run` function exists, a key binding will only be documented in
	 * the help dialog and is presumed to be implemented elsewhere.
	 *
	 * If the `section` or `tool` matches the name of an `Extension` registered with
	 * {@link CodeMirrorPreferences}, a help entry is only shown when the preference is enabled.
	 *
	 * @param {string} section The section in the help dialog where the binding should be listed.
	 * @param {string} tool Transformed into a message key like `'codemirror-keymap-<tool>'`.
	 * @param {CodeMirrorKeyBinding|null} [keyBinding=null]
	 *   `null` if this is a documentation-only.
	 * @param {EditorView} [view=null]
	 *   If provided, the key binding will be enabled as an Extension in the editor.
	 */
	registerKeyBindingHelp( section, tool, keyBinding = null, view = null ) {
		this.keymapHelpRegistry[ section ] = this.keymapHelpRegistry[ section ] || {};
		this.keymapHelpRegistry[ section ][ tool ] = keyBinding;
		if ( keyBinding.run && view ) {
			this.registerKeyBinding( keyBinding, view );
		}
	}

	/**
	 * Register a key binding with CodeMirror.
	 *
	 * @param {CodeMirrorKeyBinding} keyBinding The key binding to register.
	 * @param {EditorView} view The `EditorView` to register the key binding(s) with.
	 * @stable
	 */
	registerKeyBinding( keyBinding, view ) {
		const precFn = keyBinding.prec || Prec.default;
		const extension = precFn( keymap.of( keyBinding ) );
		view.dispatch( {
			effects: StateEffect.appendConfig.of( extension )
		} );
	}

	/**
	 * @return {Extension[]}
	 */
	get extension() {
		const extensions = [
			// Default keymap.
			keymap.of( defaultKeymap )
		];

		// Add Commands (key bindings with `run`) defined in this.keymapHelpRegistry.
		for ( const section in this.keymapHelpRegistry ) {
			for ( const command in this.keymapHelpRegistry[ section ] ) {
				/** @type {CodeMirrorKeyBinding[]} */
				const keyBindings = [].concat.apply( [],
					new Array( this.keymapHelpRegistry[ section ][ command ] )
				);
				for ( const keyBinding of keyBindings ) {
					if ( keyBinding.run ) {
						extensions.push( keymap.of( keyBinding ) );
					}
				}
			}
		}

		return extensions;
	}
}

module.exports = CodeMirrorKeymap;