Procházet zdrojové kódy

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 před 6 roky
rodič
revize
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);
+          }
         }
       });
 

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue.css


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 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;

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů