This is an automated email from the ASF dual-hosted git repository.

chanholee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 9aac655ef9 [ZEPPELIN-6332] Unify keyboard event handling between 
Angular and Monaco editor
9aac655ef9 is described below

commit 9aac655ef9fce3cdb91f4fac3dbc4a6fff9c3d1e
Author: ChanHo Lee <[email protected]>
AuthorDate: Tue Oct 7 11:44:34 2025 +0900

    [ZEPPELIN-6332] Unify keyboard event handling between Angular and Monaco 
editor
    
    ### What is this PR for?
    
    Keyboard event handling logic is currently split between in 
`paragraph.component.ts`(Angular) and `code-editor.component.ts` (Monaco 
editor) in new UI.
    
    Since both Angular and the Monaco editor capture key events, the auctual 
handler depends on where the focus is.
    
    Refactoring this logic would improve cohesion and type safety, making the 
code more readable and less prone to bugs.
    
    #### Changes
    
    ##### Extracted keybinding logic into a separate module
    - Keybinding logic involves low-level event handling details, but it is not 
a core responsibility of the components.
    - To improve separation of concerns and reusability, I extracted the logic 
into a dedicated module: `<at>zeppelin/key-binding`.
    
    ##### Added a keybinding coverter
    - Introduced a converter from Angular keybindings to Monaco editor 
keybindings.
    - This allows keybindings defined once  in `shortcuts-map.ts` to be reused 
for both Angular and Monaco editor handlers.
    
    ##### Unified keybinding handling in a single class
    - Initialization of keybinding handlers for both Angular and Monaco editor 
is now managed by a new `KeyBinder` class.
    - A `KeyBinder` instance is created in each `ParagraphComponent`.
    - Angular handlers are registerd during `ParagraphComponent` 
initialization, while Monaco handlers are registered when the editor is 
initialized (triggered by the `initKeyBindings` emitter in 
`NotebookParagraphCodeEditorComponent`).
    - All handlers simpley emit the appropriate action key to an RxJS `Subject` 
within `KeyBinder`.
    
    ##### Categorized handler for each action between Angular and Monaco editor 
at type level.
    - All actions are mapped to corresponding handler method name via 
`ParagraphActionToHandlerName`. Each actions must be explicitly specified as a 
key at the type level, and each value must be one of the method names from 
`NotebookParagraphKeyboardEventHandler`.
    - The `MonacoKeyboardEventHandler` type defines method names for some 
actions, while the rest are defined in `AngularKeyboardEventHandler`. Both are 
subtypes of `NotebookParagraphKeyboardEventHandler` and the methods in each 
type are mutually exclusive.
    - `NotebookParagraphCodeEditorComponent` implements the Monaco handler, and 
`ParagraphComponent` implements the Angular handler.
    - Both handler implementation receives an `action` parameter via` 
handleKeyEvent(action, event)`, then look up the corresponding method in 
`ParagraphActionToHandlerName` using the `action` value.
    
    
    ### What type of PR is it?
    Refactoring
    
    ### What is the Jira issue?
    [ZEPPELIN-6332](https://issues.apache.org/jira/browse/ZEPPELIN-6332)
    
    ### How should this be tested?
    Since E2E tests are not added, we could check key shortcuts manually while 
running Zeppelin.
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    
    Closes #5080 from tbonelee/override-default-keybindings.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 zeppelin-web-angular/src/app/key-binding/index.ts  |  13 +
 .../src/app/key-binding/key-binder.ts              |  73 +++++
 .../src/app/key-binding/key-code-converter.ts      | 125 +++++++++
 .../notebook-paragraph-keyboard-event-handler.ts   | 101 +++++++
 .../src/app/key-binding/paragraph-actions.ts       |  39 +++
 .../src/app/key-binding/public-api.ts              |  16 ++
 .../src/app/key-binding/shortcuts-map.ts           |  47 ++++
 .../paragraph/code-editor/code-editor.component.ts | 109 +++++---
 .../notebook/paragraph/paragraph.component.html    |   1 +
 .../notebook/paragraph/paragraph.component.ts      | 297 ++++++++++-----------
 .../src/app/services/shortcut.service.ts           |  58 ----
 11 files changed, 616 insertions(+), 263 deletions(-)

diff --git a/zeppelin-web-angular/src/app/key-binding/index.ts 
b/zeppelin-web-angular/src/app/key-binding/index.ts
new file mode 100644
index 0000000000..49e4740442
--- /dev/null
+++ b/zeppelin-web-angular/src/app/key-binding/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './public-api';
diff --git a/zeppelin-web-angular/src/app/key-binding/key-binder.ts 
b/zeppelin-web-angular/src/app/key-binding/key-binder.ts
new file mode 100644
index 0000000000..c21601affc
--- /dev/null
+++ b/zeppelin-web-angular/src/app/key-binding/key-binder.ts
@@ -0,0 +1,73 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ElementRef } from '@angular/core';
+import { editor as MonacoEditor } from 'monaco-editor';
+import { from, Subject } from 'rxjs';
+import { map, mergeMap, takeUntil } from 'rxjs/operators';
+
+import { ShortcutService } from '@zeppelin/services';
+import { castArray, chain, isNil } from 'lodash';
+import { KeyCodeConverter } from './key-code-converter';
+import { ParagraphActions } from './paragraph-actions';
+import { ShortcutsMap } from './shortcuts-map';
+
+export class KeyBinder {
+  private events$ = new Subject<{
+    action: ParagraphActions;
+    event: KeyboardEvent | null;
+  }>();
+
+  constructor(
+    private destroySubject: Subject<unknown>,
+    private host: ElementRef,
+    private shortcutService: ShortcutService
+  ) {}
+
+  keyEvent() {
+    return this.events$.asObservable();
+  }
+
+  initKeyBindingsOnAngular() {
+    const shortcutService = 
this.shortcutService.forkByElement(this.host.nativeElement);
+    from(Object.entries(ShortcutsMap))
+      .pipe(
+        mergeMap(([action, keys]) =>
+          from(Array.isArray(keys) ? keys : [keys]).pipe(
+            mergeMap(key =>
+              shortcutService
+                .bindShortcut({ keybindings: key })
+                .pipe(map(({ event }) => ({ action: action as 
ParagraphActions, event })))
+            )
+          )
+        ),
+        takeUntil(this.destroySubject)
+      )
+      .subscribe(({ action, event }) => this.events$.next({ action, event }));
+  }
+
+  initKeyBindingsOnMonaco(editor: MonacoEditor.IStandaloneCodeEditor) {
+    chain(ShortcutsMap)
+      .toPairs()
+      .flatMap(([action, keys]) => castArray(keys).map(key => ({ action, key 
})))
+      .forEach(({ action, key }) => {
+        const keyBinding = KeyCodeConverter.angularToMonacoKeyBinding(key);
+        if (isNil(keyBinding)) {
+          return;
+        }
+        editor.addCommand(keyBinding, () => {
+          this.events$.next({ action: action as ParagraphActions, event: null 
});
+        });
+      })
+      .value();
+  }
+}
diff --git a/zeppelin-web-angular/src/app/key-binding/key-code-converter.ts 
b/zeppelin-web-angular/src/app/key-binding/key-code-converter.ts
new file mode 100644
index 0000000000..01fef1cc04
--- /dev/null
+++ b/zeppelin-web-angular/src/app/key-binding/key-code-converter.ts
@@ -0,0 +1,125 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { isNil } from 'lodash';
+import { KeyCode, KeyMod } from 'monaco-editor';
+
+const ASCII_MAX = 128;
+
+function isAscii(ch: string): boolean {
+  if (ch.length !== 1) {
+    throw new Error('Only single character is allowed');
+  }
+  return ch.charCodeAt(0) < ASCII_MAX;
+}
+
+export class KeyCodeConverter {
+  private static angularToMonacoKeyConverter: Record<string, number> = {
+    control: KeyMod.WinCtrl,
+    alt: KeyMod.Alt,
+    shift: KeyMod.Shift,
+    a: KeyCode.KeyA,
+    b: KeyCode.KeyB,
+    c: KeyCode.KeyC,
+    d: KeyCode.KeyD,
+    e: KeyCode.KeyE,
+    f: KeyCode.KeyF,
+    g: KeyCode.KeyG,
+    h: KeyCode.KeyH,
+    i: KeyCode.KeyI,
+    j: KeyCode.KeyJ,
+    k: KeyCode.KeyK,
+    l: KeyCode.KeyL,
+    m: KeyCode.KeyM,
+    n: KeyCode.KeyN,
+    o: KeyCode.KeyO,
+    p: KeyCode.KeyP,
+    q: KeyCode.KeyQ,
+    r: KeyCode.KeyR,
+    s: KeyCode.KeyS,
+    t: KeyCode.KeyT,
+    u: KeyCode.KeyU,
+    v: KeyCode.KeyV,
+    w: KeyCode.KeyW,
+    x: KeyCode.KeyX,
+    y: KeyCode.KeyY,
+    z: KeyCode.KeyZ,
+    0: KeyCode.Digit0,
+    1: KeyCode.Digit1,
+    2: KeyCode.Digit2,
+    3: KeyCode.Digit3,
+    4: KeyCode.Digit4,
+    5: KeyCode.Digit5,
+    6: KeyCode.Digit6,
+    7: KeyCode.Digit7,
+    8: KeyCode.Digit8,
+    9: KeyCode.Digit9,
+    ';': KeyCode.Semicolon,
+    '=': KeyCode.Equal,
+    ',': KeyCode.Comma,
+    '-': KeyCode.Minus,
+    '.': KeyCode.Period,
+    '/': KeyCode.Slash,
+    '`': KeyCode.Backquote,
+    '[': KeyCode.BracketLeft,
+    ']': KeyCode.BracketRight,
+    '\\': KeyCode.Backslash,
+    "'": KeyCode.Quote,
+    enter: KeyCode.Enter,
+    escape: KeyCode.Escape,
+    backspace: KeyCode.Backspace,
+    tab: KeyCode.Tab,
+    space: KeyCode.Space,
+    arrowup: KeyCode.UpArrow,
+    arrowdown: KeyCode.DownArrow,
+    arrowleft: KeyCode.LeftArrow,
+    arrowright: KeyCode.RightArrow,
+    delete: KeyCode.Delete,
+    home: KeyCode.Home,
+    end: KeyCode.End,
+    pageup: KeyCode.PageUp,
+    pagedown: KeyCode.PageDown,
+    insert: KeyCode.Insert
+  };
+
+  // Characters that are typed with `Shift` key could be handled with 
non-shifted version in keybinding.
+  private static exclusions = ['_', '+', '"', '{', '}', '|'];
+
+  static angularToMonacoKeyBinding(keybinding: string) {
+    const parts = keybinding.split('.');
+    // Ignore non-ASCII characters. Non-ASCII characters are just for macOS 
compatibility.
+    // Monaco editor handles pressing `Option(Alt) + a letter` not to convert 
a letter to non-ASCII character.
+    if (parts.some(p => p.length === 1 && (!isAscii(p) || 
this.exclusions.includes(p)))) {
+      return null;
+    }
+
+    // All ASCII characters should be supported.
+    // If this error is thrown, it means that `angularToMonacoKeyConverter` 
should be updated.
+    if (
+      parts.some(
+        p => p.length === 1 && !this.exclusions.includes(p) && isAscii(p) && 
!(p in this.angularToMonacoKeyConverter)
+      )
+    ) {
+      throw new Error(`Unsupported keybinding: '${keybinding}'.`);
+    }
+
+    const convertedParts = parts.map(p => {
+      const converted = this.angularToMonacoKeyConverter[p];
+      if (isNil(converted)) {
+        throw new Error(`Key code should be defined for '${p}'.`);
+      }
+      return converted;
+    });
+
+    return convertedParts.reduce((acc, part) => acc | part, 0);
+  }
+}
diff --git 
a/zeppelin-web-angular/src/app/key-binding/notebook-paragraph-keyboard-event-handler.ts
 
b/zeppelin-web-angular/src/app/key-binding/notebook-paragraph-keyboard-event-handler.ts
new file mode 100644
index 0000000000..05fb5f46ad
--- /dev/null
+++ 
b/zeppelin-web-angular/src/app/key-binding/notebook-paragraph-keyboard-event-handler.ts
@@ -0,0 +1,101 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ParagraphActions } from './paragraph-actions';
+
+export type NullableKeyboardEvent = KeyboardEvent | null;
+
+interface NotebookParagraphKeyboardEventHandler {
+  handleRun(): void;
+  handleRunAbove(): void;
+  handleRunBelow(): void;
+  handleCancel(): void;
+  handleMoveCursorUp(): void;
+  handleMoveCursorDown(): void;
+  handleDelete(): void;
+  handleInsertAbove(): void;
+  handleInsertBelow(): void;
+  handleInsertCopyOfParagraphBelow(): void;
+  handleMoveParagraphUp(): void;
+  handleMoveParagraphDown(): void;
+  handleSwitchEditor(): void;
+  handleSwitchEnable(): void;
+  handleSwitchOutputShow(): void;
+  handleSwitchLineNumber(): void;
+  handleSwitchTitleShow(): void;
+  handleClear(): void;
+  handleLink(): void;
+  handleReduceWidth(): void;
+  handleIncreaseWidth(): void;
+  handleCutLine(): void;
+  handlePasteLine(): void;
+  handleSearchInsideCode(): void;
+  handleFindInCode(): void;
+}
+
+// If any ParagraphActions is missing here, TS compiler will complain.
+export const ParagraphActionToHandlerName = {
+  [ParagraphActions.Run]: 'handleRun',
+  [ParagraphActions.RunAbove]: 'handleRunAbove',
+  [ParagraphActions.RunBelow]: 'handleRunBelow',
+  [ParagraphActions.Cancel]: 'handleCancel',
+  [ParagraphActions.MoveCursorUp]: 'handleMoveCursorUp',
+  [ParagraphActions.MoveCursorDown]: 'handleMoveCursorDown',
+  [ParagraphActions.Delete]: 'handleDelete',
+  [ParagraphActions.InsertAbove]: 'handleInsertAbove',
+  [ParagraphActions.InsertBelow]: 'handleInsertBelow',
+  [ParagraphActions.InsertCopyOfParagraphBelow]: 
'handleInsertCopyOfParagraphBelow',
+  [ParagraphActions.MoveParagraphUp]: 'handleMoveParagraphUp',
+  [ParagraphActions.MoveParagraphDown]: 'handleMoveParagraphDown',
+  [ParagraphActions.SwitchEditor]: 'handleSwitchEditor',
+  [ParagraphActions.SwitchEnable]: 'handleSwitchEnable',
+  [ParagraphActions.SwitchOutputShow]: 'handleSwitchOutputShow',
+  [ParagraphActions.SwitchLineNumber]: 'handleSwitchLineNumber',
+  [ParagraphActions.SwitchTitleShow]: 'handleSwitchTitleShow',
+  [ParagraphActions.Clear]: 'handleClear',
+  [ParagraphActions.Link]: 'handleLink',
+  [ParagraphActions.ReduceWidth]: 'handleReduceWidth',
+  [ParagraphActions.IncreaseWidth]: 'handleIncreaseWidth',
+  [ParagraphActions.CutLine]: 'handleCutLine',
+  [ParagraphActions.PasteLine]: 'handlePasteLine',
+  [ParagraphActions.SearchInsideCode]: 'handleSearchInsideCode',
+  [ParagraphActions.FindInCode]: 'handleFindInCode'
+} as const;
+// TODO: Replace `as const` with `satisfies Record<ParagraphActions, keyof 
NotebookParagraphKeyboardEventHandler>` when typescript version is over 4.9.
+//       This allows checking both keys and values at the type level,
+//       while preserving the binding between them.
+
+const MonacoHandledParagraphActions = [
+  ParagraphActions.MoveCursorUp,
+  ParagraphActions.MoveCursorDown,
+  ParagraphActions.SwitchEditor,
+  ParagraphActions.CutLine,
+  ParagraphActions.PasteLine,
+  ParagraphActions.SearchInsideCode
+] as const;
+// TODO: Replace `as const` with `satisfies ParagraphActions[]` when 
typescript version is over 4.9.
+//       This ensures that the array contains only valid ParagraphActions,
+//       while preserving the literal value of the each element.
+
+type MonacoHandledParagraphAction = typeof 
MonacoHandledParagraphActions[number];
+
+type MonacoHandledParagraphActionHandlerName = typeof 
ParagraphActionToHandlerName[MonacoHandledParagraphAction];
+
+export type MonacoKeyboardEventHandler = Pick<
+  NotebookParagraphKeyboardEventHandler,
+  MonacoHandledParagraphActionHandlerName
+>;
+
+export type AngularKeyboardEventHandler = Omit<
+  NotebookParagraphKeyboardEventHandler,
+  MonacoHandledParagraphActionHandlerName
+>;
diff --git a/zeppelin-web-angular/src/app/key-binding/paragraph-actions.ts 
b/zeppelin-web-angular/src/app/key-binding/paragraph-actions.ts
new file mode 100644
index 0000000000..6e430c611a
--- /dev/null
+++ b/zeppelin-web-angular/src/app/key-binding/paragraph-actions.ts
@@ -0,0 +1,39 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export enum ParagraphActions {
+  Run = 'Paragraph:Run',
+  RunAbove = 'Paragraph:RunAbove',
+  RunBelow = 'Paragraph:RunBelow',
+  Cancel = 'Paragraph:Cancel',
+  MoveCursorUp = 'Paragraph:MoveCursorUp',
+  MoveCursorDown = 'Paragraph:MoveCursorDown',
+  Delete = 'Paragraph:Delete',
+  InsertAbove = 'Paragraph:InsertAbove',
+  InsertBelow = 'Paragraph:InsertBelow',
+  InsertCopyOfParagraphBelow = 'Paragraph:InsertCopyOfParagraphBelow',
+  MoveParagraphUp = 'Paragraph:MoveParagraphUp',
+  MoveParagraphDown = 'Paragraph:MoveParagraphDown',
+  SwitchEditor = 'Paragraph:SwitchEditor',
+  SwitchEnable = 'Paragraph:SwitchEnable',
+  SwitchOutputShow = 'Paragraph:SwitchOutputShow',
+  SwitchLineNumber = 'Paragraph:SwitchLineNumber',
+  SwitchTitleShow = 'Paragraph:SwitchTitleShow',
+  Clear = 'Paragraph:Clear',
+  Link = 'Paragraph:Link',
+  ReduceWidth = 'Paragraph:ReduceWidth',
+  IncreaseWidth = 'Paragraph:IncreaseWidth',
+  CutLine = 'Paragraph:CutLine',
+  PasteLine = 'Paragraph:PasteLine',
+  SearchInsideCode = 'Paragraph:SearchInsideCode',
+  FindInCode = 'Paragraph:FindInCode'
+}
diff --git a/zeppelin-web-angular/src/app/key-binding/public-api.ts 
b/zeppelin-web-angular/src/app/key-binding/public-api.ts
new file mode 100644
index 0000000000..65059d354c
--- /dev/null
+++ b/zeppelin-web-angular/src/app/key-binding/public-api.ts
@@ -0,0 +1,16 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './key-binder';
+export * from './notebook-paragraph-keyboard-event-handler';
+export * from './paragraph-actions';
+export * from './shortcuts-map';
diff --git a/zeppelin-web-angular/src/app/key-binding/shortcuts-map.ts 
b/zeppelin-web-angular/src/app/key-binding/shortcuts-map.ts
new file mode 100644
index 0000000000..a29bd16f90
--- /dev/null
+++ b/zeppelin-web-angular/src/app/key-binding/shortcuts-map.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// On macOS, pressing Option(Alt) + a letter produces a non-ASCII character
+// Shortcuts must use this resulting character instead of the plain letter for 
macOS
+import { ParagraphActions } from './paragraph-actions';
+
+export const ShortcutsMap: Record<ParagraphActions, string | string[]> = {
+  [ParagraphActions.Run]: 'shift.enter', // Run paragraph
+  [ParagraphActions.RunAbove]: 'control.shift.arrowup', // Run all above 
paragraphs (exclusive)
+  [ParagraphActions.RunBelow]: 'control.shift.arrowdown', // Run all below 
paragraphs (inclusive)
+  [ParagraphActions.Cancel]: ['control.alt.c', 'control.alt.ç'], // Cancel
+  [ParagraphActions.MoveCursorUp]: 'control.p', // Move cursor Up
+  [ParagraphActions.MoveCursorDown]: 'control.n', // Move cursor Down
+  [ParagraphActions.Delete]: ['control.alt.d', 'control.alt.∂'], // Remove 
paragraph
+  [ParagraphActions.InsertAbove]: ['control.alt.a', 'control.alt.å'], // 
Insert new paragraph above
+  [ParagraphActions.InsertBelow]: ['control.alt.b', 'control.alt.∫'], // 
Insert new paragraph below
+  [ParagraphActions.InsertCopyOfParagraphBelow]: 'control.shift.c', // Insert 
copy of paragraph below
+  [ParagraphActions.MoveParagraphUp]: ['control.alt.k', 'control.alt.˚'], // 
Move paragraph Up
+  [ParagraphActions.MoveParagraphDown]: ['control.alt.j', 'control.alt.∆'], // 
Move paragraph Down
+  [ParagraphActions.SwitchEditor]: ['control.alt.e'], // Toggle editor
+  [ParagraphActions.SwitchEnable]: ['control.alt.r', 'control.alt.®'], // 
Enable/Disable run paragraph
+  [ParagraphActions.SwitchOutputShow]: ['control.alt.o', 'control.alt.ø'], // 
Toggle output
+  [ParagraphActions.SwitchLineNumber]: ['control.alt.m', 'control.alt.µ'], // 
Toggle line number
+  [ParagraphActions.SwitchTitleShow]: ['control.alt.t', 'control.alt.†'], // 
Toggle title
+  [ParagraphActions.Clear]: ['control.alt.l', 'control.alt.¬'], // Clear output
+  [ParagraphActions.Link]: ['control.alt.w', 'control.alt.∑'], // Link this 
paragraph
+  [ParagraphActions.ReduceWidth]: ['control.shift.-', 'control.shift._'], // 
Reduce paragraph width
+  [ParagraphActions.IncreaseWidth]: ['control.shift.=', 'control.shift.+'], // 
Increase paragraph width
+  // Auto-completion - No longer needed; always applied now
+  [ParagraphActions.CutLine]: ['control.k'], // Cut the line
+  [ParagraphActions.PasteLine]: ['control.y'], // Paste the line
+  // Move cursor to the beginning - System shortcut
+  // Move cursor at the end - System shortcut
+  [ParagraphActions.SearchInsideCode]: ['control.s'],
+  // TODO: Check after the search code is implemented in 
action-bar.component.ts
+  [ParagraphActions.FindInCode]: ['control.alt.f', 'control.alt.ƒ'] // Find in 
code
+};
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
index d396bbae2e..d046e24850 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
@@ -28,6 +28,7 @@ import { editor as MonacoEditor, IDisposable, IPosition, 
KeyCode } from 'monaco-
 import { InterpreterBindingItem } from '@zeppelin/sdk';
 import { CompletionService, MessageService } from '@zeppelin/services';
 
+import { MonacoKeyboardEventHandler, ParagraphActions, 
ParagraphActionToHandlerName } from '@zeppelin/key-binding';
 import { pt2px } from '@zeppelin/utility';
 import { NotebookParagraphControlComponent } from 
'../control/control.component';
 
@@ -40,7 +41,8 @@ type IEditor = MonacoEditor.IEditor;
   styleUrls: ['./code-editor.component.less'],
   changeDetection: ChangeDetectionStrategy.OnPush
 })
-export class NotebookParagraphCodeEditorComponent implements OnChanges, 
OnDestroy, AfterViewInit {
+export class NotebookParagraphCodeEditorComponent
+  implements OnChanges, OnDestroy, AfterViewInit, MonacoKeyboardEventHandler {
   @Input() position: IPosition | null = null;
   @Input() readOnly = false;
   @Input() language?: string = 'text';
@@ -57,6 +59,7 @@ export class NotebookParagraphCodeEditorComponent implements 
OnChanges, OnDestro
   @Output() readonly editorBlur = new EventEmitter<void>();
   @Output() readonly editorFocus = new EventEmitter<void>();
   @Output() readonly toggleEditorShow = new EventEmitter<void>();
+  @Output() readonly initKeyBindings = new 
EventEmitter<IStandaloneCodeEditor>();
   private editor?: IStandaloneCodeEditor;
   private monacoDisposables: IDisposable[] = [];
   height = 18;
@@ -112,42 +115,22 @@ export class NotebookParagraphCodeEditorComponent 
implements OnChanges, OnDestro
     }
   }
 
-  // Handle Ctrl+Alt+E: Toggle editor show/hide
-  handleToggleEditorShow() {
-    this.toggleEditorShow.emit();
-  }
-
-  // Handle Ctrl+K: Cut current line to clipboard
-  async handleCutLine() {
-    if (!this.editor) {
-      return;
-    }
-
-    const position = this.editor.getPosition();
-    const model = this.editor.getModel();
-    if (!position || !model) {
-      return;
+  handleMoveCursorUp() {
+    if (this.editor) {
+      this.editor.trigger('keyboard', 'cursorUp', null);
     }
+  }
 
-    const lineNumber = position.lineNumber;
-    const lineContent = model.getLineContent(lineNumber);
-
-    if (!lineContent) {
-      return;
+  handleMoveCursorDown() {
+    if (this.editor) {
+      this.editor.trigger('keyboard', 'cursorDown', null);
     }
+  }
 
-    await navigator.clipboard.writeText(lineContent);
-
-    this.editor.executeEdits('cut-line', [
-      {
-        range: new monaco.Range(lineNumber, 1, lineNumber, lineContent.length 
+ 1),
-        text: '',
-        forceMoveMarkers: true
-      }
-    ]);
+  handleToggleEditorShow() {
+    this.toggleEditorShow.emit();
   }
 
-  // Handle Ctrl+Y: Paste from clipboard at current position
   async handlePasteFromClipboard() {
     if (!this.editor) {
       return;
@@ -166,7 +149,6 @@ export class NotebookParagraphCodeEditorComponent 
implements OnChanges, OnDestro
     }
   }
 
-  // Handle Ctrl+S: Show find widget
   handleShowFind() {
     if (this.editor) {
       this.editor.getAction('actions.find').run();
@@ -209,6 +191,7 @@ export class NotebookParagraphCodeEditorComponent 
implements OnChanges, OnDestro
 
   initializedEditor(editor: IEditor) {
     this.editor = editor as IStandaloneCodeEditor;
+    this.initKeyBindings.emit(this.editor);
     this.editor.addCommand(
       KeyCode.Escape,
       () => {
@@ -218,18 +201,6 @@ export class NotebookParagraphCodeEditorComponent 
implements OnChanges, OnDestro
       },
       '!suggestWidgetVisible'
     );
-    this.editor.addCommand(monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | 
monaco.KeyCode.KeyE, () => {
-      this.handleToggleEditorShow();
-    });
-    this.editor.addCommand(monaco.KeyMod.WinCtrl | monaco.KeyCode.KeyK, () => {
-      this.handleCutLine();
-    });
-    this.editor.addCommand(monaco.KeyMod.WinCtrl | monaco.KeyCode.KeyY, () => {
-      this.handlePasteFromClipboard();
-    });
-    this.editor.addCommand(monaco.KeyMod.WinCtrl | monaco.KeyCode.KeyS, () => {
-      this.handleShowFind();
-    });
 
     this.updateEditorOptions(this.editor);
     this.setParagraphMode();
@@ -242,6 +213,56 @@ export class NotebookParagraphCodeEditorComponent 
implements OnChanges, OnDestro
     });
   }
 
+  handleKeyEvent(action: ParagraphActions) {
+    const handlerName = ParagraphActionToHandlerName[action];
+    const handlerFn = handlerName && handlerName in this && this[handlerName 
as keyof this];
+    if (!handlerFn || typeof handlerFn !== 'function') {
+      return;
+    }
+    handlerFn.call(this);
+  }
+
+  handleSwitchEditor() {
+    this.handleToggleEditorShow();
+  }
+
+  async handleCutLine() {
+    if (!this.editor) {
+      return;
+    }
+
+    const position = this.editor.getPosition();
+    const model = this.editor.getModel();
+    if (!position || !model) {
+      return;
+    }
+
+    const lineNumber = position.lineNumber;
+    const lineContent = model.getLineContent(lineNumber);
+
+    if (!lineContent) {
+      return;
+    }
+
+    await navigator.clipboard.writeText(lineContent);
+
+    this.editor.executeEdits('cut-line', [
+      {
+        range: new monaco.Range(lineNumber, 1, lineNumber, lineContent.length 
+ 1),
+        text: '',
+        forceMoveMarkers: true
+      }
+    ]);
+  }
+
+  handlePasteLine() {
+    this.handlePasteFromClipboard();
+  }
+
+  handleSearchInsideCode() {
+    this.handleShowFind();
+  }
+
   initCompletionService(editor: IStandaloneCodeEditor): void {
     const model = editor.getModel();
     if (!model) {
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html
index 09239cfdcc..91015e8a17 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.html
@@ -84,6 +84,7 @@
       (editorFocus)="onEditorFocus()"
       (textChanged)="textChanged($event)"
       (toggleEditorShow)="toggleEditorShow()"
+      (initKeyBindings)="initKeyBindingsOnMonaco($event)"
     ></zeppelin-notebook-paragraph-code-editor>
     <zeppelin-notebook-paragraph-progress
       *ngIf="paragraph.status === 'RUNNING'"
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
index 960abbdfb0..5e9d7bf820 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
@@ -27,8 +27,9 @@ import {
   ViewChild,
   ViewChildren
 } from '@angular/core';
-import { merge, Observable, Subject } from 'rxjs';
-import { map, takeUntil } from 'rxjs/operators';
+import { editor as MonacoEditor } from 'monaco-editor';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
 
 import { NzModalService } from 'ng-zorro-antd/modal';
 
@@ -46,12 +47,17 @@ import {
   NgZService,
   NoteStatusService,
   NoteVarShareService,
-  ParagraphActions,
-  ShortcutsMap,
   ShortcutService
 } from '@zeppelin/services';
 import { SpellResult } from '@zeppelin/spell';
 
+import {
+  AngularKeyboardEventHandler,
+  KeyBinder,
+  NullableKeyboardEvent,
+  ParagraphActions,
+  ParagraphActionToHandlerName
+} from '@zeppelin/key-binding';
 import { NzResizeEvent } from 'ng-zorro-antd/resizable';
 import { NotebookParagraphResultComponent } from 
'../../share/result/result.component';
 import { NotebookParagraphCodeEditorComponent } from 
'./code-editor/code-editor.component';
@@ -68,7 +74,8 @@ type Mode = 'edit' | 'command';
   },
   changeDetection: ChangeDetectionStrategy.OnPush
 })
-export class NotebookParagraphComponent extends ParagraphBase implements 
OnInit, OnChanges, OnDestroy, AfterViewInit {
+export class NotebookParagraphComponent extends ParagraphBase
+  implements OnInit, OnChanges, OnDestroy, AfterViewInit, 
AngularKeyboardEventHandler {
   @ViewChild(NotebookParagraphCodeEditorComponent, { static: false })
   notebookParagraphCodeEditorComponent?: NotebookParagraphCodeEditorComponent;
   @ViewChildren(NotebookParagraphResultComponent) 
notebookParagraphResultComponents!: QueryList<
@@ -97,6 +104,8 @@ export class NotebookParagraphComponent extends 
ParagraphBase implements OnInit,
   private mode: Mode = 'command';
   waitConfirmFromEdit = false;
 
+  private keyBinderService: KeyBinder;
+
   updateParagraphResult(resultIndex: number, config: ParagraphConfigResult, 
result: ParagraphIResultsMsgItem): void {
     const resultComponent = 
this.notebookParagraphResultComponents.toArray()[resultIndex];
     if (resultComponent) {
@@ -246,7 +255,7 @@ export class NotebookParagraphComponent extends 
ParagraphBase implements OnInit,
     if (!this.paragraph.config.editorSetting) {
       throw new Error('editorSetting is required');
     }
-    if (this.paragraph.config.editorSetting.editOnDblClick && 
this.revisionView !== true) {
+    if (this.paragraph.config.editorSetting.editOnDblClick && 
!this.revisionView) {
       this.paragraph.config.editorHide = false;
       this.paragraph.config.tableHide = true;
       this.focusEditor();
@@ -350,7 +359,7 @@ export class NotebookParagraphComponent extends 
ParagraphBase implements OnInit,
   }
 
   insertParagraph(position: string) {
-    if (this.revisionView === true) {
+    if (this.revisionView) {
       return;
     }
     let newIndex = -1;
@@ -460,32 +469,6 @@ export class NotebookParagraphComponent extends 
ParagraphBase implements OnInit,
     this.cdr.markForCheck();
   }
 
-  moveCursorUp() {
-    const newIndex = this.note.paragraphs.findIndex(p => p.id === 
this.paragraph.id) - 1;
-    if (newIndex < 0 || newIndex >= this.note.paragraphs.length) {
-      return;
-    }
-    // save dirtyText of moving paragraphs.
-    const prevParagraph = this.note.paragraphs[newIndex];
-    // TODO(hsuanxyz): save pre paragraph?
-    this.saveParagraph();
-    this.triggerSaveParagraph.emit(prevParagraph.id);
-    this.messageService.moveParagraph(this.paragraph.id, newIndex);
-  }
-
-  moveCursorDown() {
-    const newIndex = this.note.paragraphs.findIndex(p => p.id === 
this.paragraph.id) + 1;
-    if (newIndex < 0 || newIndex >= this.note.paragraphs.length) {
-      return;
-    }
-    // save dirtyText of moving paragraphs.
-    const nextParagraph = this.note.paragraphs[newIndex];
-    // TODO(hsuanxyz): save pre paragraph?
-    this.saveParagraph();
-    this.triggerSaveParagraph.emit(nextParagraph.id);
-    this.messageService.moveParagraph(this.paragraph.id, newIndex);
-  }
-
   moveParagraphUp() {
     const newIndex = this.note.paragraphs.findIndex(p => p.id === 
this.paragraph.id) - 1;
     if (newIndex < 0 || newIndex >= this.note.paragraphs.length) {
@@ -550,146 +533,46 @@ export class NotebookParagraphComponent extends 
ParagraphBase implements OnInit,
   constructor(
     public messageService: MessageService,
     private heliumService: HeliumService,
-    private nzModalService: NzModalService,
+    private host: ElementRef,
     private noteVarShareService: NoteVarShareService,
+    private nzModalService: NzModalService,
     private shortcutService: ShortcutService,
-    private host: ElementRef,
     noteStatusService: NoteStatusService,
     cdr: ChangeDetectorRef,
     ngZService: NgZService
   ) {
     super(messageService, noteStatusService, ngZService, cdr);
+    this.keyBinderService = new KeyBinder(this.destroy$, this.host, 
this.shortcutService);
   }
 
-  ngOnInit() {
-    const shortcutService = 
this.shortcutService.forkByElement(this.host.nativeElement);
-    const observables: Array<Observable<{
-      action: ParagraphActions;
-      event: KeyboardEvent;
-    }>> = [];
-    Object.entries(ShortcutsMap).forEach(([action, keys]) => {
-      const keysArr: string[] = Array.isArray(keys) ? keys : [keys];
-      keysArr.forEach(key => {
-        observables.push(
-          shortcutService
-            .bindShortcut({
-              keybindings: key
-            })
-            .pipe(
-              takeUntil(this.destroy$),
-              map(({ event }) => {
-                return {
-                  event,
-                  action: action as ParagraphActions
-                };
-              })
-            )
-        );
-      });
-    });
+  private handleKeyEvent(action: ParagraphActions, event: 
NullableKeyboardEvent) {
+    const target = (event?.target || null) as HTMLElement | null;
 
-    merge<{
-      action: ParagraphActions;
-      event: KeyboardEvent;
-    }>(...observables)
-      .pipe(takeUntil(this.destroy$))
-      .subscribe(({ action, event }) => {
-        const target = event.target as HTMLElement;
+    // Skip handling shortcut if focused element is an input (by Dynamic form)
+    if (target?.tagName === 'INPUT') {
+      return; // ignore shortcut to make input work
+    }
 
-        // Skip handling shortcut if focused element is an input (by Dynamic 
form)
-        if (target.tagName === 'INPUT') {
-          return; // ignore shortcut to make input work
-        }
+    const handlerFn = this[ParagraphActionToHandlerName[action] as keyof 
AngularKeyboardEventHandler];
+    if (!handlerFn) {
+      return;
+    }
+    event?.preventDefault();
+    handlerFn.call(this);
+  }
 
-        switch (action) {
-          case ParagraphActions.Run:
-            event.preventDefault();
-            this.runParagraph();
-            break;
-          case ParagraphActions.RunAbove:
-            this.waitConfirmFromEdit = true;
-            this.runAllAbove();
-            break;
-          case ParagraphActions.RunBelow:
-            this.waitConfirmFromEdit = true;
-            this.runAllBelowAndCurrent();
-            break;
-          case ParagraphActions.Cancel:
-            event.preventDefault();
-            this.cancelParagraph();
-            break;
-          case ParagraphActions.MoveCursorUp:
-            event.preventDefault();
-            this.moveCursorUp();
-            break;
-          case ParagraphActions.MoveCursorDown:
-            event.preventDefault();
-            this.moveCursorDown();
-            break;
-          case ParagraphActions.Delete:
-            this.removeParagraph();
-            break;
-          case ParagraphActions.InsertAbove:
-            this.insertParagraph('above');
-            break;
-          case ParagraphActions.InsertBelow:
-            this.insertParagraph('below');
-            break;
-          case ParagraphActions.InsertCopyOfParagraphBelow:
-            this.cloneParagraph('below');
-            break;
-          case ParagraphActions.MoveParagraphUp:
-            event.preventDefault();
-            this.moveParagraphUp();
-            break;
-          case ParagraphActions.MoveParagraphDown:
-            event.preventDefault();
-            this.moveParagraphDown();
-            break;
-          case ParagraphActions.SwitchEnable:
-            this.paragraph.config.enabled = !this.paragraph.config.enabled;
-            this.commitParagraph();
-            break;
-          case ParagraphActions.SwitchOutputShow:
-            this.setTableHide(!this.paragraph.config.tableHide);
-            this.commitParagraph();
-            break;
-          case ParagraphActions.SwitchLineNumber:
-            this.paragraph.config.lineNumbers = 
!this.paragraph.config.lineNumbers;
-            this.commitParagraph();
-            break;
-          case ParagraphActions.SwitchTitleShow:
-            this.paragraph.config.title = !this.paragraph.config.title;
-            this.commitParagraph();
-            break;
-          case ParagraphActions.Clear:
-            this.clearParagraphOutput();
-            break;
-          case ParagraphActions.Link:
-            this.openSingleParagraph(this.paragraph.id);
-            break;
-          case ParagraphActions.ReduceWidth:
-            if (!this.paragraph.config.colWidth) {
-              throw new Error('colWidth is required');
-            }
-            this.paragraph.config.colWidth = Math.max(1, 
this.paragraph.config.colWidth - 1);
-            this.cdr.markForCheck();
-            this.changeColWidth(true);
-            break;
-          case ParagraphActions.IncreaseWidth:
-            if (!this.paragraph.config.colWidth) {
-              throw new Error('colWidth is required');
-            }
-            this.paragraph.config.colWidth = Math.min(12, 
this.paragraph.config.colWidth + 1);
-            this.cdr.markForCheck();
-            this.changeColWidth(true);
-            break;
-          case ParagraphActions.FindInCode:
-            this.searchCode.emit();
-            break;
-          default:
-            break;
-        }
+  initKeyBindingsOnMonaco(editor: MonacoEditor.IStandaloneCodeEditor) {
+    this.keyBinderService.initKeyBindingsOnMonaco(editor);
+  }
+
+  ngOnInit() {
+    this.keyBinderService.initKeyBindingsOnAngular();
+    this.keyBinderService
+      .keyEvent()
+      .pipe(takeUntil(this.destroy$))
+      .subscribe(event => {
+        this.handleKeyEvent(event.action, event.event);
+        
this.notebookParagraphCodeEditorComponent?.handleKeyEvent(event.action);
       });
     this.setResults(this.paragraph);
     this.originalText = this.paragraph.text;
@@ -727,6 +610,98 @@ export class NotebookParagraphComponent extends 
ParagraphBase implements OnInit,
     }
   }
 
+  handleRun() {
+    this.runParagraph();
+  }
+
+  handleRunAbove() {
+    this.waitConfirmFromEdit = true;
+    this.runAllAbove();
+  }
+
+  handleRunBelow() {
+    this.waitConfirmFromEdit = true;
+    this.runAllBelowAndCurrent();
+  }
+
+  handleCancel() {
+    this.cancelParagraph();
+  }
+
+  handleDelete() {
+    this.removeParagraph();
+  }
+
+  handleInsertAbove() {
+    this.insertParagraph('above');
+  }
+
+  handleInsertBelow() {
+    this.insertParagraph('below');
+  }
+
+  handleInsertCopyOfParagraphBelow() {
+    this.cloneParagraph('below');
+  }
+
+  handleMoveParagraphUp() {
+    this.moveParagraphUp();
+  }
+
+  handleMoveParagraphDown() {
+    this.moveParagraphDown();
+  }
+
+  handleSwitchEnable() {
+    this.paragraph.config.enabled = !this.paragraph.config.enabled;
+    this.commitParagraph();
+  }
+
+  handleSwitchOutputShow() {
+    this.setTableHide(!this.paragraph.config.tableHide);
+    this.commitParagraph();
+  }
+
+  handleSwitchLineNumber() {
+    this.paragraph.config.lineNumbers = !this.paragraph.config.lineNumbers;
+    this.commitParagraph();
+  }
+
+  handleSwitchTitleShow() {
+    this.paragraph.config.title = !this.paragraph.config.title;
+    this.commitParagraph();
+  }
+
+  handleClear() {
+    this.clearParagraphOutput();
+  }
+
+  handleLink() {
+    this.openSingleParagraph(this.paragraph.id);
+  }
+
+  handleReduceWidth() {
+    if (!this.paragraph.config.colWidth) {
+      throw new Error('colWidth is required');
+    }
+    this.paragraph.config.colWidth = Math.max(1, 
this.paragraph.config.colWidth - 1);
+    this.cdr.markForCheck();
+    this.changeColWidth(true);
+  }
+
+  handleIncreaseWidth() {
+    if (!this.paragraph.config.colWidth) {
+      throw new Error('colWidth is required');
+    }
+    this.paragraph.config.colWidth = Math.min(12, 
this.paragraph.config.colWidth + 1);
+    this.cdr.markForCheck();
+    this.changeColWidth(true);
+  }
+
+  handleFindInCode() {
+    this.searchCode.emit();
+  }
+
   ngOnChanges(changes: SimpleChanges): void {
     const { index, select, scrolled } = changes;
     if (
diff --git a/zeppelin-web-angular/src/app/services/shortcut.service.ts 
b/zeppelin-web-angular/src/app/services/shortcut.service.ts
index f3cdb96620..82cdf34998 100644
--- a/zeppelin-web-angular/src/app/services/shortcut.service.ts
+++ b/zeppelin-web-angular/src/app/services/shortcut.service.ts
@@ -15,64 +15,6 @@ import { Inject, Injectable } from '@angular/core';
 import { EventManager } from '@angular/platform-browser';
 import { Observable } from 'rxjs';
 
-export enum ParagraphActions {
-  Run = 'Paragraph:Run',
-  RunAbove = 'Paragraph:RunAbove',
-  RunBelow = 'Paragraph:RunBelow',
-  Cancel = 'Paragraph:Cancel',
-  MoveCursorUp = 'Paragraph:MoveCursorUp',
-  MoveCursorDown = 'Paragraph:MoveCursorDown',
-  Delete = 'Paragraph:Delete',
-  InsertAbove = 'Paragraph:InsertAbove',
-  InsertBelow = 'Paragraph:InsertBelow',
-  InsertCopyOfParagraphBelow = 'Paragraph:InsertCopyOfParagraphBelow',
-  MoveParagraphUp = 'Paragraph:MoveParagraphUp',
-  MoveParagraphDown = 'Paragraph:MoveParagraphDown',
-  SwitchEnable = 'Paragraph:SwitchEnable',
-  SwitchOutputShow = 'Paragraph:SwitchOutputShow',
-  SwitchLineNumber = 'Paragraph:SwitchLineNumber',
-  SwitchTitleShow = 'Paragraph:SwitchTitleShow',
-  Clear = 'Paragraph:Clear',
-  Link = 'Paragraph:Link',
-  ReduceWidth = 'Paragraph:ReduceWidth',
-  IncreaseWidth = 'Paragraph:IncreaseWidth',
-  FindInCode = 'Paragraph:FindInCode'
-}
-
-// On macOS, pressing Option(Alt) + a letter produces a non-ASCII character
-// Shortcuts must use this resulting character instead of the plain letter for 
macOS
-export const ShortcutsMap = {
-  [ParagraphActions.Run]: 'shift.enter', // Run paragraph
-  [ParagraphActions.RunAbove]: 'control.shift.arrowup', // Run all above 
paragraphs (exclusive)
-  [ParagraphActions.RunBelow]: 'control.shift.arrowdown', // Run all below 
paragraphs (inclusive)
-  [ParagraphActions.Cancel]: ['control.alt.c', 'control.alt.ç'], // Cancel
-  [ParagraphActions.MoveCursorUp]: 'control.p', // Move cursor Up
-  [ParagraphActions.MoveCursorDown]: 'control.n', // Move cursor Down
-  [ParagraphActions.Delete]: ['control.alt.d', 'control.alt.∂'], // Remove 
paragraph
-  [ParagraphActions.InsertAbove]: ['control.alt.a', 'control.alt.å'], // 
Insert new paragraph above
-  [ParagraphActions.InsertBelow]: ['control.alt.b', 'control.alt.∫'], // 
Insert new paragraph below
-  [ParagraphActions.InsertCopyOfParagraphBelow]: 'control.shift.c', // Insert 
copy of paragraph below
-  [ParagraphActions.MoveParagraphUp]: ['control.alt.k', 'control.alt.˚'], // 
Move paragraph Up
-  [ParagraphActions.MoveParagraphDown]: ['control.alt.j', 'control.alt.∆'], // 
Move paragraph Down
-  [ParagraphActions.SwitchEnable]: ['control.alt.r', 'control.alt.®'], // 
Enable/Disable run paragraph
-  [ParagraphActions.SwitchOutputShow]: ['control.alt.o', 'control.alt.ø'], // 
Toggle output
-  // Toggle editor - Shortcut logic is implemented in the editor component
-  [ParagraphActions.SwitchLineNumber]: ['control.alt.m', 'control.alt.µ'], // 
Toggle line number
-  [ParagraphActions.SwitchTitleShow]: ['control.alt.t', 'control.alt.†'], // 
Toggle title
-  [ParagraphActions.Clear]: ['control.alt.l', 'control.alt.¬'], // Clear output
-  [ParagraphActions.Link]: ['control.alt.w', 'control.alt.∑'], // Link this 
paragraph
-  [ParagraphActions.ReduceWidth]: 'control.shift._', // Reduce paragraph width
-  [ParagraphActions.IncreaseWidth]: 'control.shift.=', // Increase paragraph 
width
-  // Auto-completion - No longer needed; always applied now
-  // Cut the line - Shortcut logic is implemented in the editor component
-  // Paste the line - Shortcut logic is implemented in the editor component
-  // Search inside the code - Shortcut logic is implemented in the editor 
component
-  // Move cursor to the beginning - System shortcut
-  // Move cursor at the end - System shortcut
-  // TODO: Check after the search code is implemented in 
action-bar.component.ts
-  [ParagraphActions.FindInCode]: ['control.alt.f', 'control.alt.ƒ'] // Find in 
code
-};
-
 export interface ShortcutEvent {
   event: KeyboardEvent;
   keybindings: string;


Reply via email to