Browse Source

HUE-9028 [editor] Anchor gutter and range markings in the Ace editor for executables

With this change any gutter or range markings are kept for unaltered statements when editing surrounding statements.
Johan Ahlen 6 years ago
parent
commit
810f6fac5d

+ 4 - 0
desktop/core/src/desktop/js/apps/notebook2/execution/executable.js

@@ -69,6 +69,10 @@ export default class Executable {
 
 
     this.previousExecutable = undefined;
     this.previousExecutable = undefined;
     this.nextExecutable = undefined;
     this.nextExecutable = undefined;
+
+    this.obseverState = {};
+
+    this.lost = false;
   }
   }
 
 
   setStatus(status) {
   setStatus(status) {

+ 2 - 0
desktop/core/src/desktop/js/apps/notebook2/execution/executor.js

@@ -64,6 +64,7 @@ class Executor {
         });
         });
         if (allExecutablesIndex[executable.getKey()]) {
         if (allExecutablesIndex[executable.getKey()]) {
           executable = allExecutablesIndex[executable.getKey()];
           executable = allExecutablesIndex[executable.getKey()];
+          executable.parsedStatement = parsedStatement;
           delete allExecutablesIndex[executable.getKey()];
           delete allExecutablesIndex[executable.getKey()];
         }
         }
         if (
         if (
@@ -89,6 +90,7 @@ class Executor {
 
 
     // Cancel any "lost" executables and any batch chain it's part of
     // Cancel any "lost" executables and any batch chain it's part of
     executables.lost.forEach(lostExecutable => {
     executables.lost.forEach(lostExecutable => {
+      lostExecutable.lost = true;
       lostExecutable.cancelBatchChain();
       lostExecutable.cancelBatchChain();
     });
     });
 
 

+ 121 - 0
desktop/core/src/desktop/js/ko/bindings/ace/aceAnchoredRange.js

@@ -0,0 +1,121 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you 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.
+
+// TODO: depends on Ace
+
+const clearGutterCss = (cssClass, session, startRow, endRow) => {
+  for (let row = startRow; row <= endRow; row++) {
+    session.removeGutterDecoration(row, cssClass);
+  }
+};
+
+const setGuttercss = (cssClass, session, startRow, endRow) => {
+  for (let row = startRow; row <= endRow; row++) {
+    session.addGutterDecoration(row, cssClass);
+  }
+};
+
+export default class AceAnchoredRange {
+  constructor(editor) {
+    this.id = Math.random();
+    this.editor = editor;
+    const doc = this.editor.getSession().doc;
+    this.startAnchor = doc.createAnchor(0, 0);
+    this.endAnchor = doc.createAnchor(0, 0);
+    this.changed = false;
+
+    this.markerCssClasses = {};
+    this.gutterCssClasses = {};
+    this.refreshThrottle = -1;
+
+    this.attachChangeHandler();
+  }
+
+  attachChangeHandler() {
+    const throttledRefresh = () => {
+      window.clearTimeout(this.refreshThrottle);
+      this.refreshThrottle = window.setTimeout(this.refresh.bind(this), 10);
+    };
+
+    this.startAnchor.on('change', throttledRefresh);
+    this.endAnchor.on('change', throttledRefresh);
+  }
+
+  refresh() {
+    const session = this.editor.getSession();
+    const newStart = this.startAnchor.getPosition();
+    const newEnd = this.endAnchor.getPosition();
+
+    Object.keys(this.gutterCssClasses).forEach(cssClass => {
+      const rowSpan = this.gutterCssClasses[cssClass];
+      clearGutterCss(cssClass, session, rowSpan.start, rowSpan.end);
+      rowSpan.start = newStart.row;
+      rowSpan.end = newEnd.row;
+      setGuttercss(cssClass, session, rowSpan.start, rowSpan.end);
+    });
+  }
+
+  move(parseLocation, leadingEmptyLineCount) {
+    const lastRow = parseLocation.last_line - 1;
+    const firstRow = Math.min(lastRow, parseLocation.first_line - 1 + (leadingEmptyLineCount || 0));
+    const firstCol = leadingEmptyLineCount ? 0 : parseLocation.first_column;
+    this.startAnchor.setPosition(firstRow, firstCol);
+    this.endAnchor.setPosition(lastRow, parseLocation.last_column);
+  }
+
+  addGutterCss(cssClass) {
+    const session = this.editor.getSession();
+    const startRow = this.startAnchor.getPosition().row;
+    const endRow = this.endAnchor.getPosition().row;
+    this.gutterCssClasses[cssClass] = { start: startRow, end: endRow };
+    setGuttercss(cssClass, session, startRow, endRow);
+  }
+
+  addMarkerCss(cssClass) {
+    if (!this.markerCssClasses[cssClass]) {
+      const AceRange = ace.require('ace/range').Range;
+      const range = new AceRange(0, 0, 0, 0);
+      range.start = this.startAnchor;
+      range.end = this.endAnchor;
+      this.markerCssClasses[cssClass] = this.editor.getSession().addMarker(range, cssClass);
+    }
+  }
+
+  removeMarkerCss(cssClass) {
+    if (this.markerCssClasses[cssClass]) {
+      this.editor.getSession().removeMarker(this.markerCssClasses[cssClass]);
+      delete this.markerCssClasses[cssClass];
+    }
+  }
+
+  removeGutterCss(cssClass) {
+    if (this.gutterCssClasses[cssClass]) {
+      const session = this.editor.getSession();
+      const rowSpan = this.gutterCssClasses[cssClass];
+      delete this.gutterCssClasses[cssClass];
+      clearGutterCss(cssClass, session, rowSpan.start, rowSpan.end);
+    }
+  }
+
+  dispose() {
+    window.clearTimeout(this.refreshThrottle);
+    this.startAnchor.detach();
+    this.endAnchor.detach();
+
+    Object.keys(this.gutterCssClasses).forEach(this.removeGutterCss.bind(this));
+    Object.keys(this.markerCssClasses).forEach(this.removeMarkerCss.bind(this));
+  }
+}

+ 37 - 51
desktop/core/src/desktop/js/ko/bindings/ace/aceGutterHandler.js

@@ -17,6 +17,7 @@
 import huePubSub from 'utils/huePubSub';
 import huePubSub from 'utils/huePubSub';
 import { EXECUTABLE_UPDATED_EVENT } from 'apps/notebook2/execution/executable';
 import { EXECUTABLE_UPDATED_EVENT } from 'apps/notebook2/execution/executable';
 import { ACTIVE_STATEMENT_CHANGED_EVENT } from 'ko/bindings/ace/aceLocationHandler';
 import { ACTIVE_STATEMENT_CHANGED_EVENT } from 'ko/bindings/ace/aceLocationHandler';
+import AceAnchoredRange from 'ko/bindings/ace/aceAnchoredRange';
 
 
 // TODO: depends on Ace
 // TODO: depends on Ace
 
 
@@ -27,6 +28,7 @@ const ACTIVE_CSS = 'ace-active-gutter-decoration';
 const COMPLETED_CSS = 'ace-completed-gutter-decoration';
 const COMPLETED_CSS = 'ace-completed-gutter-decoration';
 const EXECUTING_CSS = 'ace-executing-gutter-decoration';
 const EXECUTING_CSS = 'ace-executing-gutter-decoration';
 const FAILED_CSS = 'ace-failed-gutter-decoration';
 const FAILED_CSS = 'ace-failed-gutter-decoration';
+const FAILED_MARKER_CSS = 'ace-failed-marker';
 
 
 const getLeadingEmptyLineCount = parsedStatement => {
 const getLeadingEmptyLineCount = parsedStatement => {
   let leadingEmptyLineCount = 0;
   let leadingEmptyLineCount = 0;
@@ -40,29 +42,6 @@ const getLeadingEmptyLineCount = parsedStatement => {
   return leadingEmptyLineCount;
   return leadingEmptyLineCount;
 };
 };
 
 
-const forEachLine = (statement, callback) => {
-  const leadingEmptyLineCount = getLeadingEmptyLineCount(statement);
-  let line = statement.location.first_line - 1 + leadingEmptyLineCount;
-  for (line; line < statement.location.last_line; line++) {
-    callback(line);
-  }
-};
-
-const clearErrorForLine = (session, line) => {
-  const markers = session.getMarkers(false);
-  Object.keys(markers).some(key => {
-    const marker = markers[key];
-    if (
-      marker.clazz === 'ace_error-line' &&
-      marker.range.start.row <= line &&
-      line <= marker.range.end.row
-    ) {
-      session.removeMarker(marker.id);
-      return true;
-    }
-  });
-};
-
 export default class AceGutterHandler {
 export default class AceGutterHandler {
   constructor(options) {
   constructor(options) {
     this.editor = options.editor;
     this.editor = options.editor;
@@ -71,7 +50,8 @@ export default class AceGutterHandler {
 
 
     this.disposals = [];
     this.disposals = [];
 
 
-    const previouslyMarkedActiveLines = [];
+    const activeStatementAnchor = new AceAnchoredRange(this.editor);
+    activeStatementAnchor.addGutterCss(ACTIVE_CSS);
 
 
     const changedSubscription = huePubSub.subscribe(
     const changedSubscription = huePubSub.subscribe(
       ACTIVE_STATEMENT_CHANGED_EVENT,
       ACTIVE_STATEMENT_CHANGED_EVENT,
@@ -79,44 +59,50 @@ export default class AceGutterHandler {
         if (statementDetails.id !== this.editorId || !statementDetails.activeStatement) {
         if (statementDetails.id !== this.editorId || !statementDetails.activeStatement) {
           return;
           return;
         }
         }
-
-        const session = this.editor.getSession();
-        while (previouslyMarkedActiveLines.length) {
-          session.removeGutterDecoration(previouslyMarkedActiveLines.shift(), ACTIVE_CSS);
-        }
-
-        forEachLine(statementDetails.activeStatement, line => {
-          previouslyMarkedActiveLines.push(line);
-          session.addGutterDecoration(line, ACTIVE_CSS);
-        });
+        const leadingEmptyLineCount = getLeadingEmptyLineCount(statementDetails.activeStatement);
+        activeStatementAnchor.move(
+          statementDetails.activeStatement.location,
+          leadingEmptyLineCount
+        );
       }
       }
     );
     );
 
 
     this.disposals.push(() => {
     this.disposals.push(() => {
       changedSubscription.remove();
       changedSubscription.remove();
+      activeStatementAnchor.dispose();
     });
     });
 
 
     if (this.executor) {
     if (this.executor) {
-      const session = this.editor.getSession();
-      const AceRange = window.ace.require('ace/range').Range;
-
       const executableSub = huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
       const executableSub = huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
         if (executable.executor === this.executor) {
         if (executable.executor === this.executor) {
-          const statement = executable.parsedStatement;
-          forEachLine(statement, line => {
-            session.removeGutterDecoration(line, COMPLETED_CSS);
-            session.removeGutterDecoration(line, EXECUTING_CSS);
-            clearErrorForLine(session, line);
-            if (executable.isRunning()) {
-              session.addGutterDecoration(line, EXECUTING_CSS);
-            } else if (executable.isSuccess()) {
-              session.addGutterDecoration(line, COMPLETED_CSS);
-            } else if (executable.isFailed()) {
-              const range = new AceRange(line, 0, line, session.getLine(line).length);
-              session.addMarker(range, 'ace_error-line');
-              session.addGutterDecoration(line, FAILED_CSS);
+          if (executable.lost) {
+            if (executable.obseverState.aceAnchor) {
+              executable.obseverState.aceAnchor.dispose();
+              delete executable.obseverState.aceAnchor;
             }
             }
-          });
+            return;
+          }
+
+          const statement = executable.parsedStatement;
+          if (!executable.obseverState.aceAnchor) {
+            executable.obseverState.aceAnchor = new AceAnchoredRange(this.editor);
+          }
+          const leadingEmptyLineCount = getLeadingEmptyLineCount(statement);
+          executable.obseverState.aceAnchor.move(statement.location, leadingEmptyLineCount);
+          const anchoredRange = executable.obseverState.aceAnchor;
+          anchoredRange.removeGutterCss(COMPLETED_CSS);
+          anchoredRange.removeGutterCss(EXECUTING_CSS);
+          anchoredRange.removeGutterCss(FAILED_CSS);
+          anchoredRange.removeMarkerCss(FAILED_MARKER_CSS);
+
+          if (executable.isRunning()) {
+            anchoredRange.addGutterCss(EXECUTING_CSS);
+          } else if (executable.isSuccess()) {
+            anchoredRange.addGutterCss(COMPLETED_CSS);
+          } else if (executable.isFailed()) {
+            anchoredRange.addMarkerCss(FAILED_MARKER_CSS);
+            anchoredRange.addGutterCss(FAILED_CSS);
+          }
         }
         }
       });
       });
 
 

File diff suppressed because it is too large
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue.css


File diff suppressed because it is too large
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue3-extra.css


+ 14 - 0
desktop/core/src/desktop/static/desktop/less/hue-cross-version.less

@@ -293,6 +293,13 @@ input[type='password']::-ms-reveal {
   }
   }
 
 
   .ace_marker-layer {
   .ace_marker-layer {
+    .ace-failed-marker {
+      position: absolute;
+      width: 100% !important;
+      margin-left: -3px;
+    }
+
+    .ace-failed-marker,
     .ace_error-line {
     .ace_error-line {
       background-color: @cui-pink-050 !important;
       background-color: @cui-pink-050 !important;
       opacity: 0.5;
       opacity: 0.5;
@@ -328,6 +335,13 @@ input[type='password']::-ms-reveal {
   }
   }
 
 
   .ace_marker-layer {
   .ace_marker-layer {
+    .ace-failed-marker {
+      position: absolute;
+      width: 100% !important;
+      margin-left: -3px;
+    }
+
+    .ace-failed-marker,
     .ace_error-line {
     .ace_error-line {
       background-color: @cui-pink-050 !important;
       background-color: @cui-pink-050 !important;
       opacity: 0.5;
       opacity: 0.5;

Some files were not shown because too many files changed in this diff