You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by zj...@apache.org on 2020/01/12 06:38:19 UTC

[zeppelin] 02/16: [ZEPPELIN-4321] Support shortcuts for the paragraphs

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

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

commit d3979c2709d95ff17a10f3f3bbca91150f645a78
Author: Hsuan Lee <hs...@gmail.com>
AuthorDate: Fri Nov 8 17:20:59 2019 +0800

    [ZEPPELIN-4321] Support shortcuts for the paragraphs
    
    ### What is this PR for?
    
    We are using Angular Latest to refactor Zeppelin's front-end. When implementing the features of the shortcuts, we found that the current shortcuts is somewhat complicated and did not distinguish the Command/Ctrl key between Mac and Windows.
    
    So we compared the following applications:
    
    - [Jupyter](https://github.com/jupyter/jupyter)
    - [JupyterLab](https://github.com/jupyterlab/jupyterlab)
    - [Google Colaboratory](https://colab.research.google.com/)
    
    They can distinguish between edit-mode and command-mode, which will simplify the shortcuts complexity. Meanwhile, we use the [Monaco editor](https://github.com/microsoft/monaco-editor) in the refactor version, it is the core library of [VSCode](https://github.com/microsoft/vscode). We think it is a good choice to use its shortcuts design.
    
    Therefore, for the above reasons, we proposed to redesign Zeppelin's shortcuts according to the table following.
    
    | Actions                                               | Mode    | Mac              | Windows / Linux       | Old(Mac)         |
    |-------------------------------------------------------|---------|------------------|-----------------------|------------------|
    | Command mode                                          | Edit    | ESC              | ESC                   | -                |
    | Edit mode                                             | Command | Enter            | Enter                 | -                |
    | Run                                                   | -       | ⇧ + Enter        | ⇧ + Enter             | ⇧ + Enter        |
    | Run all below                                         | -       | ⇧ + ⌘ + Enter    | ⇧ + Ctrl + Enter      | ⇧ + Ctrl + Enter |
    | Run all above                                         | -       | ⇧ + ⌥ + Enter    | ⇧ + Alt + Enter       | ⇧ + Ctrl + Enter |
    | Cancel                                                | -       | ⇧ + ⌘ + C        | ⇧ + Ctrl + C          | ⌥ + Ctrl + C     |
    | Switch all line number                                | -       | ⇧ + ⌘ + L        | ⇧ + Ctrl + L          | -                |
    | Show / Hide all output                                | -       | ⇧ + ⌘ + O        | ⇧ + Ctrl + O          | -                |
    | Show / Hide all title                                 | -       | ⇧ + ⌘ + T        | ⇧ + Ctrl + T          | -                |
    | Clear output                                          | -       | ⌘ + ⌥ + L        | Ctrl + Alt + L        | ⌥ + Ctrl + L     |
    | Enable/Disable                                        | -       | ⌘ + ⌥ + R        | Ctrl + Alt + R        | ⌥ + Ctrl + R     |
    | Reduce width                                          | -       | ⌘ + ⌥ + +        | Ctrl + Alt + -        | ⇧ + Ctrl + -     |
    | Increase width                                        | -       | ⌘ + ⌥ + -        | Ctrl + Alt + +        | ⇧ + Ctrl + +     |
    | Delete                                                | Command | ⇧ + Del            | ⇧ + Del                  | ⌥ + Ctrl + D     |
    | Move to up                                            | Command | ⌘ + K / Up       | Ctrl + K / Up         | ⌥ + Ctrl + K     |
    | Move to down                                          | Command | ⌘ + J / Down     | Ctrl + J / Down       | ⌥ + Ctrl + J     |
    | Select above                                          | Command | K / Up           | K / Up                | -                |
    | Select below                                          | Command | J / Down         | J / Down              | -                |
    | Switch line number                                    | Command | L                | L                     | ⌥ + Ctrl + M     |
    | Show / Hide title                                     | Command | T                | T                     | ⌥ + Ctrl + T     |
    | Show / Hide output                                    | Command | O                | O                     | ⌥ + Ctrl + O     |
    | Show / Hide editor                                    | Command | E                | E                     | ⌥ + Ctrl + E     |
    | Insert above                                          | Command | A                | A                     | ⌥ + Ctrl + A     |
    | Insert below                                          | Command | B                | B                     | ⌥ + Ctrl + B     |
    | Search                                                | Edit    | ⌘ + F            | Ctrl + F              | ⌥ + Ctrl + F     |
    | Increase Indent                                       | Edit    | Tab              | Tab                   | -                |
    | Decrease Indent                                       | Edit    | ⇧ + Tab          | ⇧ + Tab               | -                |
    | Comment Out / In                                      | Edit    | ⌘ + /            | Ctrl + /              | Ctrl + /         |
    | Undo                                                  | Edit    | ⌘ + Z            | Ctrl + Z              | Ctrl + Z         |
    | Redo                                                  | Edit    | ⇧ + ⌘ + Z        | Ctrl + Y              | -                |
    | Increase font size                                    | Edit    | ⌘ + .            | Ctrl + .              | -                |
    | Decrease font size                                    | Edit    | ⌘ + ,            | Ctrl + ,              | -                |
    | Decrease Indent                                       | Edit    | ⌘ + [            | Ctrl + [              | -                |
    | Increase Indent                                       | Edit    | ⌘ + ]            | Ctrl + ]              | -                |
    | Move the line down                                    | Edit    | ⌥ + Down         | Alt + Down            | ⌥ + Down         |
    | Move the line up                                      | Edit    | ⌥ + Up           | Alt + Up              | ⌥ + Down         |
    | Replace                                               | Edit    | ⌘ + ⌥ + F        | Ctrl + F              | -                |
    | Select all                                            | Edit    | ⌘ + A            | Ctrl + A              | ⌘ + A            |
    | Select downward                                       | Edit    | ⇧ + Down         | ⇧ + Down              | ⇧ + Down         |
    | Select right                                          | Edit    | ⇧ + Right        | ⇧ + Right             | ⇧ + Right        |
    | Select left                                           | Edit    | ⇧ + Left         | ⇧ + Left              | ⇧ + Left         |
    | Select upward                                         | Edit    | ⇧ + Up           | ⇧ + Up                | ⇧ + Up           |
    | Select to the end                                     | Edit    | ⌘ + ⇧ + Right    | Alt + ⇧ + Right       | ⌘ + ⇧ + Right    |
    | Select to the start                                   | Edit    | ⌘ + ⇧ + Left     | Alt + ⇧ + Left        | ⌘ + ⇧ + Left     |
    | Align text right                                      | Edit    | ⌥ + Right        | Ctrl + ⇧ + Right      | ⌥ + Right        |
    | Align text left                                       | Edit    | ⌥ + Left         | Ctrl + ⇧ + Left       | ⌥ + Left         |
    | Add multi-cursor above                                | Edit    | ⌘ + ⌥ + Up       | Ctrl + Alt + Up       | -                |
    | Add multi-cursor below                                | Edit    | ⌘ + ⌥ + Down     | Ctrl + Alt + Down     | -                |
    | Move multi-cursor from current line to the line above | Edit    | ⌘ + ⌥ + ⇧ + Up   | Ctrl + Alt + ⇧ + Up   | -                |
    | Move multi-cursor from current line to the line below | Edit    | ⌘ + ⌥ + ⇧ + Down | Ctrl + Alt + ⇧ + Down | -                |
    
    ### What type of PR is it?
    [Feature]
    
    ### Todos
    * [ ] - Task
    
    ### What is the Jira issue?
    
    https://issues.apache.org/jira/browse/ZEPPELIN-4402
    https://issues.apache.org/jira/browse/ZEPPELIN-4321
    
    ### How should this be tested?
    * First time? Setup Travis CI as described on https://zeppelin.apache.org/contribution/contributions.html#continuous-integration
    * Strongly recommended: add automated unit tests for any new or changed behavior
    * Outline any manual steps to test the PR here.
    
    ### Screenshots (if appropriate)
    
    ### Questions:
    * Does the licenses files need update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Author: Hsuan Lee <hs...@gmail.com>
    
    Closes #3517 from hsuanxyz/feat/hot-key and squashes the following commits:
    
    0dec67601 [Hsuan Lee] feat(paragraph): support shortcut
---
 zeppelin-web-angular/package-lock.json             |  76 +++++--
 zeppelin-web-angular/package.json                  |   6 +-
 .../workspace/notebook/notebook.component.html     |   6 +-
 .../pages/workspace/notebook/notebook.component.ts |  16 +-
 .../paragraph/code-editor/code-editor.component.ts |  13 ++
 .../paragraph/control/control.component.ts         |  24 +--
 .../notebook/paragraph/paragraph.component.html    |   9 +-
 .../notebook/paragraph/paragraph.component.less    |   5 +
 .../notebook/paragraph/paragraph.component.ts      | 227 ++++++++++++++++++++-
 .../notebook/paragraph/result/result.component.ts  |   3 +
 .../src/app/services/public-api.ts                 |   1 +
 .../src/app/services/shortcut.service.ts           | 109 ++++++++++
 12 files changed, 436 insertions(+), 59 deletions(-)

diff --git a/zeppelin-web-angular/package-lock.json b/zeppelin-web-angular/package-lock.json
index 46c0a07..85ad2a6 100644
--- a/zeppelin-web-angular/package-lock.json
+++ b/zeppelin-web-angular/package-lock.json
@@ -6852,23 +6852,54 @@
       }
     },
     "husky": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/husky/-/husky-2.7.0.tgz",
-      "integrity": "sha512-LIi8zzT6PyFpcYKdvWRCn/8X+6SuG2TgYYMrM6ckEYhlp44UcEduVymZGIZNLiwOUjrEud+78w/AsAiqJA/kRg==",
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.9.tgz",
+      "integrity": "sha512-Yolhupm7le2/MqC1VYLk/cNmYxsSsqKkTyBhzQHhPK1jFnC89mmmNVuGtLNabjDI6Aj8UNIr0KpRNuBkiC4+sg==",
       "dev": true,
       "requires": {
-        "cosmiconfig": "^5.2.0",
+        "chalk": "^2.4.2",
+        "ci-info": "^2.0.0",
+        "cosmiconfig": "^5.2.1",
         "execa": "^1.0.0",
-        "find-up": "^3.0.0",
         "get-stdin": "^7.0.0",
-        "is-ci": "^2.0.0",
-        "pkg-dir": "^4.1.0",
-        "please-upgrade-node": "^3.1.1",
-        "read-pkg": "^5.1.1",
+        "opencollective-postinstall": "^2.0.2",
+        "pkg-dir": "^4.2.0",
+        "please-upgrade-node": "^3.2.0",
+        "read-pkg": "^5.2.0",
         "run-node": "^1.0.0",
         "slash": "^3.0.0"
       },
       "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "find-up": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+          "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^5.0.0",
+            "path-exists": "^4.0.0"
+          }
+        },
         "locate-path": {
           "version": "5.0.0",
           "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -6900,18 +6931,6 @@
           "dev": true,
           "requires": {
             "find-up": "^4.0.0"
-          },
-          "dependencies": {
-            "find-up": {
-              "version": "4.1.0",
-              "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
-              "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
-              "dev": true,
-              "requires": {
-                "locate-path": "^5.0.0",
-                "path-exists": "^4.0.0"
-              }
-            }
           }
         },
         "slash": {
@@ -6919,6 +6938,15 @@
           "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
           "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
           "dev": true
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
         }
       }
     },
@@ -10409,6 +10437,12 @@
         "is-wsl": "^1.1.0"
       }
     },
+    "opencollective-postinstall": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz",
+      "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==",
+      "dev": true
+    },
     "opn": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
diff --git a/zeppelin-web-angular/package.json b/zeppelin-web-angular/package.json
index ae96fe7..63b6be1 100644
--- a/zeppelin-web-angular/package.json
+++ b/zeppelin-web-angular/package.json
@@ -44,8 +44,6 @@
     "zone.js": "~0.9.1"
   },
   "devDependencies": {
-    "monaco-editor-webpack-plugin": "^1.7.0",
-    "ngx-build-plus": "^8.1.5",
     "@angular-devkit/build-angular": "^0.803.9",
     "@angular-devkit/build-ng-packagr": "~0.803.6",
     "@angular/cli": "~8.3.9",
@@ -61,7 +59,7 @@
     "codelyzer": "^5.0.0",
     "dotenv": "^8.0.0",
     "https-proxy-agent": "^2.2.1",
-    "husky": "^2.2.0",
+    "husky": "^3.0.9",
     "jasmine-core": "~3.4.0",
     "jasmine-spec-reporter": "~4.2.1",
     "karma": "~4.1.0",
@@ -70,7 +68,9 @@
     "karma-jasmine": "~2.0.1",
     "karma-jasmine-html-reporter": "^1.4.0",
     "lint-staged": "^8.1.6",
+    "monaco-editor-webpack-plugin": "^1.7.0",
     "ng-packagr": "^5.4.0",
+    "ngx-build-plus": "^8.1.5",
     "prettier": "^1.17.0",
     "protractor": "~5.4.0",
     "ts-node": "~7.0.0",
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
index 3bad5ee..c447a59 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
@@ -36,8 +36,10 @@
   <div class="paragraph-area">
     <div class="paragraph-inner" nz-row>
       <zeppelin-notebook-paragraph nz-col
-                                   *ngFor="let p of note.paragraphs;let first = first; let last = last;"
+                                   *ngFor="let p of note.paragraphs;let first = first; let last = last; index as i"
                                    [nzSpan]="p.config.colWidth * 2"
+                                   [select]="p.id === selectId"
+                                   [index]="i"
                                    [paragraph]="p"
                                    [note]="note"
                                    [looknfeel]="note.config.looknfeel"
@@ -47,6 +49,8 @@
                                    [revisionView]="revisionView"
                                    [first]="first"
                                    [last]="last"
+                                   (selectAtIndex)="onSelectAtIndex($event)"
+                                   (selected)="onParagraphSelect($event)"
                                    (triggerSaveParagraph)="saveParagraph($event)"
                                    (saveNoteTimer)="startSaveTimer()"></zeppelin-notebook-paragraph>
     </div>
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
index a894971..97479a7 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
@@ -21,7 +21,7 @@ import {
 } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 import { isNil } from 'lodash';
-import { Subject } from 'rxjs';
+import { Subject} from 'rxjs';
 import { distinctUntilKeyChanged, takeUntil } from 'rxjs/operators';
 
 import { MessageListener, MessageListenersManager } from '@zeppelin/core';
@@ -48,6 +48,7 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
   private destroy$ = new Subject();
   note: Note['note'];
   permissions: Permissions;
+  selectId: string | null = null;
   isOwner = true;
   noteRevisions: RevisionListItem[] = [];
   currentRevision: string;
@@ -216,6 +217,17 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
     }, 10000);
   }
 
+  onParagraphSelect(id: string) {
+    this.selectId = id;
+  }
+
+  onSelectAtIndex(index: number) {
+    const scopeIndex = Math.min(this.note.paragraphs.length, Math.max(0, index));
+    if (this.note.paragraphs[scopeIndex]) {
+      this.selectId = this.note.paragraphs[scopeIndex].id;
+    }
+  }
+
   saveNote() {
     if (this.note && this.note.paragraphs && this.listOfNotebookParagraphComponent) {
       this.listOfNotebookParagraphComponent.toArray().forEach(p => {
@@ -276,7 +288,7 @@ export class NotebookComponent extends MessageListenersManager implements OnInit
     private noteVarShareService: NoteVarShareService,
     private ticketService: TicketService,
     private securityService: SecurityService,
-    private router: Router
+    private router: Router,
   ) {
     super(messageService);
   }
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 6dabb4b..0711e81 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
@@ -55,6 +55,7 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro
   @Input() pid: string;
   @Output() readonly textChanged = new EventEmitter<string>();
   @Output() readonly editorBlur = new EventEmitter<void>();
+  @Output() readonly editorFocus = new EventEmitter<void>();
   private editor: IStandaloneCodeEditor;
   private monacoDisposables: IDisposable[] = [];
   height = 0;
@@ -76,6 +77,7 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro
     this.monacoDisposables.push(
       editor.onDidFocusEditorText(() => {
         this.ngZone.runOutsideAngular(() => {
+          this.editorFocus.emit();
           editor.updateOptions({ renderLineHighlight: 'all' });
         });
       }),
@@ -85,6 +87,7 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro
           editor.updateOptions({ renderLineHighlight: 'none' });
         });
       }),
+
       editor.onDidChangeModelContent(() => {
         this.ngZone.run(() => {
           this.text = editor.getModel().getValue();
@@ -123,6 +126,16 @@ export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestro
       });
     }
 
+    this.editor.addCommand(
+      monaco.KeyCode.Escape,
+      () => {
+        if (document.activeElement instanceof HTMLElement) {
+          document.activeElement.blur();
+        }
+      },
+      '!suggestWidgetVisible'
+    );
+
     this.updateEditorOptions();
     this.setParagraphMode();
     this.initEditorListener();
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts
index 085fa26..5b95953 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/control/control.component.ts
@@ -71,6 +71,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges {
   @Output() readonly runAllAbove = new EventEmitter<void>();
   @Output() readonly runAllBelowAndCurrent = new EventEmitter<void>();
   @Output() readonly cloneParagraph = new EventEmitter<void>();
+  @Output() readonly removeParagraph = new EventEmitter<void>();
   fontSizeOption = [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
   dropdownVisible = false;
   isMac = navigator.appVersion.indexOf('Mac') !== -1;
@@ -190,7 +191,7 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges {
         show: this.paragraphLength > 1,
         disabled: this.isEntireNoteRunning,
         icon: 'delete',
-        trigger: () => this.removeParagraph(),
+        trigger: () => this.onRemoveParagraph(),
         shortCut: `Ctrl+${this.isMac ? 'Option' : 'Alt'}+D`,
         keyBindings: monaco ? [monaco.KeyMod.WinCtrl | monaco.KeyMod.Alt | monaco.KeyCode.KEY_D] : []
       }
@@ -258,25 +259,8 @@ export class NotebookParagraphControlComponent implements OnInit, OnChanges {
     }
   }
 
-  removeParagraph() {
-    if (!this.isEntireNoteRunning) {
-      if (this.paragraphLength === 1) {
-        this.nzModalService.warning({
-          nzTitle: `Warning`,
-          nzContent: `All the paragraphs can't be deleted`
-        });
-      } else {
-        this.nzModalService.confirm({
-          nzTitle: 'Delete Paragraph',
-          nzContent: 'Do you want to delete this paragraph?',
-          nzOnOk: () => {
-            this.messageService.paragraphRemove(this.pid);
-            this.cdr.markForCheck();
-            // TODO(hsuanxyz) moveFocusToNextParagraph
-          }
-        });
-      }
-    }
+  onRemoveParagraph() {
+    this.removeParagraph.emit();
   }
 
   trigger(event: EventEmitter<void>) {
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 861f955..1c42f87 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
@@ -14,7 +14,10 @@
   <zeppelin-notebook-add-paragraph *ngIf="!revisionView && looknfeel !== 'report'"
                                    [disabled]="isEntireNoteRunning"
                                    (addParagraph)="insertParagraph('above')"></zeppelin-notebook-add-paragraph>
-  <div class="paragraph" [class.simple]="looknfeel !== 'default'" [class.report]="looknfeel === 'report'">
+  <div class="paragraph"
+       [class.focused]="select"
+       [class.simple]="looknfeel !== 'default'"
+       [class.report]="looknfeel === 'report'">
     <zeppelin-elastic-input *ngIf="paragraph.config.title"
                             [value]="paragraph.title"
                             [min]="true"
@@ -39,6 +42,7 @@
                                          [(editorHide)]="paragraph.config.editorHide"
                                          [(runOnSelectionChange)]="paragraph.config.runOnSelectionChange"
                                          (tableHideChange)="commitParagraph()"
+                                         (removeParagraph)="removeParagraph()"
                                          (colWidthChange)="changeColWidth(true)"
                                          (fontSizeChange)="commitParagraph()"
                                          (editorHideChange)="commitParagraph()"
@@ -65,7 +69,8 @@
                                              [lineNumbers]="paragraph.config.lineNumbers"
                                              [readOnly]="isEntireNoteRunning || isParagraphRunning || revisionView"
                                              [language]="paragraph.config.editorSetting?.language"
-                                             (editorBlur)="saveParagraph()"
+                                             (editorBlur)="onEditorBlur()"
+                                             (editorFocus)="onEditorFocus()"
                                              (textChanged)="textChanged($event)"></zeppelin-notebook-paragraph-code-editor>
     <zeppelin-notebook-paragraph-progress *ngIf="paragraph.status === 'RUNNING'"
                                           [progress]="progress"></zeppelin-notebook-paragraph-progress>
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.less b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.less
index f24d693..60cc5ac 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.less
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.less
@@ -18,6 +18,7 @@
 }
 
 .themeMixin({
+
   .paragraph {
     background: @component-background;
     border: 1px solid @border-color-split;
@@ -25,6 +26,10 @@
     padding: 32px 12px 12px 12px;
     position: relative;
 
+    &.focused {
+      box-shadow: 0 0 5px rgba(0, 0, 0, 0.46);
+    }
+
     zeppelin-notebook-paragraph-code-editor + zeppelin-notebook-paragraph-dynamic-forms {
       margin-top: 24px;
     }
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 21d57bb..1dde62d 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
@@ -13,7 +13,7 @@
 import {
   ChangeDetectionStrategy,
   ChangeDetectorRef,
-  Component,
+  Component, ElementRef,
   EventEmitter,
   Input,
   OnChanges,
@@ -21,15 +21,16 @@ import {
   OnInit,
   Output,
   QueryList,
+  SimpleChanges,
   ViewChild,
   ViewChildren
 } from '@angular/core';
-import { Subject } from 'rxjs';
-import { takeUntil } from 'rxjs/operators';
+import {merge, Observable, Subject} from 'rxjs';
+import {map, takeUntil} from 'rxjs/operators';
 
 import DiffMatchPatch from 'diff-match-patch';
 import { isEmpty, isEqual } from 'lodash';
-import { NzModalService } from 'ng-zorro-antd';
+import { NzModalService } from 'ng-zorro-antd/modal';
 
 import { MessageListener, MessageListenersManager } from '@zeppelin/core';
 import {
@@ -52,7 +53,10 @@ import {
   NgZService,
   NoteStatusService,
   NoteVarShareService,
-  ParagraphStatus
+  ParagraphActions,
+  ParagraphStatus,
+  ShortcutsMap,
+  ShortcutService
 } from '@zeppelin/services';
 import { SpellResult } from '@zeppelin/spell/spell-result';
 
@@ -60,10 +64,16 @@ import { NzResizeEvent } from 'ng-zorro-antd/resizable';
 import { NotebookParagraphCodeEditorComponent } from './code-editor/code-editor.component';
 import { NotebookParagraphResultComponent } from './result/result.component';
 
+type Mode = 'edit' | 'command';
+
 @Component({
   selector: 'zeppelin-notebook-paragraph',
   templateUrl: './paragraph.component.html',
   styleUrls: ['./paragraph.component.less'],
+  host: {
+    'tabindex': '-1',
+    '(focusin)': 'onFocus()'
+  },
   changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class NotebookParagraphComponent extends MessageListenersManager implements OnInit, OnChanges, OnDestroy {
@@ -76,6 +86,8 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
   @Input() note: Note['note'];
   @Input() looknfeel: string;
   @Input() revisionView: boolean;
+  @Input() select: boolean = false;
+  @Input() index: number = -1;
   @Input() viewOnly: boolean;
   @Input() last: boolean;
   @Input() collaborativeMode = false;
@@ -83,8 +95,12 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
   @Input() interpreterBindings: InterpreterBindingItem[] = [];
   @Output() readonly saveNoteTimer = new EventEmitter();
   @Output() readonly triggerSaveParagraph = new EventEmitter<string>();
+  @Output() readonly selected = new EventEmitter<string>();
+  @Output() readonly selectAtIndex = new EventEmitter<number>();
 
   private destroy$ = new Subject();
+  private mode: Mode = 'command';
+  waitConfirmFromEdit = false;
   dirtyText: string;
   originalText: string;
   isEntireNoteRunning = false;
@@ -185,6 +201,19 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
     }
   }
 
+  switchMode(mode: Mode): void {
+    if (mode === this.mode) {
+      return;
+    }
+    this.mode = mode;
+    if (mode === 'edit') {
+      this.focusEditor();
+    } else {
+      this.blurEditor();
+      (this.host.nativeElement as HTMLElement).focus();
+    }
+  }
+
   updateParagraph(oldPara: ParagraphItem, newPara: ParagraphItem, updateCallback: () => void) {
     // 1. can't update on revision view
     if (!this.revisionView) {
@@ -237,6 +266,34 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
     this.saveNoteTimer.emit();
   }
 
+  onFocus() {
+    this.selected.emit(this.paragraph.id);
+  }
+
+  focusEditor() {
+    this.paragraph.focus = true;
+    this.saveParagraph();
+    this.cdr.markForCheck();
+  }
+
+  blurEditor() {
+    this.paragraph.focus = false;
+    (this.host.nativeElement as HTMLElement).focus();
+    this.saveParagraph();
+    this.cdr.markForCheck();
+  }
+
+  onEditorFocus() {
+    this.switchMode('edit');
+  }
+
+  onEditorBlur() {
+    // Ignore events triggered by open the confirm box in edit mode
+    if (!this.waitConfirmFromEdit) {
+      this.switchMode('command');
+    }
+  }
+
   saveParagraph() {
     const dirtyText = this.paragraph.text;
     if (dirtyText === undefined || dirtyText === this.originalText) {
@@ -248,6 +305,27 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
     this.cdr.markForCheck();
   }
 
+  removeParagraph() {
+    if (!this.isEntireNoteRunning) {
+      if (this.note.paragraphs.length === 1) {
+        this.nzModalService.warning({
+          nzTitle: `Warning`,
+          nzContent: `All the paragraphs can't be deleted`
+        });
+      } else {
+        this.nzModalService.confirm({
+          nzTitle: 'Delete Paragraph',
+          nzContent: 'Do you want to delete this paragraph?',
+          nzOnOk: () => {
+            this.messageService.paragraphRemove(this.paragraph.id);
+            this.cdr.markForCheck();
+            // TODO(hsuanxyz) moveFocusToNextParagraph
+          }
+        });
+      }
+    }
+  }
+
   runAllAbove() {
     const index = this.note.paragraphs.findIndex(p => p.id === this.paragraph.id);
     const toRunParagraphs = this.note.paragraphs.filter((p, i) => i < index);
@@ -298,7 +376,11 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
       nzOnOk: () => {
         this.messageService.runAllParagraphs(this.note.id, paragraphs);
       }
-    });
+    }).afterClose
+      .pipe(takeUntil(this.destroy$))
+      .subscribe(() => {
+        this.waitConfirmFromEdit = false;
+      });
     // TODO(hsuanxyz): save cursor
   }
 
@@ -486,7 +568,6 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
   setTitle(title: string) {
     this.paragraph.title = title;
     this.commitParagraph();
-    this.cdr.markForCheck();
   }
 
   commitParagraph() {
@@ -498,6 +579,7 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
       settings: { params }
     } = this.paragraph;
     this.messageService.commitParagraph(id, title, text, config, params, this.note.id);
+    this.cdr.markForCheck();
   }
 
   initializeDefault(config: ParagraphConfig) {
@@ -586,7 +668,6 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
   onConfigChange(configResult: ParagraphConfigResult, index: number) {
     this.paragraph.config.results[index] = configResult;
     this.commitParagraph();
-    this.cdr.markForCheck();
   }
 
   setEditorHide(editorHide: boolean) {
@@ -610,12 +691,128 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
     private nzModalService: NzModalService,
     private noteVarShareService: NoteVarShareService,
     private cdr: ChangeDetectorRef,
-    private ngZService: NgZService
+    private ngZService: NgZService,
+    private shortcutService: ShortcutService,
+    private host: ElementRef
   ) {
     super(messageService);
   }
 
   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
+            }
+          }))
+        );
+      });
+    });
+
+    merge<{
+      action: ParagraphActions,
+      event: KeyboardEvent
+    }>(...observables)
+      .pipe(takeUntil(this.destroy$))
+      .subscribe(({action, event}) => {
+      if (this.mode === 'command') {
+        switch (action) {
+          case ParagraphActions.InsertAbove:
+            this.insertParagraph('above');
+            break;
+          case ParagraphActions.InsertBelow:
+            this.insertParagraph('below');
+            break;
+          case ParagraphActions.SwitchEditorShow:
+            this.setEditorHide(!this.paragraph.config.editorHide);
+            this.commitParagraph();
+            break;
+          case ParagraphActions.SwitchOutputShow:
+            this.setTableHide(!this.paragraph.config.tableHide);
+            this.commitParagraph();
+            break;
+          case ParagraphActions.SwitchTitleShow:
+            this.paragraph.config.title = !this.paragraph.config.title;
+            this.commitParagraph();
+            break;
+          case ParagraphActions.SwitchLineNumber:
+            this.paragraph.config.lineNumbers = !this.paragraph.config.lineNumbers;
+            this.commitParagraph();
+            break;
+          case ParagraphActions.MoveToUp:
+            this.moveUpParagraph();
+            break;
+          case ParagraphActions.MoveToDown:
+            this.moveDownParagraph();
+            break;
+          case ParagraphActions.SwitchEnable:
+            this.paragraph.config.enabled = !this.paragraph.config.enabled;
+            this.commitParagraph();
+            break;
+          case ParagraphActions.ReduceWidth:
+            this.paragraph.config.colWidth = Math.max(1, this.paragraph.config.colWidth - 1);
+            this.cdr.markForCheck();
+            this.changeColWidth(true);
+            break;
+          case ParagraphActions.IncreaseWidth:
+            this.paragraph.config.colWidth = Math.min(12, this.paragraph.config.colWidth + 1);
+            this.cdr.markForCheck();
+            this.changeColWidth(true);
+            break;
+          case ParagraphActions.Delete:
+            this.removeParagraph();
+            break;
+          case ParagraphActions.SelectAbove:
+            event.preventDefault();
+            this.selectAtIndex.emit(this.index - 1);
+            break;
+          case ParagraphActions.SelectBelow:
+            event.preventDefault();
+            this.selectAtIndex.emit(this.index + 1);
+            break;
+          default:
+            break;
+        }
+      }
+      switch (action) {
+        case ParagraphActions.EditMode:
+          if (this.mode === 'command') {
+            event.preventDefault();
+          }
+          if (!this.paragraph.config.editorHide) {
+            this.switchMode('edit');
+          }
+          break;
+        case ParagraphActions.Run:
+          event.preventDefault();
+          this.runParagraph();
+          break;
+        case ParagraphActions.RunBelow:
+          this.waitConfirmFromEdit = true;
+          this.runAllBelowAndCurrent();
+          break;
+        case ParagraphActions.Cancel:
+          event.preventDefault();
+          this.cancelParagraph();
+          break;
+        default:
+          break;
+      }
+    });
+
     this.setResults();
     this.originalText = this.paragraph.text;
     this.isEntireNoteRunning = this.noteStatusService.isEntireNoteRunning(this.note);
@@ -644,7 +841,17 @@ export class NotebookParagraphComponent extends MessageListenersManager implemen
       });
   }
 
-  ngOnChanges(): void {}
+  ngOnChanges(changes: SimpleChanges): void {
+    const { index, select } = changes;
+    if (index && index.currentValue !== index.previousValue && this.select
+    || select && select.currentValue === true && select.previousValue !== true) {
+      if (this.host.nativeElement) {
+        setTimeout(() => {
+          (this.host.nativeElement as HTMLElement).focus();
+        })
+      }
+    }
+  }
 
   ngOnDestroy(): void {
     super.ngOnDestroy();
diff --git a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts
index 850bc18..903f72b 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/result/result.component.ts
@@ -254,6 +254,9 @@ export class NotebookParagraphResultComponent implements OnInit, AfterViewInit,
   }
 
   setGraphConfig() {
+    if (!this.config || !this.config.graph) {
+      return;
+    }
     const visualizationItem = this.visualizations.find(v => v.id === this.config.graph.mode);
     if (!visualizationItem || !visualizationItem.instance) {
       return;
diff --git a/zeppelin-web-angular/src/app/services/public-api.ts b/zeppelin-web-angular/src/app/services/public-api.ts
index d56ec7e..d6709ac 100644
--- a/zeppelin-web-angular/src/app/services/public-api.ts
+++ b/zeppelin-web-angular/src/app/services/public-api.ts
@@ -26,3 +26,4 @@ export * from './ng-z.service';
 export * from './array-ordering.service';
 export * from './note-list.service';
 export * from './runtime-compiler.service';
+export * from './shortcut.service';
diff --git a/zeppelin-web-angular/src/app/services/shortcut.service.ts b/zeppelin-web-angular/src/app/services/shortcut.service.ts
new file mode 100644
index 0000000..b6d6a3a
--- /dev/null
+++ b/zeppelin-web-angular/src/app/services/shortcut.service.ts
@@ -0,0 +1,109 @@
+import {DOCUMENT} from "@angular/common";
+import {Inject, Injectable} from '@angular/core';
+import {EventManager} from "@angular/platform-browser";
+import {Observable} from "rxjs";
+
+export enum ParagraphActions {
+  EditMode = 'Paragraph:EditMode',
+  CommandMode = 'Paragraph:CommandMode',
+  Run = 'Paragraph:Run',
+  RunBelow = 'Paragraph:RunBelow',
+  Cancel = 'Paragraph:Cancel',
+  Clear = 'Paragraph:Clear',
+  ReduceWidth = 'Paragraph:ReduceWidth',
+  IncreaseWidth = 'Paragraph:IncreaseWidth',
+  Delete = 'Paragraph:Delete',
+  MoveToUp = 'Paragraph:MoveToUp',
+  MoveToDown = 'Paragraph:MoveToDown',
+  SelectAbove = 'Paragraph:SelectAbove',
+  SelectBelow = 'Paragraph:SelectBelow',
+  InsertAbove = 'Paragraph:InsertAbove',
+  InsertBelow = 'Paragraph:InsertBelow',
+  SwitchLineNumber = 'Paragraph:SwitchLineNumber',
+  SwitchTitleShow = 'Paragraph:SwitchTitleShow',
+  SwitchOutputShow = 'Paragraph:SwitchOutputShow',
+  SwitchEditorShow = 'Paragraph:SwitchEditorShow',
+  SwitchEnable = 'Paragraph:SwitchEnable'
+}
+
+export const ShortcutsMap = {
+  [ParagraphActions.EditMode]: 'enter',
+  [ParagraphActions.CommandMode]: 'esc',
+  [ParagraphActions.Run]: 'shift.enter',
+  [ParagraphActions.RunBelow]: 'shift.ctrlCmd.enter',
+  [ParagraphActions.Cancel]: 'shift.ctrlCmd.c',
+  // Need register special character `¬` in MacOS
+  [ParagraphActions.Clear]: ['alt.ctrlCmd.l', 'alt.ctrlCmd.¬'],
+  // Need register special character `®` in MacOS
+  [ParagraphActions.SwitchEnable]: ['alt.ctrlCmd.r', 'alt.ctrlCmd.®'],
+  // Need register special character `–` in MacOS
+  [ParagraphActions.ReduceWidth]: ['alt.ctrlCmd.-', 'alt.ctrlCmd.–'],
+  // Need register special character `≠` in MacOS
+  [ParagraphActions.IncreaseWidth]: ['alt.ctrlCmd.+', 'alt.ctrlCmd.≠'],
+  [ParagraphActions.Delete]: 'shift.delete',
+  [ParagraphActions.MoveToUp]: ['ctrlCmd.k', 'ctrlCmd.arrowup'],
+  [ParagraphActions.MoveToDown]: ['ctrlCmd.j', 'ctrlCmd.arrowdown'],
+  [ParagraphActions.SelectAbove]: ['k', 'arrowup'],
+  [ParagraphActions.SelectBelow]: ['j', 'arrowdown'],
+  [ParagraphActions.SwitchLineNumber]: 'l',
+  [ParagraphActions.SwitchTitleShow]: 't',
+  [ParagraphActions.SwitchOutputShow]: 'o',
+  [ParagraphActions.SwitchEditorShow]: 'e',
+  [ParagraphActions.InsertAbove]: 'a',
+  [ParagraphActions.InsertBelow]: 'b'
+};
+
+export interface ShortcutEvent {
+  event: KeyboardEvent
+  keybindings: string;
+}
+
+export interface ShortcutOption {
+  scope?: HTMLElement,
+  keybindings: string
+}
+
+function isMacOS() {
+  return navigator.platform.indexOf('Mac') > -1
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ShortcutService {
+
+  private element: HTMLElement;
+
+  constructor(private eventManager: EventManager,
+              @Inject(DOCUMENT) _document: any) {
+    this.element = _document;
+  }
+
+  forkByElement(element: HTMLElement) {
+    return new ShortcutService(this.eventManager, element);
+  }
+
+  bindShortcut(option: ShortcutOption): Observable<ShortcutEvent> {
+    const host = option.scope || this.element;
+    // `ctrlCmd` is special symbol, will be replaced `meta` in MacOS, 'control' in Windows/Linux
+    const keybindings = option.keybindings
+      .replace(/ctrlCmd/g, isMacOS() ? 'meta' : 'control');
+    const event = `keydown.${keybindings}`;
+    let dispose: Function;
+    return new Observable<ShortcutEvent>(observer => {
+      const handler = event => {
+        observer.next({
+          event,
+          keybindings: option.keybindings
+        });
+      };
+
+      dispose = this.eventManager.addEventListener(host, event, handler);
+
+      return () => {
+        dispose();
+      };
+    })
+  }
+
+}