Преглед изворни кода

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 година
родитељ
комит
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.nextExecutable = undefined;
+
+    this.obseverState = {};
+
+    this.lost = false;
   }
 
   setStatus(status) {

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

@@ -64,6 +64,7 @@ class Executor {
         });
         if (allExecutablesIndex[executable.getKey()]) {
           executable = allExecutablesIndex[executable.getKey()];
+          executable.parsedStatement = parsedStatement;
           delete allExecutablesIndex[executable.getKey()];
         }
         if (
@@ -89,6 +90,7 @@ class Executor {
 
     // Cancel any "lost" executables and any batch chain it's part of
     executables.lost.forEach(lostExecutable => {
+      lostExecutable.lost = true;
       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 { EXECUTABLE_UPDATED_EVENT } from 'apps/notebook2/execution/executable';
 import { ACTIVE_STATEMENT_CHANGED_EVENT } from 'ko/bindings/ace/aceLocationHandler';
+import AceAnchoredRange from 'ko/bindings/ace/aceAnchoredRange';
 
 // TODO: depends on Ace
 
@@ -27,6 +28,7 @@ const ACTIVE_CSS = 'ace-active-gutter-decoration';
 const COMPLETED_CSS = 'ace-completed-gutter-decoration';
 const EXECUTING_CSS = 'ace-executing-gutter-decoration';
 const FAILED_CSS = 'ace-failed-gutter-decoration';
+const FAILED_MARKER_CSS = 'ace-failed-marker';
 
 const getLeadingEmptyLineCount = parsedStatement => {
   let leadingEmptyLineCount = 0;
@@ -40,29 +42,6 @@ const getLeadingEmptyLineCount = parsedStatement => {
   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 {
   constructor(options) {
     this.editor = options.editor;
@@ -71,7 +50,8 @@ export default class AceGutterHandler {
 
     this.disposals = [];
 
-    const previouslyMarkedActiveLines = [];
+    const activeStatementAnchor = new AceAnchoredRange(this.editor);
+    activeStatementAnchor.addGutterCss(ACTIVE_CSS);
 
     const changedSubscription = huePubSub.subscribe(
       ACTIVE_STATEMENT_CHANGED_EVENT,
@@ -79,44 +59,50 @@ export default class AceGutterHandler {
         if (statementDetails.id !== this.editorId || !statementDetails.activeStatement) {
           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(() => {
       changedSubscription.remove();
+      activeStatementAnchor.dispose();
     });
 
     if (this.executor) {
-      const session = this.editor.getSession();
-      const AceRange = window.ace.require('ace/range').Range;
-
       const executableSub = huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
         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);
+          }
         }
       });
 

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue.css


Разлика између датотеке није приказан због своје велике величине
+ 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-failed-marker {
+      position: absolute;
+      width: 100% !important;
+      margin-left: -3px;
+    }
+
+    .ace-failed-marker,
     .ace_error-line {
       background-color: @cui-pink-050 !important;
       opacity: 0.5;
@@ -328,6 +335,13 @@ input[type='password']::-ms-reveal {
   }
 
   .ace_marker-layer {
+    .ace-failed-marker {
+      position: absolute;
+      width: 100% !important;
+      margin-left: -3px;
+    }
+
+    .ace-failed-marker,
     .ace_error-line {
       background-color: @cui-pink-050 !important;
       opacity: 0.5;

Неке датотеке нису приказане због велике количине промена