Browse Source

HUE-9028 [editor] Add support for select to execute multiple in notebook 2

When selecting multiple statements it will execute all of them in order without stopping as we now store the result per statement. It's also possible to execute multiple chains in parallel by selecting non-intersecting chains.

If a previous selected execution chain is intersecting a new execution it will cancel the previous chain.
Johan Ahlen 6 years ago
parent
commit
8a93433e73

+ 6 - 4
desktop/core/src/desktop/js/apps/notebook2/components/ko.snippetExecuteActions.js → desktop/core/src/desktop/js/apps/notebook2/components/ko.executableActions.js

@@ -21,7 +21,7 @@ import I18n from 'utils/i18n';
 import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import DisposableComponent from 'ko/components/DisposableComponent';
 
 
-export const NAME = 'snippet-execute-actions';
+export const NAME = 'executable-actions';
 
 
 export const EXECUTE_ACTIVE_EXECUTABLE_EVENT = 'executable.active.executable';
 export const EXECUTE_ACTIVE_EXECUTABLE_EVENT = 'executable.active.executable';
 
 
@@ -57,7 +57,7 @@ const TEMPLATE = `
 </div>
 </div>
 `;
 `;
 
 
-class SnippetExecuteActions extends DisposableComponent {
+class ExecutableActions extends DisposableComponent {
   constructor(params) {
   constructor(params) {
     super();
     super();
     this.stopping = ko.observable(false);
     this.stopping = ko.observable(false);
@@ -96,7 +96,9 @@ class SnippetExecuteActions extends DisposableComponent {
   }
   }
 
 
   async execute() {
   async execute() {
-    this.beforeExecute();
+    if (this.beforeExecute) {
+      await this.beforeExecute();
+    }
     const executable = this.activeExecutable();
     const executable = this.activeExecutable();
     if (executable) {
     if (executable) {
       await executable.reset();
       await executable.reset();
@@ -112,4 +114,4 @@ class SnippetExecuteActions extends DisposableComponent {
   }
   }
 }
 }
 
 
-componentUtils.registerComponent(NAME, SnippetExecuteActions, TEMPLATE);
+componentUtils.registerComponent(NAME, ExecutableActions, TEMPLATE);

+ 0 - 2
desktop/core/src/desktop/js/apps/notebook2/components/ko.executableProgressBar.js

@@ -23,8 +23,6 @@ import componentUtils from 'ko/components/componentUtils';
 import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import { sleep } from 'utils/hueUtils';
 import { sleep } from 'utils/hueUtils';
-import { REDRAW_FIXED_HEADERS_EVENT } from 'apps/notebook2/events';
-import huePubSub from 'utils/huePubSub';
 
 
 export const NAME = 'executable-progress-bar';
 export const NAME = 'executable-progress-bar';
 
 

+ 8 - 3
desktop/core/src/desktop/js/apps/notebook2/components/spec/ko.snippetExecuteActions.spec.js → desktop/core/src/desktop/js/apps/notebook2/components/spec/ko.executableActions.spec.js

@@ -1,9 +1,10 @@
 import huePubSub from 'utils/huePubSub';
 import huePubSub from 'utils/huePubSub';
 import { koSetup } from 'spec/jasmineSetup';
 import { koSetup } from 'spec/jasmineSetup';
-import { NAME } from '../ko.snippetExecuteActions';
+import { NAME } from '../ko.executableActions';
 import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+import { sleep } from 'utils/hueUtils';
 
 
-describe('ko.snippetExecuteActions.js', () => {
+describe('ko.executableActions.js', () => {
   const setup = koSetup();
   const setup = koSetup();
 
 
   it('should render component', async () => {
   it('should render component', async () => {
@@ -11,6 +12,7 @@ describe('ko.snippetExecuteActions.js', () => {
       cancel: () => {},
       cancel: () => {},
       execute: () => {},
       execute: () => {},
       isReady: () => true,
       isReady: () => true,
+      reset: () => {},
       nextExecutable: {},
       nextExecutable: {},
       status: EXECUTION_STATUS.ready
       status: EXECUTION_STATUS.ready
     };
     };
@@ -30,10 +32,11 @@ describe('ko.snippetExecuteActions.js', () => {
       cancel: () => {
       cancel: () => {
         cancelCalled = true;
         cancelCalled = true;
       },
       },
-      execute: () => {
+      execute: async () => {
         executeCalled = true;
         executeCalled = true;
       },
       },
       isReady: () => true,
       isReady: () => true,
+      reset: () => {},
       nextExecutable: {},
       nextExecutable: {},
       status: EXECUTION_STATUS.ready
       status: EXECUTION_STATUS.ready
     };
     };
@@ -49,6 +52,8 @@ describe('ko.snippetExecuteActions.js', () => {
     expect(wrapper.querySelector('[data-test="stop"]')).toBeFalsy();
     expect(wrapper.querySelector('[data-test="stop"]')).toBeFalsy();
     wrapper.querySelector('[data-test="execute"]').click();
     wrapper.querySelector('[data-test="execute"]').click();
 
 
+    await sleep(0);
+
     expect(executeCalled).toBeTruthy();
     expect(executeCalled).toBeTruthy();
     mockExecutable.status = EXECUTION_STATUS.running;
     mockExecutable.status = EXECUTION_STATUS.running;
     huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
     huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);

+ 30 - 2
desktop/core/src/desktop/js/apps/notebook2/execution/executable.js

@@ -65,6 +65,7 @@ export default class Executable {
     this.executeStarted = 0;
     this.executeStarted = 0;
     this.executeEnded = 0;
     this.executeEnded = 0;
 
 
+    this.previousExecutable = undefined;
     this.nextExecutable = undefined;
     this.nextExecutable = undefined;
   }
   }
 
 
@@ -97,6 +98,24 @@ export default class Executable {
     );
     );
   }
   }
 
 
+  cancelBatchChain() {
+    if (this.previousExecutable) {
+      this.previousExecutable.nextExecutable = undefined;
+      this.previousExecutable.cancelBatchChain();
+      this.previousExecutable = undefined;
+    }
+
+    if (!this.isReady()) {
+      this.cancel();
+    }
+
+    if (this.nextExecutable) {
+      this.nextExecutable.previousExecutable = undefined;
+      this.nextExecutable.cancelBatchChain();
+      this.nextExecutable = undefined;
+    }
+  }
+
   async execute() {
   async execute() {
     if (!this.isReady()) {
     if (!this.isReady()) {
       return;
       return;
@@ -140,7 +159,7 @@ export default class Executable {
     statusCheckCount++;
     statusCheckCount++;
 
 
     this.cancellables.push(
     this.cancellables.push(
-      apiHelper.checkExecutionStatus({ executable: this }).done(queryStatus => {
+      apiHelper.checkExecutionStatus({ executable: this }).done(async queryStatus => {
         switch (this.status) {
         switch (this.status) {
           case EXECUTION_STATUS.success:
           case EXECUTION_STATUS.success:
             this.executeEnded = Date.now();
             this.executeEnded = Date.now();
@@ -155,6 +174,12 @@ export default class Executable {
               this.result = new ExecutionResult(this);
               this.result = new ExecutionResult(this);
               this.result.fetchRows();
               this.result.fetchRows();
             }
             }
+            if (this.nextExecutable) {
+              if (!this.nextExecutable.isReady()) {
+                await this.nextExecutable.reset();
+              }
+              this.nextExecutable.execute();
+            }
             break;
             break;
           case EXECUTION_STATUS.expired:
           case EXECUTION_STATUS.expired:
             this.executeEnded = Date.now();
             this.executeEnded = Date.now();
@@ -214,11 +239,14 @@ export default class Executable {
         await this.close();
         await this.close();
       } catch (err) {}
       } catch (err) {}
     }
     }
+    this.handle = {
+      statement_id: 0
+    };
     this.setProgress(0);
     this.setProgress(0);
+    this.setStatus(EXECUTION_STATUS.ready);
   }
   }
 
 
   async close() {
   async close() {
-    console.log('closing');
     while (this.cancellables.length) {
     while (this.cancellables.length) {
       const nextCancellable = this.cancellables.pop();
       const nextCancellable = this.cancellables.pop();
       try {
       try {

+ 62 - 35
desktop/core/src/desktop/js/apps/notebook2/execution/executor.js

@@ -39,50 +39,77 @@ class Executor {
     this.executables = [];
     this.executables = [];
   }
   }
 
 
-  update(statementDetails) {
+  getExecutables(statementDetails) {
+    const allExecutablesIndex = {};
+    this.executables.forEach(executable => {
+      allExecutablesIndex[executable.getKey()] = executable;
+    });
+
+    const selectedExecutables = [];
+    let activeDatabase = this.database();
+    let currentSelectedIndex = 0;
     const newExecutables = statementDetails.precedingStatements
     const newExecutables = statementDetails.precedingStatements
       .concat(statementDetails.activeStatement, statementDetails.followingStatements)
       .concat(statementDetails.activeStatement, statementDetails.followingStatements)
-      .map(
-        parsedStatement =>
-          new SqlExecutable({
-            parsedStatement: parsedStatement,
-            database: this.database(),
-            executor: this
-          })
-      );
+      .map(parsedStatement => {
+        if (/USE/i.test(parsedStatement.firstToken)) {
+          const dbMatch = parsedStatement.statement.match(/use\s+([^;]+)/i);
+          if (dbMatch) {
+            activeDatabase = dbMatch[1];
+          }
+        }
+        let executable = new SqlExecutable({
+          parsedStatement: parsedStatement,
+          database: activeDatabase,
+          executor: this
+        });
+        if (allExecutablesIndex[executable.getKey()]) {
+          executable = allExecutablesIndex[executable.getKey()];
+          delete allExecutablesIndex[executable.getKey()];
+        }
+        if (
+          currentSelectedIndex < statementDetails.selectedStatements.length &&
+          parsedStatement === statementDetails.selectedStatements[currentSelectedIndex]
+        ) {
+          selectedExecutables.push(executable);
+          currentSelectedIndex++;
+        }
+        return executable;
+      });
 
 
-    const existingExecutableIndex = {};
-    this.executables.forEach(executable => {
-      existingExecutableIndex[executable.getKey()] = executable;
-    });
+    const lostExecutables = Object.keys(allExecutablesIndex).map(key => allExecutablesIndex[key]);
+    return {
+      all: newExecutables,
+      lost: lostExecutables,
+      selected: selectedExecutables
+    };
+  }
 
 
-    let activeExecutable = new SqlExecutable({
-      parsedStatement: statementDetails.activeStatement,
-      database: this.database(),
-      executor: this
+  update(statementDetails, beforeExecute) {
+    const executables = this.getExecutables(statementDetails);
+
+    // Cancel any "lost" executables and any batch chain it's part of
+    executables.lost.forEach(lostExecutable => {
+      lostExecutable.cancelBatchChain();
     });
     });
 
 
-    if (existingExecutableIndex[activeExecutable.getKey()]) {
-      activeExecutable = existingExecutableIndex[activeExecutable.getKey()];
-    }
+    // Cancel any intersecting batch chains and create a new chain if just before execute
+    if (beforeExecute) {
+      executables.selected.forEach(executable => executable.cancelBatchChain());
 
 
-    // Refresh the executables list
-    this.executables = newExecutables.map(newExecutable => {
-      let actualExecutable = newExecutable;
-      const existingExecutable = existingExecutableIndex[newExecutable.getKey()];
-      if (existingExecutable) {
-        actualExecutable = existingExecutable;
-        delete existingExecutableIndex[newExecutable.getKey()];
-      }
-      return actualExecutable;
-    });
+      let previous = undefined;
+      executables.selected.forEach(executable => {
+        if (previous) {
+          executable.previousExecutable = previous;
+          previous.nextExecutable = executable;
+        }
+        previous = executable;
+      });
+    }
 
 
-    // Cancel lost executables
-    Object.keys(existingExecutableIndex).forEach(key => {
-      existingExecutableIndex[key].cancel();
-    });
+    // Update the executables list
+    this.executables = executables.all;
 
 
-    return activeExecutable;
+    return executables.selected[0];
   }
   }
 }
 }
 
 

+ 0 - 44
desktop/core/src/desktop/js/apps/notebook2/execution/sqlExecutable.js

@@ -16,7 +16,6 @@
 
 
 import apiHelper from 'api/apiHelper';
 import apiHelper from 'api/apiHelper';
 import Executable from 'apps/notebook2/execution/executable';
 import Executable from 'apps/notebook2/execution/executable';
-import sqlStatementsParser from 'parse/sqlStatementsParser';
 
 
 const BATCHABLE_STATEMENT_TYPES = /ALTER|CREATE|DELETE|DROP|GRANT|INSERT|INVALIDATE|LOAD|SET|TRUNCATE|UPDATE|UPSERT|USE/i;
 const BATCHABLE_STATEMENT_TYPES = /ALTER|CREATE|DELETE|DROP|GRANT|INSERT|INVALIDATE|LOAD|SET|TRUNCATE|UPDATE|UPSERT|USE/i;
 
 
@@ -49,49 +48,6 @@ export default class SqlExecutable extends Executable {
     return this.database + '_' + this.parsedStatement.statement;
     return this.database + '_' + this.parsedStatement.statement;
   }
   }
 
 
-  /**
-   *
-   * @param options
-   * @param options.database
-   * @param options.statement
-   * @param options.executor
-   * @param options.sourceType
-   * @param options.compute
-   * @param options.namespace
-   * @return {Array}
-   */
-  static fromStatement(options) {
-    const result = [];
-    let database = options.database;
-    sqlStatementsParser.parse(options.statement).forEach(parsedStatement => {
-      // If there's no first token it's a trailing comment
-      if (parsedStatement.firstToken) {
-        let skip = false;
-        // TODO: Do we want to send USE statements separately or do we want to send database as param instead?
-        if (/USE/i.test(parsedStatement.firstToken)) {
-          const dbMatch = parsedStatement.statement.match(/use\s+([^;]+)/i);
-          if (dbMatch) {
-            database = dbMatch[1];
-            skip = this.sourceType === 'impala' || this.sourceType === 'hive';
-          }
-        }
-        if (!skip) {
-          result.push(
-            new SqlExecutable({
-              executor: options.executor,
-              sourceType: options.sourceType,
-              compute: options.compute,
-              namespace: options.namespace,
-              database: database,
-              parsedStatement: parsedStatement
-            })
-          );
-        }
-      }
-    });
-    return result;
-  }
-
   canExecuteInBatch() {
   canExecuteInBatch() {
     return this.parsedStatement && BATCHABLE_STATEMENT_TYPES.test(this.parsedStatement.firstToken);
     return this.parsedStatement && BATCHABLE_STATEMENT_TYPES.test(this.parsedStatement.firstToken);
   }
   }

+ 10 - 9
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -19,10 +19,10 @@ import ko from 'knockout';
 import komapping from 'knockout.mapping';
 import komapping from 'knockout.mapping';
 import { markdown } from 'markdown';
 import { markdown } from 'markdown';
 
 
+import 'apps/notebook2/components/ko.executableActions';
 import 'apps/notebook2/components/ko.executableLogs';
 import 'apps/notebook2/components/ko.executableLogs';
 import 'apps/notebook2/components/ko.executableProgressBar';
 import 'apps/notebook2/components/ko.executableProgressBar';
 import 'apps/notebook2/components/ko.snippetEditorActions';
 import 'apps/notebook2/components/ko.snippetEditorActions';
-import 'apps/notebook2/components/ko.snippetExecuteActions';
 import 'apps/notebook2/components/ko.snippetResults';
 import 'apps/notebook2/components/ko.snippetResults';
 
 
 import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
 import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
@@ -40,7 +40,7 @@ import {
   ACTIVE_STATEMENT_CHANGED_EVENT,
   ACTIVE_STATEMENT_CHANGED_EVENT,
   REFRESH_STATEMENT_LOCATIONS_EVENT
   REFRESH_STATEMENT_LOCATIONS_EVENT
 } from 'sql/aceLocationHandler';
 } from 'sql/aceLocationHandler';
-import { EXECUTE_ACTIVE_EXECUTABLE_EVENT } from 'apps/notebook2/components/ko.snippetExecuteActions';
+import { EXECUTE_ACTIVE_EXECUTABLE_EVENT } from 'apps/notebook2/components/ko.executableActions';
 
 
 // TODO: Remove. Temporary here for debug
 // TODO: Remove. Temporary here for debug
 window.SqlExecutable = SqlExecutable;
 window.SqlExecutable = SqlExecutable;
@@ -369,6 +369,12 @@ export default class Snippet {
     this.lastExecutedStatement = ko.observable(null);
     this.lastExecutedStatement = ko.observable(null);
     this.statementsList = ko.observableArray();
     this.statementsList = ko.observableArray();
 
 
+    let beforeExecute = false;
+    this.beforeExecute = () => {
+      beforeExecute = true;
+      huePubSub.publish(REFRESH_STATEMENT_LOCATIONS_EVENT, this);
+    };
+
     huePubSub.subscribe(
     huePubSub.subscribe(
       ACTIVE_STATEMENT_CHANGED_EVENT,
       ACTIVE_STATEMENT_CHANGED_EVENT,
       statementDetails => {
       statementDetails => {
@@ -387,10 +393,9 @@ export default class Snippet {
               break;
               break;
             }
             }
           }
           }
-
           this.positionStatement(statementDetails.activeStatement);
           this.positionStatement(statementDetails.activeStatement);
-          this.activeExecutable(this.executor.update(statementDetails));
-
+          this.activeExecutable(this.executor.update(statementDetails, beforeExecute));
+          beforeExecute = false;
           if (statementDetails.activeStatement) {
           if (statementDetails.activeStatement) {
             const _statements = [];
             const _statements = [];
             statementDetails.precedingStatements.forEach(statement => {
             statementDetails.precedingStatements.forEach(statement => {
@@ -1003,10 +1008,6 @@ export default class Snippet {
       isSqlEngine: this.isSqlDialect
       isSqlEngine: this.isSqlDialect
     });
     });
 
 
-    this.beforeExecute = () => {
-      huePubSub.publish(REFRESH_STATEMENT_LOCATIONS_EVENT, this);
-    };
-
     huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
     huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
       if (this.activeExecutable() === executable) {
       if (this.activeExecutable() === executable) {
         this.status(executable.status);
         this.status(executable.status);

+ 57 - 14
desktop/core/src/desktop/js/sql/aceLocationHandler.js

@@ -498,17 +498,30 @@ class AceLocationHandler {
       );
       );
     };
     };
 
 
+    const getFirstPosition = (editorPositionOne, editorPositionTwo) => {
+      if (editorPositionOne.row === editorPositionTwo.row) {
+        return editorPositionOne.column <= editorPositionTwo.column
+          ? editorPositionOne
+          : editorPositionTwo;
+      }
+      return editorPositionOne.row < editorPositionTwo.row ? editorPositionOne : editorPositionTwo;
+    };
+
+    const equalPositions = (editorPositionOne, editorPositionTwo) => {
+      return (
+        editorPositionOne.row === editorPositionTwo.row &&
+        editorPositionOne.column === editorPositionTwo.column
+      );
+    };
+
     let lastExecutingStatement = null;
     let lastExecutingStatement = null;
     const updateActiveStatement = function(cursorChange) {
     const updateActiveStatement = function(cursorChange) {
       if (!self.snippet.isSqlDialect()) {
       if (!self.snippet.isSqlDialect()) {
         return;
         return;
       }
       }
       const selectionRange = self.editor.getSelectionRange();
       const selectionRange = self.editor.getSelectionRange();
-      const editorLocation = selectionRange.start;
-      if (
-        selectionRange.start.row !== selectionRange.end.row ||
-        selectionRange.start.column !== selectionRange.end.column
-      ) {
+      const cursorLocation = selectionRange.start;
+      if (!equalPositions(selectionRange.start, selectionRange.end)) {
         if (!cursorChange && self.snippet.result && self.snippet.result.statement_range()) {
         if (!cursorChange && self.snippet.result && self.snippet.result.statement_range()) {
           let executingStatement = self.snippet.result.statement_range();
           let executingStatement = self.snippet.result.statement_range();
           // Row and col are 0 for both start and end on execute, so if the selection hasn't changed we'll use last known executed statement
           // Row and col are 0 for both start and end on execute, so if the selection hasn't changed we'll use last known executed statement
@@ -522,10 +535,10 @@ class AceLocationHandler {
             executingStatement = lastExecutingStatement;
             executingStatement = lastExecutingStatement;
           }
           }
           if (executingStatement.start.row === 0) {
           if (executingStatement.start.row === 0) {
-            editorLocation.column += executingStatement.start.column;
+            cursorLocation.column += executingStatement.start.column;
           } else if (executingStatement.start.row !== 0 || executingStatement.start.column !== 0) {
           } else if (executingStatement.start.row !== 0 || executingStatement.start.column !== 0) {
-            editorLocation.row += executingStatement.start.row;
-            editorLocation.column = executingStatement.start.column;
+            cursorLocation.row += executingStatement.start.row;
+            cursorLocation.column = executingStatement.start.column;
           }
           }
           lastExecutingStatement = executingStatement;
           lastExecutingStatement = executingStatement;
         } else {
         } else {
@@ -533,23 +546,45 @@ class AceLocationHandler {
         }
         }
       }
       }
 
 
-      if (cursorChange && activeStatement) {
+      if (cursorChange && activeStatement && !window.ENABLE_NOTEBOOK_2) {
         // Don't update when cursor stays in the same statement
         // Don't update when cursor stays in the same statement
-        if (isPointInside(activeStatement.location, editorLocation)) {
+        if (isPointInside(activeStatement.location, cursorLocation)) {
           return;
           return;
         }
         }
       }
       }
+      const selectedStatements = [];
       const precedingStatements = [];
       const precedingStatements = [];
       const followingStatements = [];
       const followingStatements = [];
       activeStatement = null;
       activeStatement = null;
 
 
+      const firstSelectionPoint = getFirstPosition(selectionRange.start, selectionRange.end);
+      const lastSelectionPoint =
+        selectionRange.start === firstSelectionPoint ? selectionRange.end : selectionRange.start;
+
       let found = false;
       let found = false;
       let statementIndex = 0;
       let statementIndex = 0;
+      let insideSelection = false;
       if (lastKnownStatements.length === 1) {
       if (lastKnownStatements.length === 1) {
         activeStatement = lastKnownStatements[0];
         activeStatement = lastKnownStatements[0];
       } else {
       } else {
         lastKnownStatements.forEach(statement => {
         lastKnownStatements.forEach(statement => {
-          if (isPointInside(statement.location, editorLocation)) {
+          if (!equalPositions(firstSelectionPoint, lastSelectionPoint)) {
+            if (!insideSelection && isPointInside(statement.location, firstSelectionPoint)) {
+              insideSelection = true;
+            }
+            if (insideSelection) {
+              selectedStatements.push(statement);
+
+              if (
+                isPointInside(statement.location, lastSelectionPoint) ||
+                (statement.location.last_line === lastSelectionPoint.row + 1 &&
+                  statement.location.last_column === lastSelectionPoint.column)
+              ) {
+                insideSelection = false;
+              }
+            }
+          }
+          if (isPointInside(statement.location, cursorLocation)) {
             statementIndex++;
             statementIndex++;
             found = true;
             found = true;
             activeStatement = statement;
             activeStatement = statement;
@@ -571,6 +606,10 @@ class AceLocationHandler {
         }
         }
       }
       }
 
 
+      if (!selectedStatements.length) {
+        selectedStatements.push(activeStatement);
+      }
+
       huePubSub.publish(ACTIVE_STATEMENT_CHANGED_EVENT, {
       huePubSub.publish(ACTIVE_STATEMENT_CHANGED_EVENT, {
         id: self.editorId,
         id: self.editorId,
         editorChangeTime: lastKnownStatements.editorChangeTime,
         editorChangeTime: lastKnownStatements.editorChangeTime,
@@ -578,11 +617,12 @@ class AceLocationHandler {
         totalStatementCount: lastKnownStatements.length,
         totalStatementCount: lastKnownStatements.length,
         precedingStatements: precedingStatements,
         precedingStatements: precedingStatements,
         activeStatement: activeStatement,
         activeStatement: activeStatement,
+        selectedStatements: selectedStatements,
         followingStatements: followingStatements
         followingStatements: followingStatements
       });
       });
 
 
       if (activeStatement) {
       if (activeStatement) {
-        self.checkForSyntaxErrors(activeStatement.location, editorLocation);
+        self.checkForSyntaxErrors(activeStatement.location, firstSelectionPoint);
       }
       }
     };
     };
 
 
@@ -609,6 +649,7 @@ class AceLocationHandler {
     let cursorChangePaused = false; // On change the cursor is also moved, this limits the calls while typing
     let cursorChangePaused = false; // On change the cursor is also moved, this limits the calls while typing
 
 
     let lastStart;
     let lastStart;
+    let lastEnd;
     let lastCursorPosition;
     let lastCursorPosition;
     const changeSelectionListener = self.editor.on('changeSelection', () => {
     const changeSelectionListener = self.editor.on('changeSelection', () => {
       if (cursorChangePaused) {
       if (cursorChangePaused) {
@@ -628,13 +669,15 @@ class AceLocationHandler {
 
 
         // The active statement is initially the top one in the selection, batch execution updates this.
         // The active statement is initially the top one in the selection, batch execution updates this.
         const newStart = self.editor.getSelectionRange().start;
         const newStart = self.editor.getSelectionRange().start;
+        const newEnd = self.editor.getSelectionRange().end;
         if (
         if (
-          self.snippet.isSqlDialect() &&
-          (!lastStart || lastStart.row !== newStart.row || lastStart.column !== newStart.column)
+          (self.snippet.isSqlDialect() && (!lastStart || !equalPositions(lastStart, newStart))) ||
+          (window.ENABLE_NOTEBOOK_2 && (!lastEnd || !equalPositions(lastEnd, newEnd)))
         ) {
         ) {
           window.clearTimeout(updateThrottle);
           window.clearTimeout(updateThrottle);
           updateActiveStatement(true);
           updateActiveStatement(true);
           lastStart = newStart;
           lastStart = newStart;
+          lastEnd = newEnd;
         }
         }
       }, 100);
       }, 100);
     });
     });

+ 1 - 1
desktop/libs/notebook/src/notebook/templates/editor_components2.mako

@@ -1335,7 +1335,7 @@
 
 
   <script type ="text/html" id="snippet-execution-controls${ suffix }">
   <script type ="text/html" id="snippet-execution-controls${ suffix }">
     <div class="snippet-actions clearfix">
     <div class="snippet-actions clearfix">
-      <div class="pull-left" data-bind="component: { name: 'snippet-execute-actions', params: { activeExecutable: activeExecutable, beforeExecute: beforeExecute } }" />
+      <div class="pull-left" data-bind="component: { name: 'executable-actions', params: { activeExecutable: activeExecutable, beforeExecute: beforeExecute } }" />
       <!-- ko if: isSqlDialect() && !$root.isPresentationMode() -->
       <!-- ko if: isSqlDialect() && !$root.isPresentationMode() -->
       <div class="pull-right" data-bind="component: { name: 'snippet-editor-actions', params: { snippet: $data } }" />
       <div class="pull-right" data-bind="component: { name: 'snippet-editor-actions', params: { snippet: $data } }" />
       <!-- /ko -->
       <!-- /ko -->