// Get any elem by ID const get = function(elem) { return document.getElementById(elem); }; // Main ICEcoder object var ICEcoder = { // ==== // INIT // ==== // URLs we're viewing ICEcoder and its assets from iceLoc: window.location.origin + window.location.pathname.replace(/\/$/, ""), assetsLoc: get('icecoderJSFile').dataset.assetsRoot, // Define settings filesW: 250, // Width of files pane minFilesW: 14, // Min width of files pane maxFilesW: 250, // Max width of files pane selectedTab: 0, // The tab that's currently selected savedPoints: [], // Ints array to indicate save points for docs savedContents: [], // Array of last known saved contents openFiles: [], // Array of open file URLs openFileMDTs: [], // Array of open file modification datetimes openFileVersions: [], // Array of open file version counts cMInstances: [], // List of CodeMirror instance no's nextcMInstance: 1, // Next available CodeMirror instance no selectedFiles: [], // Array of selected files thisFileFolderType: '', // The type of current item - file or folder thisFileFolderLink: '', // The id value of the current item results: [], // Array of find coords (line & char) resultsLines: [], // Array of lines containing results (simpler version of results) findResult: 0, // Array position of current find in results findRegex: false, // If find attempts are done using regex findUpdateMultiInfoID: [], // ID of multiple results modal elem to update & text, when rename/replace is successful scrollbarVisible: false, // Indicates if the main pane has a scrollbar mouseDown: false, // If the mouse is down mouseDownInCM: false, // If the mouse is down within CodeMirror instance (can be false, 'editor' or 'gutter') draggingFilesW: false, // If we're dragging the file manager width draggingTab: false, // If we're dragging a tab draggingWithKey: false, // The key that's down while dragging, false if no key tabLeftPos: [], // Array of left positions of tabs inside content area overCloseLink: false, // If mouse is over close link on tab or not colorCurrentBG: '#1d1d1b', // Current tab/file background color colorCurrentText: '#fff', // Current tab/file text color colorOpenBG: '#c3c3c3', // Open tab/file background color colorOpenTextFile: '#000', // Open file text color colorOpenTextTab: '#888', // Open tab text color colorSelectedBG: '#49d', // Selected tab/file background color colorSelectedText: '#fff', // Selected tab/file text color colorDropTgtBGFile: '#f80', // Drop dir target background color prevTab: 0, // Previous tab to current serverQueueItems: [], // Array of URLs to call in order origCursorPos: false, // Original cursor position before jump to definition origSelectionPos: false, // Original selection position before jump to definition previewWindow: false, // Target variable for the preview window previewWindowLoading: false, // Loading state of preview window pluginIntervalRefs: [], // Array of plugin interval refs overPopup: false, // Indicates if we're over a popup or not cmdKey: false, // Tracking Apple Command key up/down state codeZoomedOut: false, // If true, code on non declaration lines is zoomed out showingTool: false, // Which tool is showing right now (terminal, output, database, git etc) oppTagReplaceData: [], // Will contain data for automatic opposite tag replacement to sync them fmReady: false, // Indicates if the file manager is ready for action bugReportStatus: "off", // Values of: off, error, ok, bugs bugReportPath: "", // Bug report file path bugFilesSizesSeen: [], // Array of last seen sizes of bug files bugFilesSizesActual: [], // Array of actual sizes of bug files splitPane: false, // Single or split pane editing splitPaneLeftPerc: 100, // Width of left pane as a percentage renderLineStyle: [], // Array of styles to apply on renderLine event renderPaneShiftAmount: 0, // Shift comparison main (negative) vs diff pane (positive) editorFocusInstance: "", // Name of editor instance that has focus openSeconds: 0, // Number of seconds ICEcoder has been open for indexing: false, // Indicates if ICEcoder is currently indexing ready: false, // Indicates if ICEcoder is ready for action // Set our aliases initAliases: function() { const aliasArray = ["header", "files", "fileOptions", "optionsFile", "optionsEdit", "optionsSettings", "optionsHelp", "filesFrame", "editor", "tabsBar", "findBar", "terminal", "output", "database", "git", "content", "tools", "footer", "versionsDisplay", "splitPaneControls", "splitPaneNamesMain", "splitPaneNamesDiff", "charDisplay", "byteDisplay"]; // Create our ID aliases for (let i = 0; i < aliasArray.length; i++) { this[aliasArray[i]] = get(aliasArray[i]); } }, // On load, set the layout init: function() { // Contract the file manager if the user has set to have it hidden if (false === this.lockedNav) { this.filesW = this.minFilesW; } // Set layout this.setLayout(); // State we've over the root dir, enact a selection of it, then state // we're not over it it anymore this.overFileFolder('folder', '|'); this.selectFileFolder('init'); this.overFileFolder('folder', ''); this.filesFrame.contentWindow.focus(); // Hide the loading screen & auto open last files? this.showHide('hide', get('loadingMask')); this.autoOpenInt = setInterval(function(ic) { if (ic.fmReady) { if (ic.openLastFiles) {ic.autoOpenFiles();} clearInterval(ic.autoOpenInt); } }, 4, this); // Start bug checking this.startBugChecking(); // Set the time since last user interaction this.autoLogoutTimer = 0; // Start our interval timer, runs every second this.oneSecondInt = setInterval(function(ic) { ic.autoLogoutTimer++; let unsavedFiles = false; // Check if we have any unsaved files for(let i = 1; i <= ic.savedPoints.length; i++) { if (ic.savedPoints[i - 1] !== ic.getcMInstance(i).changeGeneration()) { unsavedFiles = true; } } // Show an auto-logout warning 60 secs before a logout if(false === unsavedFiles && ic.autoLogoutMins > 1 && ic.autoLogoutTimer == (ic.autoLogoutMins * 60) - 60) { ic.autoLogoutWarningScreen(); } if (get('autoLogoutIFrame') && get('autoLogoutIFrame').contentWindow.document.getElementById('timeRemaning')) { get('autoLogoutIFrame').contentWindow.document.getElementById('timeRemaning').innerHTML = ic.autoLogoutTimer > 0 ? (ic.autoLogoutMins * 60) - ic.autoLogoutTimer : 0; } // If there aren't any unsaved files, we have a timeout period > 0 and the time is up, we can logout if (false === unsavedFiles && ic.autoLogoutMins > 0 && ic.autoLogoutTimer >= ic.autoLogoutMins * 60) { ic.logout('autoLogout'); } // Increase number of seconds ICEcoder has been open for by 1 ic.openSeconds++; // Every 5 mins, ping our file to keep the session alive if (0 === ic.openSeconds % 300) { ic.filesFrame.contentWindow.frames['pingActive'].location.href = ic.iceLoc + "/lib/session-active-ping.php"; } // Every 3 seconds, re-index if we're not already busy if (false === ic.indexing && false === ic.loadingFile && 0 === ic.serverQueueItems.length && 0 === ic.openSeconds % 3) { ic.indexing = true; // Get new data let timestampExtra = ic.indexData ? "?timestamp=" + ic.indexData.timestamps.indexed + "&csrf=" + ic.csrf : ""; fetch(ic.iceLoc + '/lib/indexer.php' + timestampExtra) .then(function(response) { // Convert to JSON return response.json(); }).then(function(data) { if (data.timestamps.changed) { ic.indexData = data; // If we have git diff data if (data.gitDiff) { ic.updateGitDiffPane(); } // If we have git content data if (data.gitContent) { ic.highlightGitDiffs(); } } ic.indexing = false; }); } }, 1000, this); // ICEcoder is ready to start using this.ready = true; }, // ====== // LAYOUT // ====== // Set our layout according to the browser size setLayout: function(setEditor) { let winW, winH, headerH, fileNavH, tabsBarH, findBarH, toolsBarH; // Determine width & height available winW = window.innerWidth; winH = window.innerHeight; // Apply sizes to various elements of the page headerH = 15, fileNavH = 38, tabsBarH = 27, findBarH = 28, toolsBarH = 30; this.header.style.width = this.tabsBar.style.width = this.findBar.style.width = winW + "px"; this.files.style.width = this.editor.style.left = this.filesW + "px"; this.optionsFile.style.width = this.optionsEdit.style.width = this.optionsSettings.style.width = this.optionsHelp.style.width = this.filesW + "px"; this.filesFrame.style.height = (winH - headerH - fileNavH - 7 - toolsBarH) + "px"; this.versionsDisplay.style.left = (this.filesW + 10) + "px"; get("serverMessage").style.left = (this.filesW + 10) + "px"; this.splitPaneControls.style.left = parseInt( ((winW - this.filesW) / 2) + this.filesW - (get("splitPaneControls").getBoundingClientRect().width / 2) , 10) + "px"; this.splitPaneNamesMain.style.left = (parseInt((winW - this.filesW) * 0.25, 10) - 50 + this.filesW) + "px"; this.splitPaneNamesDiff.style.left = (parseInt((winW - this.filesW) * 0.75, 10) - 50 + this.filesW) + "px"; this.setTabWidths(false); // If we need to set the editor sizes if (false !== setEditor) { this.editor.style.width = this.content.style.width = (winW - this.filesW) + "px"; this.terminal.style.width = (winW - this.filesW) + "px"; this.output.style.width = (winW - this.filesW - 31) + "px"; this.database.style.width = (winW - this.filesW) + "px"; this.git.style.width = (winW - this.filesW - 31) + "px"; this.content.style.height = (winH - headerH - tabsBarH - findBarH - toolsBarH) + "px"; this.terminal.style.height = this.output.style.height = this.database.style.height = this.git.style.height = this.terminal.style.top = this.output.style.top = this.database.style.top = this.git.style.top = winH + "px"; if (false !== this.showingTool) { get(this.showingTool).style.top = 0; } // Resize the CodeMirror instances to match the window size setTimeout(function(ic){ for (let i = 0; i < ic.openFiles.length; i++) { // Done the long way here as we need to call them in specific order to stop showing background and so avoiding a flicker effect if (false === ic.splitPane) { ic.content.contentWindow['cM' + ic.cMInstances[i]].setSize(ic.splitPaneLeftPerc + "%", ic.content.style.height); } ic.content.contentWindow['cM' + ic.cMInstances[i] + 'diff'].setSize((100 - ic.splitPaneLeftPerc) + "%", ic.content.style.height); ic.content.contentWindow['cM'+ ic.cMInstances[i] + 'diff'].getWrapperElement().style.left = ic.splitPaneLeftPerc + "%"; if (true === ic.splitPane) { ic.content.contentWindow['cM' + ic.cMInstances[i]].setSize(ic.splitPaneLeftPerc + "%", ic.content.style.height); } } // Place resultsBar on-top scrollbar ic.content.contentWindow.document.getElementById('resultsBar').style.right = false === ic.splitPane ? 0 : parseInt(parseInt(ic.content.style.width, 10) / 2, 10) + "px"; }, 4, this); } }, // Set the layout as split pane or not setSplitPane: function(onOff) { let cM, cMdiff; this.splitPane = "on" === onOff ? true : false; get('splitPaneControlsOff').style.opacity = this.splitPane ? 0.2 : 0.8; get('splitPaneControlsOn').style.opacity = this.splitPane ? 0.8 : 0.2; get('splitPaneNamesMain').style.opacity = get('splitPaneNamesDiff').style.opacity = this.splitPane ? 1 : 0; this.setLayout(); // Also clear marks (if going to a single pane) or redo the marks (if split pane) if (true === this.splitPane) { this.updateDiffs(); // Also set the scroll position to match cM = this.getcMInstance(); this.cMonScroll(cM, 'cM' + this.cMInstances[this.selectedTab - 1]); } else { cM = this.getcMInstance(); cMdiff = this.getcMdiffInstance(); if (cM) { // Clear all main pane marks cMmarks = cM.getAllMarks(); for (let i = 0; i < cMmarks.length; i++) { cMmarks[i].clear(); } // Clear all diff pane marks cMdiffMarks = cMdiff.getAllMarks(); for (let i = 0; i < cMdiffMarks.length; i++) { cMdiffMarks[i].clear(); } } } // Animate in/out the split pane // First, clear any existing split pane interval anim if ("undefined" !== typeof this.animSplitPaneInt) { clearInterval(this.animSplitPaneInt); } // Now set the interval to animate it in/out this.animSplitPaneInt = setInterval(function(ic) { // Animate split pane in if (ic.splitPane && ic.splitPaneLeftPerc > 50.1) { ic.splitPaneLeftPerc = ((ic.splitPaneLeftPerc - 50) / 1.8) + 50; // Animate split pane out } else if (!ic.splitPane && ic.splitPaneLeftPerc < 99.9) { ic.splitPaneLeftPerc = (50 - ((100 - ic.splitPaneLeftPerc) / 1.8)) + 50; // Finish animating split pane in/out } else { ic.splitPaneLeftPerc = ic.splitPane ? 50 : 100; clearInterval(ic.animSplitPaneInt); } ic.setLayout(); }, 4, this); }, // Tool show/hide toggle toolShowHideToggle: function(tool) { let winH; winH = window.innerHeight; if (-1 < ["terminal", "output", "database", "git"].indexOf(tool)) { // Set out of view as a start point get('terminal').style.top = winH + "px"; get('output').style.top = winH + "px"; get('database').style.top = winH + "px"; get('git').style.top = winH + "px"; // Now set tool requested, out of view, or in view get(tool).style.top = tool === this.showingTool ? winH + "px" : 0; // Carry out any extras... if (tool === "terminal") { // Focus on command prompt setTimeout(function(ic){ ic.terminal.contentWindow.document.getElementById('command').focus(); }, 0 ,this); } // Note which tool we're showing this.showingTool = this.showingTool !== tool ? tool : false; // Display and make close icon clickable to close this tool setTimeout(function() { get('closeIcon').style.display = ICEcoder.showingTool !== false ? "inline-block" : "none"; get('closeIcon').onclick = function() { ICEcoder.toolShowHideToggle(ICEcoder.showingTool); } }, this.showingTool !== false ? 200 : 0); } }, // Set the width of the file manager on demand changeFilesW: function(expandContract) { if (false === this.lockedNav || this.filesW === this.minFilesW) { if ("undefined" !== typeof this.changeFilesInt) {clearInterval(this.changeFilesInt)} this.changeFilesInt = setInterval(function(ic) {ic.changeFilesWStep(expandContract)}, 10, this); } }, // Expand/contract the file manager in half-steps changeFilesWStep: function (expandContract) { if ("expand" === expandContract) { this.filesW < this.maxFilesW - 1 ? this.filesW += Math.ceil((this.maxFilesW - this.filesW) / 2) : this.filesW = this.maxFilesW; } else { this.filesW > this.minFilesW + 1 ? this.filesW -= Math.ceil((this.filesW - this.minFilesW) / 2) : this.filesW = this.minFilesW; } if (("expand" === expandContract && this.filesW === this.maxFilesW) || ("contract" === expandContract && this.filesW === this.minFilesW)) { clearInterval(this.changeFilesInt); } // Redo the layout to match this.setLayout(); }, // Can click-drag file manager width? canResizeFilesW: function() { // If we have the cursor set we must be able! if (true === this.ready && "w-resize" === document.body.style.cursor) { // If our mouse is down (and went down on the CM instance's gutter) and we're within a 250px - half of avail width range if (true === this.mouseDown && "gutter" === this.mouseDownInCM) { this.filesW = this.maxFilesW = this.mouseX >= 250 && this.mouseX <= window.innerWidth / 2 ? this.mouseX : this.mouseX < 250 ? 250 : window.innerWidth / 2; // Set various widths based on the new width this.files.style.width = this.filesFrame.style.width = this.filesW + "px"; this.setLayout(); this.draggingFilesW = true; } } else { this.draggingFilesW = false; } }, // Lock & unlock the file manager navigation on demand lockUnlockNav: function() { let lockIconOpen, lockIconClosed; lockIconOpen = this.filesFrame.contentWindow.document.getElementById('fmLockOpen'); lockIconClosed = this.filesFrame.contentWindow.document.getElementById('fmLockClosed'); this.lockedNav = false === this.lockedNav; lockIconOpen.style.display = this.lockedNav ? "none" : "inline-block"; lockIconClosed.style.display = this.lockedNav ? "inline-block" : "none"; }, // Show/hide the plugins on demand showHidePlugins: function(vis) { get('plugins').style.width = "show" === vis ? '55px' : '3px'; get('plugins').style.background = "show" === vis ? '#333' : 'transparent'; if ("show" === vis) { this.changeFilesW('expand'); } }, // ====== // EDITOR // ====== // Set editor stats setEditorStats: function() { this.getCaretPosition(); this.updateCharDisplay(); this.updateByteDisplay(); }, // On focus cMonFocus: function(thisCM, cMinstance) { this.setEditorStats(); this.editorFocusInstance = cMinstance; this.getCaretPosition(); }, // On blur cMonBlur: function(thisCM, cMinstance) { // Nothing as yet }, // On key up cMonKeyUp: function(thisCM, cMinstance, evt) { let key; key = evt.keyCode ?? evt.which ?? evt.charCode; // Return true (to continue) if we're not CTRL+dragging and // we have CTRL/Cmd key down, or if it's the CTRL key, or Cmd key now up if ("CTRL" !== this.draggingWithKey && (this.ctrlCmdKeyDown(evt) || 17 === key || this.isCmdKey(key))) { return true; } this.setEditorStats(); }, // On cursor activity cMonCursorActivity: function(thisCM, cMinstance) { let thisCMPrevLine; this.setEditorStats(); thisCM.removeLineClass(this['cMActiveLine'+cMinstance], "background"); if(thisCM.getCursor('start').line === thisCM.getCursor().line) { this['cMActiveLine' + cMinstance] = thisCM.addLineClass(thisCM.getCursor().line, "background", "cm-s-activeLine"); } if ("CSS" === this.caretLocType) { this.cssColorPreview(); } thisCMPrevLine = this.editorFocusInstance.indexOf('diff') > -1 ? this.prevLineDiff : this.prevLine; if (thisCMPrevLine !== thisCM.getCursor().line && thisCM.getLine(thisCMPrevLine) && thisCM.getLine(thisCMPrevLine).length > 0 && thisCM.getLine(thisCMPrevLine).replace(/\s/g, '').length === 0) { thisCM.replaceRange("", {line: thisCMPrevLine, ch: 0}, {line: thisCMPrevLine, ch: 1000000}, "+input"); } // Set the cursor to text height, not line height and update any old highlighted results message setTimeout(function(ic) { let paneMatch; // Loop through styles to check if we have to adjust cursor height for (let i = 0; i < ic.renderLineStyle.length; i++) { // We have no matching pane to start with paneMatch = false; // Is the pane we need to set the cursor on this pane? if ( ("diff" !== ic.renderLineStyle[i][0] && 1 === cMinstance.indexOf("diff")) || ("diff" === ic.renderLineStyle[i][0] && -1 < cMinstance.indexOf("diff")) ) {paneMatch = true;} // If the pane matches & also the line we're on is the line we have a style set for, set that cursor height if (paneMatch && thisCM.getCursor().line + 1 === ic.renderLineStyle[i][1]) { thisCM.setOption("cursorHeight", thisCM.defaultTextHeight() / thisCM.lineInfo(thisCM.getCursor().line).handle.height); } else { thisCM.setOption("cursorHeight", 1); } } // If we have no selection but still have highlighted result message, run updateResultsDisplay if ("" === thisCM.getSelection() && 0 === get('results').innerHTML.indexOf("Highlighted result")) { ic.updateResultsDisplay('show'); } }, 0, this); }, // On before change cMonBeforeChange: function(thisCM, cMinstance, changeObj, cM) { let sels, tagInfo, tagOpp, thisData; // Get the selections sels = thisCM.listSelections(); // For each of the user selections for (let i = 0; i < sels.length; i++) { // Get the matching tagInfo for current cursor position tagInfo = cM.findMatchingTag(thisCM, sels[i].anchor); // If we're not ending a tag (autocompletion) and we have tagInfo and not undoing/redoing (which handles changes itself) if (0 !== changeObj.text[0].indexOf(">") && "undefined" !== typeof tagInfo && "undo" !== changeObj.origin && "redo" !== changeObj.origin) { // If we also have both open and close tag info if ("undefined" !== typeof tagInfo.open && "undefined" !== typeof tagInfo.close) { // Log the opposite tag info tagOpp = tagInfo.at === "open" ? "close" : "open"; if (null !== tagInfo[tagOpp]) { thisData = tagInfo[tagOpp].tag + ";" + tagInfo[tagOpp].from.line + ":" + tagInfo[tagOpp].from.ch; // Check that string firstly isn't in array and if not, push it in if (-1 === this.oppTagReplaceData.indexOf(thisData)) { this.oppTagReplaceData.push(thisData); } } } } } }, // On change cMonChange: function(thisCM, cMinstance, changeObj, cM) { let sels, rData, theTag, thisLine, thisChar, tagInfo, charDiff, closeDiff, repl1, repl2, filepath, filename, fileExt; // Get the selections sels = thisCM.listSelections(); // If we're not loading the file, it's a change, so update tab if (false === this.loadingFile) { this.redoTabHighlight(this.selectedTab); } // Detect if we have a scrollbar & set layout again setTimeout(function(ic) { ic.scrollBarVisible = thisCM.getScrollInfo().height > thisCM.getScrollInfo().clientHeight; ic.setLayout(); }, 0, this); // If we're replacing opposite tag strings, do that if ("undefined" !== typeof this.oppTagReplaceData[0]) { // For each one of them, grab our data to work with for (let i = 0; i < this.oppTagReplaceData.length; i++) { // Extract data from that string rData = this.oppTagReplaceData[i].split(";"); theTag = rData[0]; thisLine = parseInt(rData[1].split(":")[0], 10); thisChar = parseInt(rData[1].split(":")[1], 10); // Get the tag info for matching tag if (sels[i]) { tagInfo = cM.findMatchingTag(thisCM, sels[i].anchor); } // If we have tagInfo if ("undefined" !== typeof tagInfo) { // Get the opposite tag string theTag = "open" === tagInfo.at ? tagInfo.open.tag : tagInfo.close.tag; // If we have changeObj.from info to work with if ("undefined" !== typeof changeObj.from) { // Same line changing needs a character pos shift charDiff = thisLine === changeObj.from.line ? changeObj.text[0].length - changeObj.removed[0].length : 0; // Also need to adjust if we're in the close tag on same line closeDiff = "close" === tagInfo.at && thisLine === changeObj.from.line ? changeObj.removed[0].length - changeObj.text[0].length + 1 : 1; // Work out the replace from and to positions repl1 = {line: thisLine, ch: thisChar + charDiff+("open" === tagInfo.at ? 2 : closeDiff)}; repl2 = {line: thisLine, ch: thisChar + charDiff+("open" === tagInfo.at ? 2 : closeDiff) + rData[0].length}; } } // Replace our string over the range, if this token string isn't blank and the end tag matches our original tag if ("" !== theTag.trim() && "undefined" !== typeof repl1 && "undefined" !== typeof repl2 && thisCM.getRange(repl1,repl2) === rData[0]) { thisCM.replaceRange(theTag, repl1, repl2, "+input"); // If at the close tag, don't autocomplete if (tagInfo.at === "close") { this.autocompleteSkip = true; } } } } // Reset our array for next time and redo editor stats this.oppTagReplaceData = []; this.setEditorStats(); // Update the list of functions and classes this.updateFunctionClassList(); filepath = this.openFiles[this.selectedTab - 1]; if (filepath) { filename = filepath.substr(filepath.lastIndexOf("/") + 1); fileExt = filename.substr(filename.lastIndexOf(".") + 1); } // Update diffs if we have a split pane if (true === this.splitPane) { // Need 0ms tickover so we handle char change first setTimeout(function(ic){ic.updateDiffs();}, 0, this); } // Highlight Git diff colors in gutter if (this.indexData) { this.highlightGitDiffs(); } // Clear any previous doFindTimeout var if (undefined !== typeof this.doFindTimeout) { clearInterval(this.doFindTimeout); } // If we have something to find in this document, find in 50 ms (unless cancelled by another keypress) if ("" !== get('find').value && t['this document'] === document.findAndReplace.target.value) { this.doFindTimeout = setTimeout(function (ic) { ic.findReplace(get('find').value, false, false, false); }, 50, this); } // Update HTML edited files live if (filepath && this.previewWindow.location && filepath !== "/[NEW]") { this.updatePreviewWindow(thisCM, filepath, filename, fileExt); } // Update the title tag to indicate any changes this.indicateChanges(); }, // On update cMonUpdate: function(thisCM, cMinstance) { // Nothing as yet }, // On scroll cMonScroll: function(thisCM, cMinstance) { let cM, cMdiff, otherCM; if (true === this.splitPane) { // Get both main & diff instance and work out the instance we're not scrolling cM = this.getcMInstance(); cMdiff = this.getcMdiffInstance(); otherCM = cMinstance.indexOf('diff') > -1 ? cM : cMdiff; if (cM) { // Scroll other pane x & y to match this one we're scrolling, after a tickover to avoid judder // 0ms = drag scrollbar, 50 = mouse wheel setTimeout(function(){otherCM.scrollTo(thisCM.getScrollInfo().left, thisCM.getScrollInfo().top);}, true === this.mouseDown ? 0 : 50); } } }, // On input read cMonInputRead: function(thisCM, cMinstance) { if ("kepypress" === this.autoComplete && this.codeAssist) { if (!thisCM.state.completionActive) { if (!this.autocompleteSkip) { this.autocomplete(); } else { this.autocompleteSkip = false; } } } }, // On gutter click cMonGutterClick: function(thisCM, line, gutter, evt, cMinstance) { this.mouseDownInCM = "gutter"; }, // On mouse down cMonMouseDown: function(thisCM, cMinstance, evt) { this.mouseDownInCM = "editor"; }, // On paste cMonPaste: function(thisCM, cMinstance, evt, clipboardData) { // Get text from clipboard, the number of lines (excluding last) // and so the startLine and endLine and auto-indent for that range const text = clipboardData.getData('Text'); const num = text.split("\n").length - 1; const startLine = thisCM.getCursor().line; const endLine = startLine + num; // Now auto-indent after a 0ms tickover setTimeout(() => { parent.ICEcoder.autoIndentLines(startLine, endLine); }, 0); }, // On context menu cMonContextMenu: function(thisCM, cMinstance, evt) { // Set cursor const currCoords = thisCM.coordsChar({left: evt.pageX, top: evt.pageY}); thisCM.setCursor(currCoords); // If CTRL key down if (evt.ctrlKey) { setTimeout(function(ic) { ic.jumpToDefinition(); }, 0, this); } }, // On drag over cMonDragOver: function(thisCM, evt, cMinstance) { this.setDragCursor(evt, 'editor'); }, // On render line cMonRenderLine: function(thisCM, cMinstance, line, element) { let paneMatch; // Loop through styles to use when rendering lines for (let i = 0; i < this.renderLineStyle.length; i++) { // We have no matching pane to start with paneMatch = false; // Is the pane we need to style this pane? if ( ("diff" !== this.renderLineStyle[i][0] && -1 === cMinstance.indexOf("diff")) || ("diff" === this.renderLineStyle[i][0] && -1 < cMinstance.indexOf("diff")) ) {paneMatch = true;} // If the pane matches & also the line we're rendering is the line we have a style set for, set that style if (paneMatch && thisCM.lineInfo(line).line + 1 == this.renderLineStyle[i][1]) { element.style[this.renderLineStyle[i][2]] = this.renderLineStyle[i][3]; } } }, // Show function args tooltip functionArgsTooltip: function(e, area) { let numLintErrors; if (this.indexData && this.indexData.functions) { // If we have no files open, return early if (0 === this.openFiles.length) { get('tooltip').style.display = "none"; return true; } let i; // Get cM instance, and the word under mouse pointer const cM = this.getcMInstance(); const coordsChar = cM.coordsChar({left: this.mouseX - this.maxFilesW, top: this.mouseY - 72}); const word = (cM.getRange(cM.findWordAt(coordsChar).anchor, cM.findWordAt(coordsChar).head)); // If it's not a word, return early if ("" === word) { get('tooltip').style.display = "none"; return true; } // Get result and number of results for word in functions from index JSON object list let result = null; let numResults = 0; const filePath = this.openFiles[this.selectedTab - 1]; const filePathExt = filePath.substr(filePath.lastIndexOf(".") + 1); for(i in this.indexData.functions[filePathExt]) { if (i === word) { result = this.indexData.functions[filePathExt][i]; numResults++; } } // If we have a single result and the mouse pointer is not over the definition of it (that would be pointless), show tooltip if (1 === numResults && -1 === [null, "def"].indexOf(cM.getTokenTypeAt(coordsChar))) { get('tooltip').style.display = "block"; get('tooltip').style.left = (this.mouseX + 10) + "px"; numLintErrors = this.content.contentWindow.document.getElementsByClassName("CodeMirror-lint-tooltip")[0]; numLintErrors = numLintErrors && numLintErrors.childNodes ? numLintErrors.childNodes.length : 0; get('tooltip').style.top = (this.mouseY - 30 - (0 < numLintErrors ? 18 * numLintErrors : 0) ) + "px"; get('tooltip').style.zIndex = 1000; // Limit function args list to 200 char max if (result.params.length > 200) { result.params = result.params.substr(0, 200) + "...)"; } get('tooltip').innerHTML = result.params; // Else hide it } else { get('tooltip').style.display = "none"; } } }, // Update diffs shown to the user in each pane updateDiffs: function() { let cM, cMdiff, mainText, diffText, sm, opcodes, cMmarks, cMdiffMarks, amt, sDiffs; // Reset the style array container and main vs diff pane shift difference this.renderLineStyle = []; this.renderPaneShiftAmount = 0; cM = this.getcMInstance(); cMdiff = this.getcMdiffInstance(); // Get the baseText and newText values from the two textboxes, and split them into lines mainText = cM ? difflib.stringAsLines(cM.getValue()) : ""; diffText = cMdiff ? difflib.stringAsLines(cMdiff.getValue()) : ""; // Create a SequenceMatcher instance that diffs the two sets of lines sm = new difflib.SequenceMatcher(mainText, diffText); // Get the opcodes from the SequenceMatcher instance // Opcodes is a list of 3-tuples describing what changes should be made to the base text in order to yield the new text opcodes = sm.get_opcodes(); if (cM) { // Clear all main pane marks cMmarks = cM.getAllMarks(); for (let i = 0; i < cMmarks.length; i++) { cMmarks[i].clear(); } // Clear all diff pane marks cMdiffMarks = cMdiff.getAllMarks(); for (let i = 0; i < cMdiffMarks.length; i++) { cMdiffMarks[i].clear(); } } if (cM && "" !== cMdiff.getValue()) { // For each opcode returned by jsdifflib for (let i = 0; i < opcodes.length; i++) { // If not 'equal' status for the section, we have a 'replace', 'delete' or 'insert' status, so do something if ("equal" !== opcodes[i][0]) { // ========= // MAIN PANE // ========= // Replacing? Pad out main pane line to match equivalent last line in diff pane if ("replace" === opcodes[i][0]) { // Line amount is diff between end of both panes at this point in our loop, plus 1 line and our overall document shift, multiplied by font size amt = ((opcodes[i][4] - opcodes[i][2] + 1 + this.renderPaneShiftAmount) * cM.defaultTextHeight()); // Add on the extra heights for any wrapped lines for (let j = opcodes[i][4] - 1; j <= opcodes[i][2] - 1; j++) { if (cMdiff.getLineHandle(j).height > cM.defaultTextHeight()) { amt += cMdiff.getLineHandle(j).height - cM.defaultTextHeight(); } } // If we have an height greater than the default text height, add a new style if (amt > cM.defaultTextHeight()) { this.renderLineStyle.push(["main", opcodes[i][2], "height", amt + "px"]); } // Mark text in 2 colours, for each line for (let j = 0; j<(opcodes[i][2]) - (opcodes[i][1]); j++) { sDiffs = (this.findStringDiffs(cM.getLine(opcodes[i][1] + j),cMdiff.getLine(opcodes[i][3] + j))); cM.markText({line: opcodes[i][1]+j, ch: 0}, {line: opcodes[i][3] + j + this.renderPaneShiftAmount, ch: sDiffs[0]}, {className: "diffGreyLighter"}); cM.markText({line: opcodes[i][1]+j, ch: sDiffs[0]}, {line: opcodes[i][3] + j + this.renderPaneShiftAmount, ch: sDiffs[0] + sDiffs[1]}, {className: "diffGrey"}); cM.markText({line: opcodes[i][1]+j, ch: sDiffs[0] + sDiffs[1]}, {line: opcodes[i][3] + j + this.renderPaneShiftAmount, ch: 1000000}, {className: "diffGreyLighter"}); } // Inserting } else { cM.markText({line: opcodes[i][1], ch: 0}, {line: opcodes[i][2] - 1, ch: 1000000}, {className: "diffGreen"}); } // If inserting or deleting and the main pane hasn't changed, we need to pad out the line in that pane if ("replace" !== opcodes[i][0] && opcodes[i][1] === opcodes[i][2]) { this.renderLineStyle.push(["main", opcodes[i][2], "height", ((opcodes[i][4] - opcodes[i][3] + 1) * cM.defaultTextHeight()) + "px"]); // Mark the range with empty class cM.markText({line: opcodes[i][2] - 1, ch: 0}, {line: opcodes[i][2]-1, ch: 1000000}, {className: "diffNone"}); } // ========= // DIFF PANE // ========= // Replacing? Pad out diff pane line to match equivalent last line in main pane if ("replace" === opcodes[i][0]) { // Line amount is diff between end of both panes at this point in our loop, plus 1 line and our overall document shift, multiplied by font size amt = ((opcodes[i][2] - opcodes[i][4] + 1 - this.renderPaneShiftAmount) * cM.defaultTextHeight()); // Add on the extra heights for any wrapped lines for (let j = opcodes[i][4] - 1; j <= opcodes[i][2] - 1; j++) { if (cM.getLineHandle(j).height > cM.defaultTextHeight()) { amt += cM.getLineHandle(j).height - cM.defaultTextHeight(); } } // If we have an height greater than the default text height, add a new style if (amt > cM.defaultTextHeight()) { this.renderLineStyle.push(["diff", opcodes[i][4], "height", amt + "px"]); } // Mark text in 2 colours, for each line for (let j = 0; j<(opcodes[i][4]) - (opcodes[i][3]); j++) { sDiffs = (this.findStringDiffs(cM.getLine(opcodes[i][1] + j),cMdiff.getLine(opcodes[i][3] + j))); cMdiff.markText({line: opcodes[i][1] + j - this.renderPaneShiftAmount, ch: 0}, {line: opcodes[i][3] + j, ch: sDiffs[0]}, {className: "diffGreyLighter"}); cMdiff.markText({line: opcodes[i][1] + j - this.renderPaneShiftAmount, ch: sDiffs[0]}, {line: opcodes[i][3] + j, ch: sDiffs[0] + sDiffs[2]}, {className: "diffGrey"}); cMdiff.markText({line: opcodes[i][1] + j - this.renderPaneShiftAmount, ch: sDiffs[0] + sDiffs[2]}, {line: opcodes[i][3] + j, ch: 1000000}, {className: "diffGreyLighter"}); } // Deleting } else { cMdiff.markText({line: opcodes[i][3], ch: 0}, {line: opcodes[i][4] - 1, ch: 1000000}, {className: "diffRed"}); } // If inserting or deleting and the diff pane hasn't changed, we need to pad out the line in that pane if ("replace" !== opcodes[i][0] && opcodes[i][3] === opcodes[i][4]) { this.renderLineStyle.push(["diff", opcodes[i][4], "height", ((opcodes[i][2] - opcodes[i][1] + 1) * cM.defaultTextHeight()) + "px"]); // Mark the range with empty class cMdiff.markText({line: opcodes[i][4] - 1, ch: 0}, {line: opcodes[i][4] - 1, ch: 1000000}, {className: "diffNone"}); } // Finally, set the last amount shifted for this change this.renderPaneShiftAmount = (opcodes[i][2] - opcodes[i][4]); } } } }, // Find diffs between 2 strings findStringDiffs: function(a, b) { if ("undefined" == typeof a) {a = ""} if ("undefined" == typeof b) {b = ""} for (var c = 0, // start from the first character d = a.length, e = b.length; // and from the last characters of both strings a[c] && // if not at the end of the string and a[c] === b[c]; // if both strings are equal at this position c++); // go forward for (; d > c & e > c & // stop at the position found by the first loop a[d - 1] === b[e - 1]; // if both strings are equal at this position d--) e--; // go backward return[c, d - c, e - c] // return position and lengths of the two substrings found }, // Highlight git diffs (between what is in browser and in Git commits) highlightGitDiffs: function() { // Clear the timeout if we have one already if ("undefined" !== typeof highlightGitDiffTimeout) { clearTimeout(highlightGitDiffTimeout); } // If we have index data & Git data, after a timeout, if we have a matching path in that Git data if (this.indexData && this.indexData.gitContent) { highlightGitDiffTimeout = setTimeout(function(ic) { if (ic.indexData.gitContent[docRoot + ic.openFiles[ic.selectedTab - 1]]) { // Get the CodeMirror instance and clear the gutter for it cM = ic.getcMInstance(); cM.clearGutter("CodeMirror-linenumbers"); // Get the baseText and gitText values from the two sources, and split them into lines const mainText = cM ? difflib.stringAsLines(cM.getValue()) : ""; const gitText = difflib.stringAsLines(ic.indexData.gitContent[docRoot + ic.openFiles[ic.selectedTab - 1]].lastHashContent ?? ""); // Create a SequenceMatcher instance that diffs the two sets of lines const sm = new difflib.SequenceMatcher(gitText, mainText); // Get the opcodes from the SequenceMatcher instance // Opcodes is a list of 3-tuples describing what changes should be made to the base text in order to yield the new text const opcodes = sm.get_opcodes(); // For each opcode returned by jsdifflib for (let i = 0; i < opcodes.length; i++) { // If not 'equal' status for the section, we have a 'replace', 'delete' or 'insert' status, so do something if ("equal" !== opcodes[i][0]) { // Replacing if ("replace" === opcodes[i][0]) { // Mark text in one of 2 colours, for each line for (let j = opcodes[i][3]; j < opcodes[i][4]; j++) { let elem = document.createElement("DIV"); elem.className = "CodeMirror-linenumber"; // Only trim whitespace is different, grey if (gitText[j - (opcodes[i][4] - opcodes[i][2])] && mainText[j].trim() === gitText[j - (opcodes[i][4] - opcodes[i][2])].trim()) { elem.style.background = "#888"; // Something other than whitespace is different, orange } else { elem.style.background = "#f80"; } elem.style.color = "#111"; elem.innerHTML = j + 1; cM.setGutterMarker(j, "CodeMirror-linenumbers", elem); } // Inserting } else if ("insert" === opcodes[i][0]) { // Mark text in green for each line for (let j = opcodes[i][3]; j < opcodes[i][4]; j++) { let elem = document.createElement("DIV"); elem.className = "CodeMirror-linenumber"; elem.style.background = "#080"; elem.style.color = "#fff"; elem.innerHTML = j + 1; cM.setGutterMarker(j, "CodeMirror-linenumbers", elem); } // Deleting } else { // Add a red line to indicate where lines where deleted let elem = document.createElement("DIV"); elem.className = "CodeMirror-linenumber"; // If we haven't deleted content at end, line is above numbers if (cM.lineCount() > opcodes[i][3]) { elem.style.borderTop = "solid #b00 1px"; elem.innerHTML = opcodes[i][3] + 1; cM.setGutterMarker(opcodes[i][3], "CodeMirror-linenumbers", elem); // Otherwise, line is below last number } else { elem.style.borderBottom = "solid #b00 1px"; elem.innerHTML = opcodes[i][3]; cM.setGutterMarker(opcodes[i][3] - 1, "CodeMirror-linenumbers", elem); } } } } } }, this.loadingFile ? 100 : 0, this); } }, // Update Git diff pane (the diffs between saved content and git commits) updateGitDiffPane: function() { let gitDiffList = ""; get("toolLinkGit").className = 0 < this.indexData.gitDiff.paths.length ? "highlight info" : ""; for (let i = 0; i < this.indexData.gitDiff.paths.length; i++) { gitDiffList += '