Эх сурвалжийг харах

[editor] Add support for ctrl-enter execution in the AceEditor component

Johan Ahlen 5 жил өмнө
parent
commit
ea754975d5

+ 25 - 7
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/AceEditor.vue

@@ -60,6 +60,7 @@
 
     subTracker = new SubscriptionTracker();
     editor: Ace.Editor | null = null;
+    aceLocationHandler: AceLocationHandler | null = null;
 
     private isSqlDialect(): boolean {
       return (<EditorInterpreter>this.executor.connector()).is_sql;
@@ -86,12 +87,12 @@
       this.subTracker.subscribe('assist.set.manual.visibility', resizeAce);
       this.subTracker.subscribe('split.panel.resized', resizeAce);
 
-      const aceLocationHandler = new AceLocationHandler({
+      this.aceLocationHandler = new AceLocationHandler({
         editor: editor,
         editorId: this.id,
         executor: this.executor
       });
-      this.subTracker.addDisposable(aceLocationHandler);
+      this.subTracker.addDisposable(this.aceLocationHandler);
 
       const aceGutterHandler = new AceGutterHandler({
         editor: editor,
@@ -173,16 +174,18 @@
         );
 
         if (errorHighlightingEnabled) {
-          aceLocationHandler.attachSqlSyntaxWorker();
+          this.aceLocationHandler.attachSqlSyntaxWorker();
         }
 
         editor.customMenuOptions.setErrorHighlighting = (enabled: boolean) => {
           errorHighlightingEnabled = enabled;
           apiHelper.setInTotalStorage('hue.ace', 'errorHighlightingEnabled', enabled);
-          if (enabled) {
-            aceLocationHandler.attachSqlSyntaxWorker();
-          } else {
-            aceLocationHandler.detachSqlSyntaxWorker();
+          if (this.aceLocationHandler) {
+            if (enabled) {
+              this.aceLocationHandler.attachSqlSyntaxWorker();
+            } else {
+              this.aceLocationHandler.detachSqlSyntaxWorker();
+            }
           }
         };
         editor.customMenuOptions.getErrorHighlighting = () => errorHighlightingEnabled;
@@ -294,6 +297,21 @@
       // const aceErrorsSub = snippet.aceErrors.subscribe(newErrors => {
       //   processErrorsAndWarnings('error', newErrors);
       // });
+
+      editor.commands.addCommand({
+        name: 'execute',
+        bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter|Ctrl-Enter' },
+        exec: async () => {
+          if (this.aceLocationHandler) {
+            this.aceLocationHandler.refreshStatementLocations();
+          }
+          if (this.editor && this.executor.activeExecutable) {
+            await this.executor.activeExecutable.reset();
+            await this.executor.activeExecutable.execute();
+          }
+        }
+      });
+
       window.setTimeout(() => {
         this.$emit('ace-created', editor);
       }, 3000);

+ 159 - 153
desktop/core/src/desktop/js/apps/notebook2/components/aceEditor/AceLocationHandler.ts

@@ -110,10 +110,18 @@ export default class AceLocationHandler implements Disposable {
 
   subTracker: SubscriptionTracker = new SubscriptionTracker();
   availableDatabases = new Set<string>();
+  changeThrottle = -1;
   verifyThrottle = -1;
   updateTimeout = -1;
+  cursorChangePaused = false;
   sqlSyntaxWorkerSub?: HueSubscription;
 
+  activeStatement: ParsedSqlStatement | undefined;
+  lastKnownStatements = {
+    editorChangeTime: 0,
+    statements: <ParsedSqlStatement[]>[]
+  };
+
   constructor(options: {
     editor: Ace.Editor;
     editorId: string;
@@ -130,6 +138,7 @@ export default class AceLocationHandler implements Disposable {
     this.attachMouseListeners();
 
     this.subTracker.subscribe(this.executor.connector, this.updateAvailableDatabases.bind(this));
+    this.subTracker.trackTimeout(this.changeThrottle);
     this.subTracker.trackTimeout(this.verifyThrottle);
     this.subTracker.trackTimeout(this.updateTimeout);
     this.updateAvailableDatabases();
@@ -525,156 +534,21 @@ export default class AceLocationHandler implements Disposable {
   }
 
   attachStatementLocator(): void {
-    const lastKnownStatements = {
-      editorChangeTime: 0,
-      statements: <ParsedSqlStatement[]>[]
-    };
-    let activeStatement: ParsedSqlStatement | undefined;
-    //let lastExecutingStatement = null;
-
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    const updateActiveStatement = (cursorChange?: boolean) => {
-      if (!this.isSqlDialect()) {
-        return;
-      }
-      const selectionRange = this.editor.getSelectionRange();
-      const cursorLocation = selectionRange.start;
-      if (!equalPositions(selectionRange.start, selectionRange.end)) {
-        // TODO: Figure out what this does and why it needs the result.statement_range
-        // if (!cursorChange && this.snippet.result && this.snippet.result.statement_range()) {
-        //   let executingStatement = this.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
-        //   if (
-        //     executingStatement.start.row === 0 &&
-        //     executingStatement.start.column === 0 &&
-        //     executingStatement.end.row === 0 &&
-        //     executingStatement.end.column === 0 &&
-        //     lastExecutingStatement
-        //   ) {
-        //     executingStatement = lastExecutingStatement;
-        //   }
-        //   if (executingStatement.start.row === 0) {
-        //     cursorLocation.column += executingStatement.start.column;
-        //   } else if (executingStatement.start.row !== 0 || executingStatement.start.column !== 0) {
-        //     cursorLocation.row += executingStatement.start.row;
-        //     cursorLocation.column = executingStatement.start.column;
-        //   }
-        //   lastExecutingStatement = executingStatement;
-        // } else {
-        //   lastExecutingStatement = null;
-        // }
-      }
-
-      const selectedStatements: ParsedSqlStatement[] = [];
-      const precedingStatements: ParsedSqlStatement[] = [];
-      const followingStatements: ParsedSqlStatement[] = [];
-      activeStatement = undefined;
-
-      const firstSelectionPoint = getFirstPosition(selectionRange.start, selectionRange.end);
-      const lastSelectionPoint =
-        selectionRange.start === firstSelectionPoint ? selectionRange.end : selectionRange.start;
-
-      let found = false;
-      let statementIndex = 0;
-      let insideSelection = false;
-      if (lastKnownStatements.statements.length === 1) {
-        activeStatement = lastKnownStatements.statements[0];
-      } else {
-        lastKnownStatements.statements.forEach(statement => {
-          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++;
-            found = true;
-            activeStatement = statement;
-          } else if (!found) {
-            statementIndex++;
-            if (precedingStatements.length === STATEMENT_COUNT_AROUND_ACTIVE) {
-              precedingStatements.shift();
-            }
-            precedingStatements.push(statement);
-          } else if (found && followingStatements.length < STATEMENT_COUNT_AROUND_ACTIVE) {
-            followingStatements.push(statement);
-          }
-        });
-
-        // Can happen if multiple statements and the cursor is after the last one
-        if (!found) {
-          precedingStatements.pop();
-          activeStatement =
-            lastKnownStatements.statements[lastKnownStatements.statements.length - 1];
-        }
-      }
-
-      if (!selectedStatements.length && activeStatement) {
-        selectedStatements.push(activeStatement);
-      }
-
-      huePubSub.publish(ACTIVE_STATEMENT_CHANGED_EVENT, <ActiveStatementChangedEvent>{
-        id: this.editorId,
-        editorChangeTime: lastKnownStatements.editorChangeTime,
-        activeStatementIndex: statementIndex,
-        totalStatementCount: lastKnownStatements.statements.length,
-        precedingStatements: precedingStatements,
-        activeStatement: activeStatement,
-        selectedStatements: selectedStatements,
-        followingStatements: followingStatements
-      });
-
-      if (activeStatement) {
-        this.checkForSyntaxErrors(activeStatement.location, firstSelectionPoint);
-      }
-    };
-
-    const parseForStatements = () => {
-      if (this.isSqlDialect()) {
-        try {
-          const lastChangeTime = this.editor.lastChangeTime;
-          lastKnownStatements.statements = sqlStatementsParser.parse(this.editor.getValue());
-          lastKnownStatements.editorChangeTime = lastChangeTime;
-
-          const hueDebug = (<hueWindow>window).hueDebug;
-          if (hueDebug && hueDebug.logStatementLocations) {
-            // eslint-disable-next-line no-restricted-syntax
-            console.log(lastKnownStatements);
-          }
-        } catch (error) {
-          console.warn('Could not parse statements!');
-          console.warn(error);
-        }
-      }
-    };
-
-    let changeThrottle = window.setTimeout(parseForStatements, 0);
-    this.subTracker.trackTimeout(changeThrottle);
+    this.changeThrottle = window.setTimeout(this.parseForStatements.bind(this), 0);
 
-    window.setTimeout(updateActiveStatement, 0);
+    window.setTimeout(this.updateActiveStatement.bind(this), 0);
 
-    let cursorChangePaused = false; // On change the cursor is also moved, this limits the calls while typing
+    this.cursorChangePaused = false; // On change the cursor is also moved, this limits the calls while typing
 
     let lastStart: Ace.Position;
     let lastEnd: Ace.Position;
     let lastCursorPosition: Ace.Position;
     const changeSelectionListener = this.editor.on('changeSelection', () => {
-      if (cursorChangePaused) {
+      if (this.cursorChangePaused) {
         return;
       }
-      window.clearTimeout(changeThrottle);
-      changeThrottle = window.setTimeout(() => {
+      window.clearTimeout(this.changeThrottle);
+      this.changeThrottle = window.setTimeout(() => {
         const newCursorPosition = this.editor.getCursorPosition();
         if (
           !lastCursorPosition ||
@@ -698,7 +572,7 @@ export default class AceLocationHandler implements Disposable {
             !lastEnd ||
             !equalPositions(lastEnd, newEnd))
         ) {
-          updateActiveStatement(true);
+          this.updateActiveStatement(true);
           lastStart = newStart;
           lastEnd = newEnd;
         }
@@ -711,12 +585,12 @@ export default class AceLocationHandler implements Disposable {
 
     const changeListener = this.editor.on('change', () => {
       if (this.isSqlDialect()) {
-        window.clearTimeout(changeThrottle);
-        cursorChangePaused = true;
-        changeThrottle = window.setTimeout(() => {
-          parseForStatements();
-          updateActiveStatement();
-          cursorChangePaused = false;
+        window.clearTimeout(this.changeThrottle);
+        this.cursorChangePaused = true;
+        this.changeThrottle = window.setTimeout(() => {
+          this.parseForStatements();
+          this.updateActiveStatement();
+          this.cursorChangePaused = false;
         }, 500);
         this.editor.lastChangeTime = Date.now();
       }
@@ -728,15 +602,147 @@ export default class AceLocationHandler implements Disposable {
 
     this.subTracker.subscribe(REFRESH_STATEMENT_LOCATIONS_EVENT, editorId => {
       if (editorId === this.editorId) {
-        cursorChangePaused = true;
-        window.clearTimeout(changeThrottle);
-        parseForStatements();
-        updateActiveStatement();
-        cursorChangePaused = false;
+        this.refreshStatementLocations();
       }
     });
   }
 
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  updateActiveStatement(cursorChange?: boolean): void {
+    if (!this.isSqlDialect()) {
+      return;
+    }
+    const selectionRange = this.editor.getSelectionRange();
+    const cursorLocation = selectionRange.start;
+    if (!equalPositions(selectionRange.start, selectionRange.end)) {
+      // TODO: Figure out what this does and why it needs the result.statement_range
+      // if (!cursorChange && this.snippet.result && this.snippet.result.statement_range()) {
+      //   let executingStatement = this.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
+      //   if (
+      //     executingStatement.start.row === 0 &&
+      //     executingStatement.start.column === 0 &&
+      //     executingStatement.end.row === 0 &&
+      //     executingStatement.end.column === 0 &&
+      //     lastExecutingStatement
+      //   ) {
+      //     executingStatement = lastExecutingStatement;
+      //   }
+      //   if (executingStatement.start.row === 0) {
+      //     cursorLocation.column += executingStatement.start.column;
+      //   } else if (executingStatement.start.row !== 0 || executingStatement.start.column !== 0) {
+      //     cursorLocation.row += executingStatement.start.row;
+      //     cursorLocation.column = executingStatement.start.column;
+      //   }
+      //   lastExecutingStatement = executingStatement;
+      // } else {
+      //   lastExecutingStatement = null;
+      // }
+    }
+
+    const selectedStatements: ParsedSqlStatement[] = [];
+    const precedingStatements: ParsedSqlStatement[] = [];
+    const followingStatements: ParsedSqlStatement[] = [];
+    this.activeStatement = undefined;
+
+    const firstSelectionPoint = getFirstPosition(selectionRange.start, selectionRange.end);
+    const lastSelectionPoint =
+      selectionRange.start === firstSelectionPoint ? selectionRange.end : selectionRange.start;
+
+    let found = false;
+    let statementIndex = 0;
+    let insideSelection = false;
+    if (this.lastKnownStatements.statements.length === 1) {
+      this.activeStatement = this.lastKnownStatements.statements[0];
+    } else {
+      this.lastKnownStatements.statements.forEach(statement => {
+        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++;
+          found = true;
+          this.activeStatement = statement;
+        } else if (!found) {
+          statementIndex++;
+          if (precedingStatements.length === STATEMENT_COUNT_AROUND_ACTIVE) {
+            precedingStatements.shift();
+          }
+          precedingStatements.push(statement);
+        } else if (found && followingStatements.length < STATEMENT_COUNT_AROUND_ACTIVE) {
+          followingStatements.push(statement);
+        }
+      });
+
+      // Can happen if multiple statements and the cursor is after the last one
+      if (!found) {
+        precedingStatements.pop();
+        this.activeStatement = this.lastKnownStatements.statements[
+          this.lastKnownStatements.statements.length - 1
+        ];
+      }
+    }
+
+    if (!selectedStatements.length && this.activeStatement) {
+      selectedStatements.push(this.activeStatement);
+    }
+
+    huePubSub.publish(ACTIVE_STATEMENT_CHANGED_EVENT, <ActiveStatementChangedEvent>{
+      id: this.editorId,
+      editorChangeTime: this.lastKnownStatements.editorChangeTime,
+      activeStatementIndex: statementIndex,
+      totalStatementCount: this.lastKnownStatements.statements.length,
+      precedingStatements: precedingStatements,
+      activeStatement: this.activeStatement,
+      selectedStatements: selectedStatements,
+      followingStatements: followingStatements
+    });
+
+    if (this.activeStatement) {
+      this.checkForSyntaxErrors(this.activeStatement.location, firstSelectionPoint);
+    }
+  }
+
+  parseForStatements(): void {
+    if (this.isSqlDialect()) {
+      try {
+        const lastChangeTime = this.editor.lastChangeTime;
+        this.lastKnownStatements.statements = sqlStatementsParser.parse(this.editor.getValue());
+        this.lastKnownStatements.editorChangeTime = lastChangeTime;
+
+        const hueDebug = (<hueWindow>window).hueDebug;
+        if (hueDebug && hueDebug.logStatementLocations) {
+          // eslint-disable-next-line no-restricted-syntax
+          console.log(this.lastKnownStatements);
+        }
+      } catch (error) {
+        console.warn('Could not parse statements!');
+        console.warn(error);
+      }
+    }
+  }
+
+  refreshStatementLocations(): void {
+    this.cursorChangePaused = true;
+    window.clearTimeout(this.changeThrottle);
+    this.parseForStatements();
+    this.updateActiveStatement();
+    this.cursorChangePaused = false;
+  }
+
   clearMarkedErrors(type?: string): void {
     const markers = this.editor.getSession().$backMarkers;
     for (const markerId in markers) {

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

@@ -36,6 +36,7 @@ export default class Executor {
   executables: Executable[] = [];
   variableSubstitionHandler?: VariableSubstitutionHandler;
   snippet?: Snippet;
+  activeExecutable?: Executable;
 
   constructor(options: {
     connector: KnockoutObservable<Connector>;
@@ -101,6 +102,7 @@ export default class Executor {
     }
 
     // Update the executables list
+    this.activeExecutable = executables.active;
     this.executables = executables.all;
 
     this.executables.forEach(executable => executable.notify());

+ 11 - 6
desktop/core/src/desktop/js/apps/notebook2/execution/utils.ts

@@ -24,6 +24,7 @@ export interface SyncSqlExecutablesResult {
   edited: SqlExecutable[];
   lost: SqlExecutable[];
   selected: SqlExecutable[];
+  active: SqlExecutable;
 }
 
 export const syncSqlExecutables = (
@@ -37,15 +38,16 @@ export const syncSqlExecutables = (
 
   const existingExecutables: (Executable | undefined)[] = executor.executables.concat();
 
-  const result: SyncSqlExecutablesResult = {
-    all: [],
-    edited: [],
-    lost: [],
-    selected: []
+  const result = {
+    all: <SqlExecutable[]>[],
+    edited: <SqlExecutable[]>[],
+    lost: <SqlExecutable[]>[],
+    selected: <SqlExecutable[]>[]
   };
 
   let activeDatabase = executor.database();
   let currentSelectedIndex = 0;
+  let activeStatementIndex = 0;
   allNewStatements.forEach((parsedStatement, index) => {
     if (/USE/i.test(parsedStatement.firstToken)) {
       const dbMatch = parsedStatement.statement.match(/use\s+([^;]+)/i);
@@ -72,6 +74,9 @@ export const syncSqlExecutables = (
         executor: executor
       });
     }
+    if (parsedStatement === statementDetails.activeStatement) {
+      activeStatementIndex = index;
+    }
     result.all.push(executable);
 
     if (
@@ -87,5 +92,5 @@ export const syncSqlExecutables = (
     executable => typeof executable !== 'undefined'
   ) as SqlExecutable[];
 
-  return result;
+  return { ...result, active: result.all[activeStatementIndex] };
 };

+ 7 - 0
desktop/core/src/desktop/js/ext/ace.d.ts

@@ -22,6 +22,13 @@ declare namespace Ace {
     addError(message:string, line:number): void;
     addWarning(message:string, line:number): void;
     container: HTMLElement;
+    commands: {
+      addCommand(command: {
+        name: string,
+        bindKey: { win: string; mac: string },
+        exec(): void
+      }): void;
+    }
     completer: any;
     completers: any[];
     clearErrorsAndWarnings(type: string): void;