blob: 69ee634af0c25a46f7be34bdcea8a2c3c669d755 [file] [log] [blame]
/*
* Licensed 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.
*/
import {SpellResult} from '../../spell';
import {isParagraphRunning, ParagraphStatus} from './paragraph.status';
import moment from 'moment';
import DiffMatchPatch from 'diff-match-patch';
require('moment-duration-format');
const ParagraphExecutor = {
SPELL: 'SPELL',
INTERPRETER: 'INTERPRETER',
NONE: '', /** meaning `DONE` */
};
angular.module('zeppelinWebApp').controller('ParagraphCtrl', ParagraphCtrl);
function ParagraphCtrl($scope, $rootScope, $route, $window, $routeParams, $location,
$timeout, $compile, $http, $q, websocketMsgSrv,
baseUrlSrv, ngToast, noteVarShareService,
heliumService) {
'ngInject';
let ANGULAR_FUNCTION_OBJECT_NAME_PREFIX = '_Z_ANGULAR_FUNC_';
let completionListLength = undefined;
$rootScope.keys = Object.keys;
$scope.parentNote = null;
$scope.paragraph = {};
$scope.paragraph.results = {};
$scope.paragraph.results.msg = [];
$scope.originalText = '';
$scope.editor = null;
$scope.cursorPosition = null;
$scope.diffMatchPatch = new DiffMatchPatch();
$scope.isNoteRunning = false;
// transactional info for spell execution
$scope.spellTransaction = {
totalResultCount: 0,
renderedResultCount: 0,
propagated: false,
resultsMsg: [],
paragraphText: '',
};
let searchRanges = [];
const getCurrentRangeDefault = function() {
return {id: -1, markerId: -1};
};
let currentRange = getCurrentRangeDefault();
let editorSetting = {};
// flag that is used to set editor setting on paste percent sign
let pastePercentSign = false;
// flag that is used to set editor setting on save interpreter bindings
let setInterpreterBindings = false;
let paragraphScope = $rootScope.$new(true, $rootScope);
// to keep backward compatibility
$scope.compiledScope = paragraphScope;
paragraphScope.z = {
// z.runParagraph('20150213-231621_168813393')
runParagraph: function(paragraphId) {
if (paragraphId) {
let filtered = $scope.parentNote.paragraphs.filter(function(x) {
return x.id === paragraphId;
});
if (filtered.length === 1) {
let paragraph = filtered[0];
websocketMsgSrv.runParagraph(paragraph.id, paragraph.title, paragraph.text,
paragraph.config, paragraph.settings.params);
} else {
ngToast.danger({
content: 'Cannot find a paragraph with id \'' + paragraphId + '\'',
verticalPosition: 'top',
dismissOnTimeout: false,
});
}
} else {
ngToast.danger({
content: 'Please provide a \'paragraphId\' when calling z.runParagraph(paragraphId)',
verticalPosition: 'top',
dismissOnTimeout: false,
});
}
},
// Example: z.angularBind('my_var', 'Test Value', '20150213-231621_168813393')
angularBind: function(varName, value, paragraphId) {
// Only push to server if there paragraphId is defined
if (paragraphId) {
websocketMsgSrv.clientBindAngularObject($routeParams.noteId, varName, value, paragraphId);
} else {
ngToast.danger({
content: 'Please provide a \'paragraphId\' when calling ' +
'z.angularBind(varName, value, \'PUT_HERE_PARAGRAPH_ID\')',
verticalPosition: 'top',
dismissOnTimeout: false,
});
}
},
// Example: z.angularUnBind('my_var', '20150213-231621_168813393')
angularUnbind: function(varName, paragraphId) {
// Only push to server if paragraphId is defined
if (paragraphId) {
websocketMsgSrv.clientUnbindAngularObject($routeParams.noteId, varName, paragraphId);
} else {
ngToast.danger({
content: 'Please provide a \'paragraphId\' when calling ' +
'z.angularUnbind(varName, \'PUT_HERE_PARAGRAPH_ID\')',
verticalPosition: 'top',
dismissOnTimeout: false});
}
},
};
let angularObjectRegistry = {};
// Controller init
$scope.init = function(newParagraph, note) {
$scope.paragraph = newParagraph;
$scope.parentNote = note;
$scope.originalText = angular.copy(newParagraph.text);
$scope.chart = {};
$scope.baseMapOption = ['Streets', 'Satellite', 'Hybrid', 'Topo', 'Gray', 'Oceans', 'Terrain'];
$scope.colWidthOption = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
$scope.fontSizeOption = [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
$scope.paragraphFocused = false;
if (newParagraph.focus) {
$scope.paragraphFocused = true;
}
if (!$scope.paragraph.config) {
$scope.paragraph.config = {};
}
$scope.isNoteRunning = !!(note && note.hasOwnProperty('info') &&
note.info.hasOwnProperty('isRunning')
&& note.info.isRunning === true);
noteVarShareService.put($scope.paragraph.id + '_paragraphScope', paragraphScope);
initializeDefault($scope.paragraph.config);
if ($scope.updateParagraphNumbering) {
$scope.updateParagraphNumbering();
}
};
$scope.$on('noteRunningStatus', function(event, status) {
$scope.isNoteRunning = status;
$scope.editor.setReadOnly(status);
});
const initializeDefault = function(config) {
let forms = $scope.paragraph.settings.forms;
if (!config.colWidth) {
config.colWidth = 12;
}
if (!config.fontSize) {
config.fontSize = 9;
}
if (config.enabled === undefined) {
config.enabled = true;
}
if (config.numbering === undefined) {
if ($scope.note) {
config.numbering = $scope.note.config.numberingToggled;
} else {
config.numbering = false;
}
}
for (let idx in forms) {
if (forms[idx]) {
if (forms[idx].options) {
if (config.runOnSelectionChange === undefined) {
config.runOnSelectionChange = true;
}
}
}
}
if (!config.results) {
config.results = {};
}
if (!config.editorSetting) {
config.editorSetting = {};
} else if (config.editorSetting.editOnDblClick) {
editorSetting.isOutputHidden = config.editorSetting.editOnDblClick;
}
};
const isTabCompletion = function() {
const completionKey = $scope.paragraph.config.editorSetting.completionKey;
return completionKey === 'TAB';
};
$scope.$on('updateParagraphOutput', function(event, data) {
if ($scope.paragraph.id === data.paragraphId) {
if (!$scope.paragraph.results) {
$scope.paragraph.results = {};
}
if (!$scope.paragraph.results.msg) {
$scope.paragraph.results.msg = [];
}
let update = typeof $scope.paragraph.results.msg[data.index] !== 'undefined';
$scope.paragraph.results.msg[data.index] = {
data: data.data,
type: data.type,
};
if (update) {
$rootScope.$broadcast(
'updateResult',
$scope.paragraph.results.msg[data.index],
$scope.paragraph.config.results[data.index],
$scope.paragraph,
data.index);
}
}
});
$scope.getIframeDimensions = function() {
if ($scope.asIframe) {
let paragraphid = '#' + $routeParams.paragraphId + '_container';
let height = angular.element(paragraphid).height();
return height;
}
return 0;
};
$scope.$watch($scope.getIframeDimensions, function(newValue, oldValue) {
if ($scope.asIframe && newValue) {
let message = {};
message.height = newValue;
message.url = $location.$$absUrl;
$window.parent.postMessage(angular.toJson(message), '*');
}
});
$scope.getEditor = function() {
return $scope.editor;
};
$scope.$watch($scope.getEditor, function(newValue, oldValue) {
if (!$scope.editor) {
return;
}
if (newValue === null || newValue === undefined) {
console.log('editor isnt loaded yet, returning');
return;
}
if ($scope.revisionView === true || $scope.isNoteRunning === true) {
$scope.editor.setReadOnly(true);
} else {
$scope.editor.setReadOnly(false);
}
});
let isEmpty = function(object) {
return !object;
};
$scope.isRunning = function(paragraph) {
return isParagraphRunning(paragraph);
};
$scope.cancelParagraph = function(paragraph) {
console.log('Cancel %o', paragraph.id);
websocketMsgSrv.cancelParagraphRun(paragraph.id);
};
$scope.propagateSpellResult = function(paragraphId, paragraphTitle,
paragraphText, paragraphResults,
paragraphStatus, paragraphErrorMessage,
paragraphConfig, paragraphSettingsParam,
paragraphDateStarted, paragraphDateFinished) {
websocketMsgSrv.paragraphExecutedBySpell(
paragraphId, paragraphTitle,
paragraphText, paragraphResults,
paragraphStatus, paragraphErrorMessage,
paragraphConfig, paragraphSettingsParam,
paragraphDateStarted, paragraphDateFinished
);
};
$scope.handleSpellError = function(paragraphText, error,
digestRequired, propagated) {
const errorMessage = error.stack;
$scope.paragraph.status = ParagraphStatus.ERROR;
$scope.paragraph.errorMessage = errorMessage;
console.error('Failed to execute interpret() in spell\n', error);
if (!propagated) {
$scope.paragraph.dateFinished = $scope.getFormattedParagraphTime();
}
if (!propagated) {
$scope.propagateSpellResult(
$scope.paragraph.id, $scope.paragraph.title,
paragraphText, [], $scope.paragraph.status, errorMessage,
$scope.paragraph.config, $scope.paragraph.settings.params,
$scope.paragraph.dateStarted, $scope.paragraph.dateFinished);
}
};
$scope.prepareSpellTransaction = function(resultsMsg, propagated, paragraphText) {
$scope.spellTransaction.totalResultCount = resultsMsg.length;
$scope.spellTransaction.renderedResultCount = 0;
$scope.spellTransaction.propagated = propagated;
$scope.spellTransaction.resultsMsg = resultsMsg;
$scope.spellTransaction.paragraphText = paragraphText;
};
/**
* - update spell transaction count and
* - check transaction is finished based on the result count
* @returns {boolean}
*/
$scope.increaseSpellTransactionResultCount = function() {
$scope.spellTransaction.renderedResultCount += 1;
const total = $scope.spellTransaction.totalResultCount;
const current = $scope.spellTransaction.renderedResultCount;
return total === current;
};
$scope.cleanupSpellTransaction = function() {
const status = ParagraphStatus.FINISHED;
$scope.paragraph.executor = ParagraphExecutor.NONE;
$scope.paragraph.status = status;
$scope.paragraph.results.code = status;
const propagated = $scope.spellTransaction.propagated;
const resultsMsg = $scope.spellTransaction.resultsMsg;
const paragraphText = $scope.spellTransaction.paragraphText;
if (!propagated) {
$scope.paragraph.dateFinished = $scope.getFormattedParagraphTime();
}
if (!propagated) {
const propagable = SpellResult.createPropagable(resultsMsg);
$scope.propagateSpellResult(
$scope.paragraph.id, $scope.paragraph.title,
paragraphText, propagable, status, '',
$scope.paragraph.config, $scope.paragraph.settings.params,
$scope.paragraph.dateStarted, $scope.paragraph.dateFinished);
}
};
$scope.runParagraphUsingSpell = function(paragraphText,
magic, digestRequired, propagated) {
$scope.paragraph.status = 'RUNNING';
$scope.paragraph.executor = ParagraphExecutor.SPELL;
$scope.paragraph.results = {};
$scope.paragraph.errorMessage = '';
if (digestRequired) {
$scope.$digest();
}
try {
// remove magic from paragraphText
const splited = paragraphText.slice(paragraphText.indexOf(magic) + magic.length);
// remove leading spaces
const textWithoutMagic = splited.replace(/^\s+/g, '');
if (!propagated) {
$scope.paragraph.dateStarted = $scope.getFormattedParagraphTime();
}
// handle actual result message in promise
heliumService.executeSpell(magic, textWithoutMagic)
.then((resultsMsg) => {
$scope.prepareSpellTransaction(resultsMsg, propagated, paragraphText);
$scope.paragraph.results.msg = resultsMsg;
$scope.paragraph.config.tableHide = false;
})
.catch((error) => {
$scope.handleSpellError(paragraphText, error,
digestRequired, propagated);
});
} catch (error) {
$scope.handleSpellError(paragraphText, error,
digestRequired, propagated);
}
};
$scope.runParagraphUsingBackendInterpreter = function(paragraphText) {
websocketMsgSrv.runParagraph($scope.paragraph.id, $scope.paragraph.title,
paragraphText, $scope.paragraph.config, $scope.paragraph.settings.params);
};
$scope.bindBeforeUnload = function() {
angular.element(window).off('beforeunload');
let confirmOnPageExit = function(e) {
// If we haven't been passed the event get the window.event
e = e || window.event;
let message = 'Do you want to reload this site?';
// For IE6-8 and Firefox prior to version 4
if (e) {
e.returnValue = message;
}
// For Chrome, Safari, IE8+ and Opera 12+
return message;
};
angular.element(window).on('beforeunload', confirmOnPageExit);
};
$scope.unBindBeforeUnload = function() {
angular.element(window).off('beforeunload');
};
$scope.saveParagraph = function(paragraph) {
const dirtyText = paragraph.text;
if (dirtyText === undefined || dirtyText === $scope.originalText) {
return;
}
$scope.bindBeforeUnload();
commitParagraph(paragraph).then(function() {
$scope.originalText = dirtyText;
$scope.dirtyText = undefined;
$scope.unBindBeforeUnload();
});
};
$scope.toggleEnableDisable = function(paragraph) {
if ($scope.isNoteRunning) {
return;
}
paragraph.config.enabled = !paragraph.config.enabled;
commitParagraph(paragraph);
};
/**
* @param paragraphText to be parsed
* @param digestRequired true if calling `$digest` is required
* @param propagated true if update request is sent from other client
*/
$scope.runParagraph = function(paragraphText, digestRequired, propagated) {
if (!paragraphText || $scope.isRunning($scope.paragraph)) {
return;
}
const magic = SpellResult.extractMagic(paragraphText);
if (heliumService.getSpellByMagic(magic)) {
$scope.runParagraphUsingSpell(paragraphText, magic, digestRequired, propagated);
} else {
$scope.runParagraphUsingBackendInterpreter(paragraphText);
}
$scope.originalText = angular.copy(paragraphText);
$scope.dirtyText = undefined;
if ($scope.paragraph.config.editorSetting.editOnDblClick) {
closeEditorAndOpenTable($scope.paragraph);
} else if (editorSetting.isOutputHidden &&
!$scope.paragraph.config.editorSetting.editOnDblClick) {
// %md/%angular repl make output to be hidden by default after running
// so should open output if repl changed from %md/%angular to another
openEditorAndOpenTable($scope.paragraph);
}
editorSetting.isOutputHidden = $scope.paragraph.config.editorSetting.editOnDblClick;
};
$scope.runParagraphFromShortcut = function(paragraphText) {
// passing `digestRequired` as true to update view immediately
// without this, results cannot be rendered in view more than once
$scope.runParagraph(paragraphText, true, false);
};
$scope.runParagraphFromButton = function() {
if ($scope.isNoteRunning) {
return;
}
// we come here from the view, so we don't need to call `$digest()`
$scope.runParagraph($scope.getEditorValue(), false, false);
};
$scope.runAllToThis = function(paragraph) {
if ($scope.isNoteRunning) {
return;
}
$scope.$emit('runAllAbove', paragraph, true);
};
$scope.runAllFromThis = function(paragraph) {
if ($scope.isNoteRunning) {
return;
}
$scope.$emit('runAllBelowAndCurrent', paragraph, true);
};
$scope.runAllFromThisFromShortcut = function(paragraph) {
if ($scope.isNoteRunning) {
return;
}
$scope.$emit('runAllBelowAndCurrent', paragraph, false);
};
$scope.runAllToThisFromShortcut = function(paragraph) {
if ($scope.isNoteRunning) {
return;
}
$scope.$emit('runAllAbove', paragraph, false);
};
$scope.turnOnAutoRun = function(paragraph) {
paragraph.config.runOnSelectionChange = !paragraph.config.runOnSelectionChange;
commitParagraph(paragraph);
};
$scope.moveUp = function(paragraph) {
if ($scope.isNoteRunning) {
return;
}
$scope.$emit('moveParagraphUp', paragraph);
};
$scope.moveDown = function(paragraph) {
if ($scope.isNoteRunning) {
return;
}
$scope.$emit('moveParagraphDown', paragraph);
};
$scope.insertNew = function(position) {
if ($scope.isNoteRunning) {
return;
}
$scope.$emit('insertParagraph', $scope.paragraph.id, position);
};
$scope.copyPara = function(position) {
if ($scope.isNoteRunning) {
return;
}
let editorValue = $scope.getEditorValue();
if (editorValue) {
$scope.copyParagraph(editorValue, position);
}
};
$scope.copyParagraph = function(data, position) {
let newIndex = -1;
for (let i = 0; i < $scope.note.paragraphs.length; i++) {
if ($scope.note.paragraphs[i].id === $scope.paragraph.id) {
// determine position of where to add new paragraph; default is below
if (position === 'above') {
newIndex = i;
} else {
newIndex = i + 1;
}
break;
}
}
if (newIndex < 0 || newIndex > $scope.note.paragraphs.length) {
return;
}
let config = angular.copy($scope.paragraph.config);
config.editorHide = false;
websocketMsgSrv.copyParagraph(newIndex, $scope.paragraph.title, data,
config, $scope.paragraph.settings.params);
};
$scope.removeParagraph = function(paragraph) {
if ($scope.isNoteRunning) {
return;
}
if ($scope.note.paragraphs.length === 1) {
BootstrapDialog.alert({
closable: true,
message: 'All the paragraphs can\'t be deleted.',
});
} else {
BootstrapDialog.confirm({
closable: true,
title: '',
message: 'Do you want to delete this paragraph?',
callback: function(result) {
if (result) {
console.log('Remove paragraph');
websocketMsgSrv.removeParagraph(paragraph.id);
$scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id);
}
},
});
}
};
$scope.clearParagraphOutput = function(paragraph) {
websocketMsgSrv.clearParagraphOutput(paragraph.id);
};
$scope.toggleEditor = function(paragraph) {
if (paragraph.config.editorHide) {
$scope.openEditor(paragraph);
} else {
$scope.closeEditor(paragraph);
}
};
$scope.closeEditor = function(paragraph) {
console.log('close the note');
paragraph.config.editorHide = true;
commitParagraph(paragraph);
};
$scope.openEditor = function(paragraph) {
console.log('open the note');
paragraph.config.editorHide = false;
commitParagraph(paragraph);
};
$scope.closeTable = function(paragraph) {
console.log('close the output');
paragraph.config.tableHide = true;
commitParagraph(paragraph);
};
$scope.openTable = function(paragraph) {
console.log('open the output');
paragraph.config.tableHide = false;
commitParagraph(paragraph);
};
$scope.showNumbering = function(paragraph) {
console.log('show numbering');
paragraph.config.numbering = true;
commitParagraph(paragraph);
};
$scope.hideNumbering = function(paragraph) {
console.log('hide numbering');
paragraph.config.numbering = false;
commitParagraph(paragraph);
};
let openEditorAndCloseTable = function(paragraph) {
manageEditorAndTableState(paragraph, false, true);
};
const closeEditorAndOpenTable = function(paragraph) {
manageEditorAndTableState(paragraph, true, false);
};
const openEditorAndOpenTable = function(paragraph) {
manageEditorAndTableState(paragraph, false, false);
};
const manageEditorAndTableState = function(paragraph, hideEditor, hideTable) {
paragraph.config.editorHide = hideEditor;
paragraph.config.tableHide = hideTable;
commitParagraph(paragraph);
};
$scope.showTitle = function(paragraph) {
paragraph.config.title = true;
commitParagraph(paragraph);
};
$scope.hideTitle = function(paragraph) {
paragraph.config.title = false;
commitParagraph(paragraph);
};
$scope.setTitle = function(paragraph) {
commitParagraph(paragraph);
};
$scope.showLineNumbers = function(paragraph) {
if ($scope.editor) {
paragraph.config.lineNumbers = true;
$scope.editor.renderer.setShowGutter(true);
commitParagraph(paragraph);
}
};
$scope.hideLineNumbers = function(paragraph) {
if ($scope.editor) {
paragraph.config.lineNumbers = false;
$scope.editor.renderer.setShowGutter(false);
commitParagraph(paragraph);
}
};
$scope.columnWidthClass = function(n) {
if ($scope.asIframe) {
return 'col-md-12';
} else {
return 'paragraph-col col-md-' + n;
}
};
$scope.changeColWidth = function(paragraph, width) {
angular.element('.navbar-right.open').removeClass('open');
paragraph.config.colWidth = width;
$scope.$broadcast('paragraphResized', $scope.paragraph.id);
commitParagraph(paragraph);
};
$scope.changeFontSize = function(paragraph, fontSize) {
angular.element('.navbar-right.open').removeClass('open');
if ($scope.editor) {
$scope.editor.setOptions({
fontSize: fontSize + 'pt',
});
autoAdjustEditorHeight($scope.editor);
paragraph.config.fontSize = fontSize;
commitParagraph(paragraph);
}
};
$scope.toggleOutput = function(paragraph) {
paragraph.config.tableHide = !paragraph.config.tableHide;
commitParagraph(paragraph);
};
$scope.aceChanged = function(_, editor) {
let session = editor.getSession();
let dirtyText = session.getValue();
$scope.dirtyText = dirtyText;
if ($scope.dirtyText !== $scope.originalText) {
if ($scope.collaborativeMode) {
$scope.sendPatch();
} else {
$scope.startSaveTimer();
}
}
setParagraphMode(session, dirtyText, editor.getCursorPosition());
if ($scope.cursorPosition) {
editor.moveCursorToPosition($scope.cursorPosition);
$scope.cursorPosition = null;
}
};
$scope.sendPatch = function() {
$scope.originalText = $scope.originalText ? $scope.originalText : '';
let patch = $scope.diffMatchPatch.patch_make($scope.originalText, $scope.dirtyText).toString();
$scope.originalText = $scope.dirtyText;
return websocketMsgSrv.patchParagraph($scope.paragraph.id, $route.current.pathParams.noteId, patch);
};
$scope.aceLoaded = function(_editor) {
let langTools = ace.require('ace/ext/language_tools');
let Range = ace.require('ace/range').Range;
_editor.$blockScrolling = Infinity;
$scope.editor = _editor;
$scope.editor.on('input', $scope.aceChanged);
if (_editor.container.id !== '{{paragraph.id}}_editor') {
$scope.editor.renderer.setShowGutter($scope.paragraph.config.lineNumbers);
$scope.editor.setShowFoldWidgets(false);
$scope.editor.setHighlightActiveLine(false);
$scope.editor.getSession().setUseWrapMode(true);
$scope.editor.setTheme('ace/theme/chrome');
$scope.editor.setReadOnly($scope.isRunning($scope.paragraph) || $scope.isNoteRunning);
$scope.editor.setHighlightActiveLine($scope.paragraphFocused);
if ($scope.paragraphFocused) {
let prefix = getParagraphMagic($scope.paragraph.text);
let paragraphText = $scope.paragraph.text ? $scope.paragraph.text.trim() : '';
$scope.editor.focus();
$scope.goToEnd($scope.editor);
if (prefix === paragraphText) {
$timeout(function() {
$scope.editor.gotoLine(2, 0);
}, 0);
}
}
autoAdjustEditorHeight(_editor);
let adjustEditorListener = () => autoAdjustEditorHeight(_editor);
angular.element(window).resize(adjustEditorListener);
$scope.$on('$destroy', () => angular.element(window).unbind('resize', adjustEditorListener));
if (navigator.appVersion.indexOf('Mac') !== -1) {
$scope.editor.setKeyboardHandler('ace/keyboard/emacs');
$rootScope.isMac = true;
} else if (navigator.appVersion.indexOf('Win') !== -1 ||
navigator.appVersion.indexOf('X11') !== -1 ||
navigator.appVersion.indexOf('Linux') !== -1) {
$rootScope.isMac = false;
// not applying emacs key binding while the binding override Ctrl-v. default behavior of paste text on windows.
}
$scope.$on('completionListLength', function(event, data) {
completionListLength = data;
});
$scope.$on('callCompletion', function(event, data) {
if($scope.paragraphFocused) {
websocketMsgSrv.completion($scope.paragraph.id, data.buf, data.pos);
}
});
let remoteCompleter = {
getCompletions: function(editor, session, pos, prefix, callback) {
let langTools = ace.require('ace/ext/language_tools');
let defaultKeywords = new Set();
// eslint-disable-next-line handle-callback-err
let getDefaultKeywords = function(err, completions) {
if (completions !== undefined) {
completions.forEach(function(c) {
defaultKeywords.add(c.value);
});
}
};
if (langTools.keyWordCompleter !== undefined) {
langTools.keyWordCompleter.getCompletions(editor, session, pos, prefix, getDefaultKeywords);
}
if (!editor.isFocused()) {
return;
}
pos = session.getTextRange(new Range(0, 0, pos.row, pos.column)).length;
let buf = session.getValue();
$rootScope.$broadcast('callCompletion', {buf: buf, pos: pos});
$scope.$on('completionList', function(event, data) {
let computeCaption = function(value, meta) {
let metaLength = meta !== undefined ? meta.length : 0;
let length = 42;
let whitespaceLength = 3;
let ellipses = '...';
let maxLengthCaption = length - metaLength - whitespaceLength - ellipses.length;
if (value !== undefined && value.length > maxLengthCaption) {
return value.substr(0, maxLengthCaption) + ellipses;
}
return value;
};
if (data.completions) {
let completions = [];
for (let c in data.completions) {
if (data.completions.hasOwnProperty(c)) {
let v = data.completions[c];
if (v.meta !== undefined && v.meta === 'keyword' && defaultKeywords.has(v.value.trim())) {
continue;
}
completions.push({
name: v.name,
value: v.value,
meta: v.meta,
caption: computeCaption(v.name, v.meta),
score: 300,
});
}
}
$rootScope.$broadcast('completionListLength', completions.length);
callback(null, completions);
}
});
},
};
langTools.setCompleters([remoteCompleter, langTools.keyWordCompleter, langTools.snippetCompleter,
langTools.textCompleter]);
$scope.editor.setOptions({
fontSize: $scope.paragraph.config.fontSize + 'pt',
enableBasicAutocompletion: true,
enableSnippets: false,
enableLiveAutocompletion: false,
});
$scope.editor.on('focus', function() {
handleFocus(true);
});
$scope.editor.on('blur', function() {
handleFocus(false);
$scope.saveParagraph($scope.paragraph);
});
$scope.editor.on('paste', function(e) {
if (e.text.indexOf('%') === 0) {
pastePercentSign = true;
}
});
$scope.editor.getSession().on('change', function(e, editSession) {
autoAdjustEditorHeight(_editor);
});
setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue());
// autocomplete on '.'
/*
$scope.editor.commands.on("afterExec", function(e, t) {
if (e.command.name == "insertstring" && e.args == "." ) {
var all = e.editor.completers;
//e.editor.completers = [remoteCompleter];
e.editor.execCommand("startAutocomplete");
//e.editor.completers = all;
}
});
*/
// remove binding
$scope.editor.commands.removeCommand('showSettingsMenu');
$scope.editor.commands.removeCommand('find');
$scope.editor.commands.removeCommand('replace');
let isOption = $rootScope.isMac ? 'option' : 'alt';
$scope.editor.commands.bindKey('ctrl-' + isOption + '-n.', null);
$scope.editor.commands.bindKey('ctrl-' + isOption + '-l', null);
$scope.editor.commands.bindKey('ctrl-' + isOption + '-w', null);
$scope.editor.commands.bindKey('ctrl-' + isOption + '-a', null);
$scope.editor.commands.bindKey('ctrl-' + isOption + '-k', null);
$scope.editor.commands.bindKey('ctrl-' + isOption + '-e', null);
$scope.editor.commands.bindKey('ctrl-' + isOption + '-t', null);
$scope.editor.commands.bindKey('ctrl-space', null);
if ($rootScope.isMac) {
$scope.editor.commands.bindKey('command-l', null);
} else {
$scope.editor.commands.bindKey('ctrl-l', null);
}
// autocomplete on 'ctrl+.'
$scope.editor.commands.bindKey('ctrl-.', 'startAutocomplete');
// Show autocomplete on tab
$scope.editor.commands.addCommand({
name: 'tabAutocomplete',
bindKey: {
win: 'tab',
mac: 'tab',
sender: 'editor|cli',
},
exec: function(env, args, request) {
let iCursor = $scope.editor.getCursorPosition();
let currentLine = $scope.editor.session.getLine(iCursor.row);
let isAllTabs = currentLine.substring(0, iCursor.column - 1).split('').every(function(char) {
return (char === '\t' || char === ' ');
});
// If user has pressed tab on first line char or if isTabCompletion() is false, keep existing behavior
// If user has pressed tab anywhere in between and editor mode is not %md, show autocomplete
if (!isAllTabs && iCursor.column && isTabCompletion()) {
$scope.editor.execCommand('startAutocomplete');
} else {
ace.config.loadModule('ace/ext/language_tools', function() {
$scope.editor.indent();
});
}
},
});
let keyBindingEditorFocusAction = function(scrollValue) {
let numRows = $scope.editor.getSession().getLength();
let currentRow = $scope.editor.getCursorPosition().row;
if (currentRow === 0 && scrollValue <= 0) {
// move focus to previous paragraph
$scope.$emit('moveFocusToPreviousParagraph', $scope.paragraph.id);
} else if (currentRow === numRows - 1 && scrollValue >= 0) {
$scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id);
} else {
$scope.scrollToCursor($scope.paragraph.id, scrollValue);
}
};
// handle cursor moves
$scope.editor.keyBinding.origOnCommandKey = $scope.editor.keyBinding.onCommandKey;
$scope.editor.keyBinding.onCommandKey = function(e, hashId, keyCode) {
if ($scope.editor.completer && $scope.editor.completer.activated) { // if autocompleter is active
} else {
// fix ace editor focus issue in chrome (textarea element goes to top: -1000px after focused by cursor move)
if (parseInt(angular.element('#' + $scope.paragraph.id + '_editor > textarea')
.css('top').replace('px', '')) < 0) {
let position = $scope.editor.getCursorPosition();
let cursorPos = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true);
angular.element('#' + $scope.paragraph.id + '_editor > textarea').css('top', cursorPos.top);
}
let ROW_UP = -1;
let ROW_DOWN = 1;
switch (keyCode) {
case 38:
if (!e.shiftKey) {
keyBindingEditorFocusAction(ROW_UP);
}
break;
case 80:
if (e.ctrlKey && !e.altKey) {
keyBindingEditorFocusAction(ROW_UP);
}
break;
case 40:
if (!e.shiftKey) {
keyBindingEditorFocusAction(ROW_DOWN);
}
break;
case 78:
if (e.ctrlKey && !e.altKey) {
keyBindingEditorFocusAction(ROW_DOWN);
}
break;
}
}
this.origOnCommandKey(e, hashId, keyCode);
};
}
};
// ref: https://github.com/ajaxorg/ace/blob/5021d0193d9f2bba5a978d0b1d7a4f73d18ce713/lib/ace/autocomplete.js#L454
const completionSupportWithoutBackend = function(str) {
let matches;
if (str.length > this.filterText && str.lastIndexOf(
this.filterText, 0) === 0) {
matches = this.filtered;
} else {
matches = this.all;
}
this.filterText = str;
matches = this.filterCompletions(matches, this.filterText);
matches = matches.sort(function(a, b) {
return b.exactMatch - a.exactMatch || b.score - a.score;
});
let prev = null;
matches = matches.filter(function(item) {
let caption = item.snippet || item.caption || item.value;
if (caption === prev) {
return false;
}
prev = caption;
return true;
});
this.filtered = matches;
};
const completionSupportWithBackend = function(str) {
let matches;
if (str.length > this.filterText && str.lastIndexOf(
this.filterText, 0) === 0) {
matches = this.filtered;
} else {
matches = this.all;
}
this.filterText = str;
matches = this.filterCompletions(matches, this.filterText);
matches = matches.sort(function(a, b) {
return b.exactMatch - a.exactMatch || b.score - a.score;
});
let prev = null;
matches = matches.filter(function(item) {
if (!_.isEmpty(item.meta)) {
if (completionListLength !== 0) {
return false;
}
}
let caption = item.snippet || item.caption || item.value;
if (caption === prev) {
return false;
}
prev = caption;
return true;
});
this.filtered = matches;
completionListLength = undefined;
};
const handleFocus = function(focused, isDigestPass) {
$scope.paragraphFocused = focused;
if (focused) {
let filteredList = ace.require('ace/autocomplete').FilteredList;
if ($scope.paragraph.config.editorSetting.completionSupport) {
filteredList.prototype.setFilter = completionSupportWithBackend;
} else {
filteredList.prototype.setFilter = completionSupportWithoutBackend;
}
}
if ($scope.editor) {
$scope.editor.setHighlightActiveLine(focused);
}
if (isDigestPass === false || isDigestPass === undefined) {
// Protect against error in case digest is already running
$timeout(function() {
// Apply changes since they come from 3rd party library
$scope.$digest();
});
}
};
let getEditorSetting = function(paragraph, pragraphText) {
let deferred = $q.defer();
if (!$scope.revisionView) {
websocketMsgSrv.getEditorSetting(paragraph.id, pragraphText);
$timeout(
$scope.$on('editorSetting', function(event, data) {
if (paragraph.id === data.paragraphId) {
deferred.resolve(data);
}
}
), 1000);
}
return deferred.promise;
};
let setEditorLanguage = function(session, language) {
let mode = 'ace/mode/';
mode += language;
$scope.paragraph.config.editorMode = mode;
session.setMode(mode);
};
const setParagraphMode = function(session, paragraphText, pos) {
// Evaluate the mode only if the the position is undefined
// or the first 30 characters of the paragraph have been modified
// or cursor position is at beginning of second line.(in case user hit enter after typing %magic)
if ((typeof pos === 'undefined') || (pos.row === 0 && pos.column < 30) ||
(pos.row === 1 && pos.column === 0) || pastePercentSign) {
// If paragraph loading, use config value if exists
if ((typeof pos === 'undefined') && $scope.paragraph.config.editorMode &&
!setInterpreterBindings) {
session.setMode($scope.paragraph.config.editorMode);
} else {
let magic = getParagraphMagic(paragraphText);
if (editorSetting.magic !== magic) {
editorSetting.magic = magic;
getEditorSetting($scope.paragraph, paragraphText)
.then(function(setting) {
setEditorLanguage(session, setting.editor.language);
_.merge($scope.paragraph.config.editorSetting, setting.editor);
});
}
}
}
pastePercentSign = false;
setInterpreterBindings = false;
};
// return the text that is composed of interpreter name and paragraph properties
const getParagraphMagic = function(paragraphText) {
let intpNameRegexp = /^\s*(%.+?)(\s)/g;
let match = intpNameRegexp.exec(paragraphText);
if (match) {
return match[1].trim();
// get default interpreter name if paragraph text doesn't start with '%'
// TODO(mina): dig into the cause what makes interpreterBindings to have no element
} else if ($scope.$parent.interpreterBindings && $scope.$parent.interpreterBindings.length !== 0) {
return $scope.$parent.interpreterBindings[0].name;
}
return '';
};
const autoAdjustEditorHeight = function(editor) {
let height =
editor.getSession().getScreenLength() *
editor.renderer.lineHeight;
angular.element('#' + editor.container.id).height(height.toString() + 'px');
editor.resize();
};
$rootScope.$on('scrollToCursor', function(event) {
// scroll on 'scrollToCursor' event only when cursor is in the last paragraph
let paragraphs = angular.element('div[id$="_paragraphColumn_main"]');
if (paragraphs[paragraphs.length - 1].id.indexOf($scope.paragraph.id) === 0) {
$scope.scrollToCursor($scope.paragraph.id, 0);
}
});
/** scrollToCursor if it is necessary
* when cursor touches scrollTriggerEdgeMargin from the top (or bottom) of the screen, it autoscroll to place cursor around 1/3 of screen height from the top (or bottom)
* paragraphId : paragraph that has active cursor
* lastCursorMove : 1(down), 0, -1(up) last cursor move event
**/
$scope.scrollToCursor = function(paragraphId, lastCursorMove) {
if (!$scope.editor || !$scope.editor.isFocused()) {
// only make sense when editor is focused
return;
}
let lineHeight = $scope.editor.renderer.lineHeight;
let headerHeight = 103; // menubar, notebook titlebar
let scrollTriggerEdgeMargin = 50;
let documentHeight = angular.element(document).height();
let windowHeight = angular.element(window).height(); // actual viewport height
let scrollPosition = angular.element(document).scrollTop();
let editorPosition = angular.element('#' + paragraphId + '_editor').offset();
let position = $scope.editor.getCursorPosition();
let lastCursorPosition = $scope.editor.renderer.$cursorLayer.getPixelPosition(position, true);
let calculatedCursorPosition = editorPosition.top + lastCursorPosition.top + lineHeight * lastCursorMove;
let scrollTargetPos;
if (calculatedCursorPosition < scrollPosition + headerHeight + scrollTriggerEdgeMargin) {
scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) / 3);
if (scrollTargetPos < 0) {
scrollTargetPos = 0;
}
} else if (calculatedCursorPosition > scrollPosition + scrollTriggerEdgeMargin + windowHeight - headerHeight) {
scrollTargetPos = calculatedCursorPosition - headerHeight - ((windowHeight - headerHeight) * 2 / 3);
if (scrollTargetPos > documentHeight) {
scrollTargetPos = documentHeight;
}
}
// cancel previous scroll animation
let bodyEl = angular.element('body');
bodyEl.stop();
bodyEl.finish();
// scroll to scrollTargetPos
if (scrollTargetPos) {
bodyEl.scrollTo(scrollTargetPos, {axis: 'y', interrupt: true, duration: 100});
}
};
$scope.getEditorValue = function() {
return !$scope.editor || $scope.viewOnly ? $scope.paragraph.text : $scope.editor.getValue();
};
$scope.getProgress = function() {
return $scope.currentProgress || 0;
};
$scope.getFormattedParagraphTime = () => {
return moment().toISOString();
};
$scope.getExecutionTime = function(pdata) {
const end = pdata.dateFinished;
const start = pdata.dateStarted;
let timeMs = Date.parse(end) - Date.parse(start);
if (isNaN(timeMs) || timeMs < 0) {
if ($scope.isResultOutdated(pdata)) {
return 'outdated';
}
return '';
}
const durationFormat = moment.duration((timeMs / 1000), 'seconds').format('h [hrs] m [min] s [sec]');
const endFormat = moment(pdata.dateFinished).format('MMMM DD YYYY, h:mm:ss A');
let user = (pdata.user === undefined || pdata.user === null) ? 'anonymous' : pdata.user;
let desc = `Took ${durationFormat}. Last updated by ${user} at ${endFormat}.`;
if ($scope.isResultOutdated(pdata)) {
desc += ' (outdated)';
}
return desc;
};
$scope.getElapsedTime = function(paragraph) {
return 'Started ' + moment(paragraph.dateStarted).fromNow() + '.';
};
$scope.isResultOutdated = function(pdata) {
if (pdata.dateUpdated !== undefined && Date.parse(pdata.dateUpdated) > Date.parse(pdata.dateStarted)) {
return true;
}
return false;
};
$scope.goToEnd = function(editor) {
editor.navigateFileEnd();
};
$scope.parseTableCell = function(cell) {
if (!isNaN(cell)) {
if (cell.length === 0 || Number(cell) > Number.MAX_SAFE_INTEGER || Number(cell) < Number.MIN_SAFE_INTEGER) {
return cell;
} else {
return Number(cell);
}
}
let d = moment(cell);
if (d.isValid()) {
return d;
}
return cell;
};
const commitParagraph = function(paragraph) {
const {
id,
title,
text,
config,
settings: {params},
} = paragraph;
return websocketMsgSrv.commitParagraph(id, title, text, config, params,
$route.current.pathParams.noteId);
};
/** Utility function */
$scope.goToSingleParagraph = function() {
let noteId = $route.current.pathParams.noteId;
let redirectToUrl = location.protocol + '//' + location.host + location.pathname + '#/notebook/' + noteId +
'/paragraph/' + $scope.paragraph.id + '?asIframe';
$window.open(redirectToUrl);
};
$scope.showScrollDownIcon = function(id) {
let doc = angular.element('#p' + id + '_text');
if (doc[0]) {
return doc[0].scrollHeight > doc.innerHeight();
}
return false;
};
$scope.scrollParagraphDown = function(id) {
let doc = angular.element('#p' + id + '_text');
doc.animate({scrollTop: doc[0].scrollHeight}, 500);
$scope.keepScrollDown = true;
};
$scope.showScrollUpIcon = function(id) {
if (angular.element('#p' + id + '_text')[0]) {
return angular.element('#p' + id + '_text')[0].scrollTop !== 0;
}
return false;
};
$scope.scrollParagraphUp = function(id) {
let doc = angular.element('#p' + id + '_text');
doc.animate({scrollTop: 0}, 500);
$scope.keepScrollDown = false;
};
$scope.$on('angularObjectUpdate', function(event, data) {
let noteId = $route.current.pathParams.noteId;
if (!data.noteId || data.noteId === noteId) {
let scope;
let registry;
if (!data.paragraphId || data.paragraphId === $scope.paragraph.id) {
scope = paragraphScope;
registry = angularObjectRegistry;
} else {
return;
}
let varName = data.angularObject.name;
if (angular.equals(data.angularObject.object, scope[varName])) {
// return when update has no change
return;
}
if (!registry[varName]) {
registry[varName] = {
interpreterGroupId: data.interpreterGroupId,
noteId: data.noteId,
paragraphId: data.paragraphId,
};
} else {
registry[varName].noteId = registry[varName].noteId || data.noteId;
registry[varName].paragraphId = registry[varName].paragraphId || data.paragraphId;
}
registry[varName].skipEmit = true;
if (!registry[varName].clearWatcher) {
registry[varName].clearWatcher = scope.$watch(varName, function(newValue, oldValue) {
console.log('angular object (paragraph) updated %o %o', varName, registry[varName]);
if (registry[varName].skipEmit) {
registry[varName].skipEmit = false;
return;
}
websocketMsgSrv.updateAngularObject(
registry[varName].noteId,
registry[varName].paragraphId,
varName,
newValue,
registry[varName].interpreterGroupId);
});
}
console.log('angular object (paragraph) created %o', varName);
scope[varName] = data.angularObject.object;
// create proxy for AngularFunction
if (varName.indexOf(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX) === 0) {
let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length);
scope[funcName] = function() {
// eslint-disable-next-line prefer-rest-params
scope[varName] = arguments;
// eslint-disable-next-line prefer-rest-params
console.log('angular function (paragraph) invoked %o', arguments);
};
console.log('angular function (paragraph) created %o', scope[funcName]);
}
}
});
$scope.$on('updateParaInfos', function(event, data) {
if (data.id === $scope.paragraph.id) {
$scope.paragraph.runtimeInfos = data.infos;
}
});
$scope.$on('angularObjectRemove', function(event, data) {
let noteId = $route.current.pathParams.noteId;
if (!data.noteId || data.noteId === noteId) {
let scope;
let registry;
if (!data.paragraphId || data.paragraphId === $scope.paragraph.id) {
scope = paragraphScope;
registry = angularObjectRegistry;
} else {
return;
}
let varName = data.name;
// clear watcher
if (registry[varName]) {
registry[varName].clearWatcher();
registry[varName] = undefined;
}
// remove scope variable
scope[varName] = undefined;
// remove proxy for AngularFunction
if (varName.indexOf(ANGULAR_FUNCTION_OBJECT_NAME_PREFIX) === 0) {
let funcName = varName.substring((ANGULAR_FUNCTION_OBJECT_NAME_PREFIX).length);
scope[funcName] = undefined;
}
}
});
/**
* @returns {boolean} true if updated is needed
*/
function isUpdateRequired(oldPara, newPara) {
return (newPara.id === oldPara.id &&
(newPara.dateCreated !== oldPara.dateCreated ||
newPara.text !== oldPara.text ||
newPara.dateFinished !== oldPara.dateFinished ||
newPara.dateStarted !== oldPara.dateStarted ||
newPara.dateUpdated !== oldPara.dateUpdated ||
newPara.status !== oldPara.status ||
newPara.jobName !== oldPara.jobName ||
newPara.title !== oldPara.title ||
isEmpty(newPara.results) !== isEmpty(oldPara.results) ||
newPara.errorMessage !== oldPara.errorMessage ||
!angular.equals(newPara.settings, oldPara.settings) ||
!angular.equals(newPara.config, oldPara.config) ||
!angular.equals(newPara.runtimeInfos, oldPara.runtimeInfos)));
}
$scope.updateAllScopeTexts = function(oldPara, newPara) {
if (oldPara.text !== newPara.text) {
if ($scope.dirtyText) { // check if editor has local update
if ($scope.dirtyText === newPara.text) { // when local update is the same from remote, clear local update
$scope.paragraph.text = newPara.text;
$scope.dirtyText = undefined;
$scope.originalText = angular.copy(newPara.text);
} else { // if there're local update, keep it.
$scope.paragraph.text = newPara.text;
}
} else {
$scope.paragraph.text = newPara.text;
$scope.originalText = angular.copy(newPara.text);
}
}
};
$scope.updateParagraphObjectWhenUpdated = function(newPara) {
// resize col width
if ($scope.paragraph.config.colWidth !== newPara.config.colWidth) {
$scope.$broadcast('paragraphResized', $scope.paragraph.id);
}
if ($scope.paragraph.config.fontSize !== newPara.config.fontSize) {
$rootScope.$broadcast('fontSizeChanged', newPara.config.fontSize);
}
/** push the rest */
$scope.paragraph.aborted = newPara.aborted;
$scope.paragraph.user = newPara.user;
$scope.paragraph.dateUpdated = newPara.dateUpdated;
$scope.paragraph.dateCreated = newPara.dateCreated;
$scope.paragraph.dateFinished = newPara.dateFinished;
$scope.paragraph.dateStarted = newPara.dateStarted;
$scope.paragraph.errorMessage = newPara.errorMessage;
$scope.paragraph.jobName = newPara.jobName;
$scope.paragraph.title = newPara.title;
$scope.paragraph.lineNumbers = newPara.lineNumbers;
$scope.paragraph.status = newPara.status;
$scope.paragraph.fontSize = newPara.fontSize;
if (newPara.status !== ParagraphStatus.RUNNING) {
$scope.paragraph.results = newPara.results;
}
$scope.paragraph.settings = newPara.settings;
$scope.paragraph.runtimeInfos = newPara.runtimeInfos;
if ($scope.editor) {
let isReadOnly = $scope.isRunning(newPara) || $scope.isNoteRunning;
$scope.editor.setReadOnly(isReadOnly);
}
if (!$scope.asIframe) {
$scope.paragraph.config = newPara.config;
initializeDefault(newPara.config);
} else {
newPara.config.editorHide = true;
newPara.config.tableHide = false;
$scope.paragraph.config = newPara.config;
}
};
$scope.updateParagraph = function(oldPara, newPara, updateCallback) {
// 1. can't update on revision view
if ($scope.revisionView === true) {
return;
}
// 2. get status, refreshed
const statusChanged = (newPara.status !== oldPara.status);
const resultRefreshed = (newPara.dateFinished !== oldPara.dateFinished) ||
isEmpty(newPara.results) !== isEmpty(oldPara.results) ||
newPara.status === ParagraphStatus.ERROR ||
(newPara.status === ParagraphStatus.FINISHED && statusChanged);
// 3. update texts managed by $scope
$scope.updateAllScopeTexts(oldPara, newPara);
// 4. execute callback to update result
updateCallback();
// 5. update remaining paragraph objects
$scope.updateParagraphObjectWhenUpdated(newPara);
// 6. handle scroll down by key properly if new paragraph is added
if (statusChanged || resultRefreshed) {
// when last paragraph runs, zeppelin automatically appends new paragraph.
// this broadcast will focus to the newly inserted paragraph
const paragraphs = angular.element('div[id$="_paragraphColumn_main"]');
if (paragraphs.length >= 2 && paragraphs[paragraphs.length - 2].id.indexOf($scope.paragraph.id) === 0) {
// rendering output can took some time. So delay scrolling event firing for sometime.
setTimeout(() => {
$rootScope.$broadcast('scrollToCursor');
}, 500);
}
}
};
/** $scope.$on */
$scope.$on('runParagraphUsingSpell', function(event, data) {
const oldPara = $scope.paragraph;
let newPara = data.paragraph;
const updateCallback = () => {
$scope.runParagraph(newPara.text, true, true);
};
if (!isUpdateRequired(oldPara, newPara)) {
return;
}
$scope.updateParagraph(oldPara, newPara, updateCallback);
});
$scope.$on('updateParagraph', function(event, data) {
const oldPara = $scope.paragraph;
const newPara = data.paragraph;
if (!isUpdateRequired(oldPara, newPara)) {
return;
}
const updateCallback = () => {
// broadcast `updateResult` message to trigger result update
if (newPara.results && newPara.results.msg) {
for (let i in newPara.results.msg) {
if (newPara.results.msg.hasOwnProperty(i)) {
const newResult = newPara.results.msg ? newPara.results.msg[i] : {};
const oldResult = (oldPara.results && oldPara.results.msg)
? oldPara.results.msg[i] : {};
const newConfig = newPara.config.results ? newPara.config.results[i] : {};
const oldConfig = oldPara.config.results ? oldPara.config.results[i] : {};
if (!angular.equals(newResult, oldResult) ||
!angular.equals(newConfig, oldConfig)) {
$rootScope.$broadcast('updateResult', newResult, newConfig, newPara, parseInt(i));
}
}
}
}
};
$scope.updateParagraph(oldPara, newPara, updateCallback);
});
$scope.$on('patchReceived', function(event, data) {
if (data.paragraphId === $scope.paragraph.id) {
let patch = data.patch;
patch = $scope.diffMatchPatch.patch_fromText(patch);
if (!$scope.paragraph.text || $scope.paragraph.text === undefined) {
$scope.paragraph.text = '';
}
$scope.paragraph.text = $scope.diffMatchPatch.patch_apply(patch, $scope.paragraph.text)[0];
$scope.originalText = angular.copy($scope.paragraph.text);
let newPosition = $scope.editor.getCursorPosition();
if (newPosition && newPosition.row && newPosition.column) {
$scope.cursorPosition = $scope.editor.getCursorPosition();
}
}
});
$scope.$on('updateProgress', function(event, data) {
if (data.id === $scope.paragraph.id) {
$scope.currentProgress = data.progress;
}
});
$scope.$on('updateStatus', function(event, data) {
if (data.id === $scope.paragraph.id) {
$scope.paragraph.status = data.status;
}
});
$scope.$on('appendParagraphOutput', function(event, data) {
if (data.paragraphId === $scope.paragraph.id) {
if (!$scope.paragraph.results) {
$scope.paragraph.results = {};
if (!$scope.paragraph.results.msg) {
$scope.paragraph.results.msg = [];
}
$scope.paragraph.results.msg[data.index] = {
data: data.data,
type: data.type,
};
$rootScope.$broadcast(
'updateResult',
$scope.paragraph.results.msg[data.index],
$scope.paragraph.config.results[data.index],
$scope.paragraph,
data.index);
}
}
});
$scope.$on('keyEvent', function(event, keyEvent) {
if ($scope.paragraphFocused) {
let paragraphId = $scope.paragraph.id;
let keyCode = keyEvent.keyCode;
let noShortcutDefined = false;
let editorHide = $scope.paragraph.config.editorHide;
if (editorHide && (keyCode === 38 || (keyCode === 80 && keyEvent.ctrlKey && !keyEvent.altKey))) { // up
// move focus to previous paragraph
$scope.$emit('moveFocusToPreviousParagraph', paragraphId);
} else if (editorHide && (keyCode === 40 || (keyCode === 78 && keyEvent.ctrlKey && !keyEvent.altKey))) { // down
// move focus to next paragraph
// $timeout stops chaining effect of focus propogation
$timeout(() => $scope.$emit('moveFocusToNextParagraph', paragraphId));
} else if (!keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 13) { // Shift + Enter
$scope.runParagraphFromShortcut($scope.getEditorValue());
} else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 38) { // Ctrl + Shift + UP
$scope.runAllToThisFromShortcut($scope.paragraph);
} else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 40) { // Ctrl + Shift + Down
$scope.runAllFromThisFromShortcut($scope.paragraph);
}else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 67) { // Ctrl + Alt + c
$scope.cancelParagraph($scope.paragraph);
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 68) { // Ctrl + Alt + d
$scope.removeParagraph($scope.paragraph);
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 75) { // Ctrl + Alt + k
$scope.moveUp($scope.paragraph);
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 74) { // Ctrl + Alt + j
$scope.moveDown($scope.paragraph);
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 65) { // Ctrl + Alt + a
$scope.insertNew('above');
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 66) { // Ctrl + Alt + b
$scope.insertNew('below');
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 79) { // Ctrl + Alt + o
$scope.toggleOutput($scope.paragraph);
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 82) { // Ctrl + Alt + r
$scope.toggleEnableDisable($scope.paragraph);
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 69) { // Ctrl + Alt + e
$scope.toggleEditor($scope.paragraph);
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 77) { // Ctrl + Alt + m
if ($scope.paragraph.config.lineNumbers) {
$scope.hideLineNumbers($scope.paragraph);
} else {
$scope.showLineNumbers($scope.paragraph);
}
} else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 189) { // Ctrl + Shift + -
$scope.changeColWidth($scope.paragraph, Math.max(1, $scope.paragraph.config.colWidth - 1));
} else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 187) { // Ctrl + Shift + =
$scope.changeColWidth($scope.paragraph, Math.min(12, $scope.paragraph.config.colWidth + 1));
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 84) { // Ctrl + Alt + t
if ($scope.paragraph.config.title) {
$scope.hideTitle($scope.paragraph);
} else {
$scope.showTitle($scope.paragraph);
}
} else if (keyEvent.ctrlKey && keyEvent.shiftKey && keyCode === 67) { // Ctrl + Alt + c
$scope.copyPara('below');
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 76) { // Ctrl + Alt + l
$scope.clearParagraphOutput($scope.paragraph);
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 87) { // Ctrl + Alt + w
$scope.goToSingleParagraph();
} else if (keyEvent.ctrlKey && keyEvent.altKey && keyCode === 70) { // Ctrl + f
$scope.$emit('toggleSearchBox');
} else {
noShortcutDefined = true;
}
if (!noShortcutDefined) {
keyEvent.preventDefault();
}
}
});
$scope.$on('focusParagraph', function(event, paragraphId, cursorPosRow, cursorPosCol, mouseEvent) {
if (cursorPosCol === null || cursorPosCol === undefined) {
cursorPosCol = 0;
}
if ($scope.paragraph.id === paragraphId) {
// focus editor
if (!$scope.paragraph.config.editorHide) {
if (!mouseEvent) {
$scope.editor.focus();
// move cursor to the first row (or the last row)
let row;
if (cursorPosRow >= 0) {
row = cursorPosRow;
$scope.editor.gotoLine(row, cursorPosCol);
} else {
row = $scope.editor.session.getLength();
$scope.editor.gotoLine(row, cursorPosCol);
}
$scope.scrollToCursor($scope.paragraph.id, cursorPosCol);
}
}
handleFocus(true);
} else {
if ($scope.editor !== undefined && $scope.editor !== null) {
$scope.editor.blur();
}
let isDigestPass = true;
handleFocus(false, isDigestPass);
}
});
$scope.$on('saveInterpreterBindings', function(event, paragraphId) {
if ($scope.paragraph.id === paragraphId && $scope.editor) {
setInterpreterBindings = true;
setParagraphMode($scope.editor.getSession(), $scope.editor.getSession().getValue());
}
});
$scope.$on('doubleClickParagraph', function(event, paragraphId) {
if ($scope.paragraph.id === paragraphId && $scope.paragraph.config.editorHide &&
$scope.paragraph.config.editorSetting.editOnDblClick && $scope.revisionView !== true) {
let deferred = $q.defer();
openEditorAndCloseTable($scope.paragraph);
$timeout(
$scope.$on('updateParagraph', function(event, data) {
deferred.resolve(data);
}
), 1000);
deferred.promise.then(function(data) {
if ($scope.editor) {
$scope.editor.focus();
$scope.goToEnd($scope.editor);
}
});
}
});
$scope.$on('openEditor', function(event) {
$scope.openEditor($scope.paragraph);
});
$scope.$on('closeEditor', function(event) {
$scope.closeEditor($scope.paragraph);
});
$scope.$on('openTable', function(event) {
$scope.openTable($scope.paragraph);
});
$scope.$on('closeTable', function(event) {
$scope.closeTable($scope.paragraph);
});
$scope.$on('setNumbering', function(event, value) {
if (value) {
$scope.showNumbering($scope.paragraph);
} else {
$scope.hideNumbering($scope.paragraph);
}
});
$scope.$on('resultRendered', function(event, paragraphId) {
if ($scope.paragraph.id !== paragraphId) {
return;
}
/** increase spell result count and return if not finished */
if (!$scope.increaseSpellTransactionResultCount()) {
return;
}
$scope.cleanupSpellTransaction();
});
$scope.$on('fontSizeChanged', function(event, fontSize) {
if ($scope.editor) {
$scope.editor.setOptions({
fontSize: fontSize + 'pt',
});
}
});
const clearSearchSelection = function() {
for (let i = 0; i < searchRanges.length; ++i) {
$scope.editor.session.removeMarker(searchRanges[i].markerId);
}
searchRanges = [];
if (currentRange.id !== -1) {
$scope.editor.session.removeMarker(currentRange.markerId);
}
currentRange = getCurrentRangeDefault();
};
$scope.onEditorClick = function() {
$scope.$emit('editorClicked');
};
$scope.$on('unmarkAll', function() {
clearSearchSelection();
});
const markAllOccurrences = function(text) {
clearSearchSelection();
if (text === '') {
return;
}
if ($scope.editor.findAll(text) === 0) {
return;
}
let ranges = $scope.editor.selection.getAllRanges();
$scope.editor.selection.toSingleRange();
$scope.editor.selection.clearSelection();
for (let i = 0; i < ranges.length; ++i) {
let id = $scope.editor.session.addMarker(ranges[i], 'ace_selected-word', 'text');
searchRanges.push({markerId: id, range: ranges[i]});
}
};
$scope.$on('markAllOccurrences', function(event, text) {
markAllOccurrences(text);
if (searchRanges.length > 0) {
$scope.$emit('occurrencesExists', searchRanges.length);
}
});
$scope.$on('nextOccurrence', function(event, paragraphId) {
if ($scope.paragraph.id !== paragraphId) {
return;
}
let highlightedRangeExists = currentRange.id !== -1;
if (highlightedRangeExists) {
$scope.editor.session.removeMarker(currentRange.markerId);
currentRange.markerId = -1;
}
++currentRange.id;
if (currentRange.id >= searchRanges.length) {
currentRange.id = -1;
$scope.$emit('noNextOccurrence');
return;
}
currentRange.markerId = $scope.editor.session.addMarker(
searchRanges[currentRange.id].range, 'ace_selection', 'text');
});
$scope.$on('prevOccurrence', function(event, paragraphId) {
if ($scope.paragraph.id !== paragraphId) {
return;
}
let highlightedRangeExists = currentRange.id !== -1;
if (highlightedRangeExists) {
$scope.editor.session.removeMarker(currentRange.markerId);
currentRange.markerId = -1;
}
if (currentRange.id === -1) {
currentRange.id = searchRanges.length;
}
--currentRange.id;
if (currentRange.id === -1) {
$scope.$emit('noPrevOccurrence');
return;
}
currentRange.markerId = $scope.editor.session.addMarker(
searchRanges[currentRange.id].range, 'ace_selection', 'text');
});
$scope.$on('replaceCurrent', function(event, from, to) {
if (currentRange.id === -1) {
return;
}
let indexFromEnd = searchRanges.length - currentRange.id - 1;
let prevId = currentRange.id;
$scope.editor.session.removeMarker(currentRange.markerId);
$scope.editor.session.replace(searchRanges[currentRange.id].range, to);
markAllOccurrences(from);
let currentIndex = searchRanges.length - indexFromEnd;
$scope.$emit('occurrencesCountChanged', currentIndex - prevId - 1);
currentRange.id = currentIndex;
if (currentRange.id === searchRanges.length) {
currentRange.id = -1;
$scope.$emit('noNextOccurrenceAfterReplace');
} else {
currentRange.markerId = $scope.editor.session.addMarker(
searchRanges[currentRange.id].range, 'ace_selection', 'text');
}
});
$scope.$on('replaceAll', function(event, from, to) {
clearSearchSelection();
$scope.editor.replaceAll(to, {needle: from});
});
$scope.$on('checkOccurrences', function() {
if (searchRanges.length > 0) {
$scope.$emit('occurrencesExists', searchRanges.length);
}
});
}