diff --git a/static/motd.interfaces.ts b/static/motd.interfaces.ts index c8d9e18f3..ab84df778 100644 --- a/static/motd.interfaces.ts +++ b/static/motd.interfaces.ts @@ -24,7 +24,7 @@ import {editor} from 'monaco-editor/'; -type Decoration = { +export type Decoration = { decoration: editor.IModelDecorationOptions; filter: string[]; name: string; diff --git a/static/multifile-service.ts b/static/multifile-service.ts index 8c5fc75c3..5b6261b70 100644 --- a/static/multifile-service.ts +++ b/static/multifile-service.ts @@ -237,7 +237,7 @@ export class MultifileService { if (file.isOpen) { const editor = this.hub.getEditorById(file.editorId); - return editor.getSource(); + return editor?.getSource() ?? ''; } else { return file.content; } diff --git a/static/panes/editor.interfaces.ts b/static/panes/editor.interfaces.ts new file mode 100644 index 000000000..946e1f0af --- /dev/null +++ b/static/panes/editor.interfaces.ts @@ -0,0 +1,32 @@ +// Copyright (c) 2022, Compiler Explorer Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +export type EditorState = { + filename?: string; + options?: { + readOnly?: boolean; + }; + source?: string; + lang?: string; +}; diff --git a/static/panes/editor.js b/static/panes/editor.js deleted file mode 100644 index ec762931d..000000000 --- a/static/panes/editor.js +++ /dev/null @@ -1,1834 +0,0 @@ -// Copyright (c) 2016, Compiler Explorer Authors -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. - -'use strict'; -var _ = require('underscore'); -var $ = require('jquery'); -var colour = require('../colour'); -var loadSaveLib = require('../widgets/load-save'); -var FontScale = require('../widgets/fontscale').FontScale; -var Components = require('../components'); -var monaco = require('monaco-editor'); -var options = require('../options').options; -var Alert = require('../alert').Alert; -var ga = require('../analytics').ga; -var monacoVim = require('monaco-vim'); -var monacoConfig = require('../monaco-config'); -var quickFixesHandler = require('../quick-fixes-handler'); -var TomSelect = require('tom-select'); -var Settings = require('../settings').Settings; -var utils = require('../utils'); -require('../formatter-registry'); -require('../modes/_all'); - -var loadSave = new loadSaveLib.LoadSave(); -var languages = options.languages; - -// eslint-disable-next-line max-statements -function Editor(hub, state, container) { - this.id = state.id || hub.nextEditorId(); - this.container = container; - this.domRoot = container.getElement(); - this.domRoot.html($('#codeEditor').html()); - this.hub = hub; - this.eventHub = hub.createEventHub(); - // Should probably be its own function somewhere - this.settings = Settings.getStoredSettings(); - this.ourCompilers = {}; - this.ourExecutors = {}; - this.httpRoot = window.httpRoot; - this.asmByCompiler = {}; - this.defaultFileByCompiler = {}; - this.busyCompilers = {}; - this.colours = []; - this.treeCompilers = {}; - - this.decorations = {}; - this.prevDecorations = []; - this.extraDecorations = []; - - this.fadeTimeoutId = -1; - - this.editorSourceByLang = {}; - this.alertSystem = new Alert(); - this.alertSystem.prefixMessage = 'Editor #' + this.id; - - this.filename = state.filename || false; - - this.awaitingInitialResults = false; - this.selection = state.selection; - - this.revealJumpStack = []; - - this.langKeys = _.keys(languages); - this.initLanguage(state); - - var root = this.domRoot.find('.monaco-placeholder'); - var legacyReadOnly = state.options && !!state.options.readOnly; - this.editor = monaco.editor.create( - root[0], - monacoConfig.extendConfig( - { - language: this.currentLanguage.monaco, - readOnly: - !!options.readOnly || - legacyReadOnly || - (window.compilerExplorerOptions && window.compilerExplorerOptions.mobileViewer), - glyphMargin: !options.embedded, - }, - this.settings - ) - ); - this.editor.getModel().setEOL(monaco.editor.EndOfLineSequence.LF); - - if (state.source !== undefined) { - this.setSource(state.source); - } else { - this.updateEditorCode(); - } - - var startFolded = /^[/*#;]+\s*setup.*/; - if (state.source && state.source.match(startFolded)) { - // With reference to https://github.com/Microsoft/monaco-editor/issues/115 - // I tried that and it didn't work, but a delay of 500 seems to "be enough". - // FIXME: Currently not working - No folding is performed - setTimeout( - _.bind(function () { - this.editor.setSelection(new monaco.Selection(1, 1, 1, 1)); - this.editor.focus(); - this.editor.getAction('editor.fold').run(); - //this.editor.clearSelection(); - }, this), - 500 - ); - } - - this.initEditorActions(); - this.initButtons(state); - this.initCallbacks(); - - if (this.settings.useVim) { - this.enableVim(); - } - - var usableLanguages = _.filter(languages, function (language) { - return hub.compilerService.compilersByLang[language.id]; - }); - - this.selectize = new TomSelect(this.languageBtn, { - sortField: 'name', - valueField: 'id', - labelField: 'name', - searchField: ['name'], - placeholder: '🔍 Select a language...', - options: _.map(usableLanguages, _.identity), - items: [this.currentLanguage.id], - dropdownParent: 'body', - plugins: ['dropdown_input'], - onChange: _.bind(this.onLanguageChange, this), - closeAfterSelect: true, - render: { - option: renderSelectizeOption, - item: renderSelectizeItem, - }, - }); - - // We suppress posting changes until the user has stopped typing by: - // * Using _.debounce() to run emitChange on any key event or change - // only after a delay. - // * Only actually triggering a change if the document text has changed from - // the previous emitted. - this.lastChangeEmitted = null; - this.onSettingsChange(this.settings); - // this.editor.on("keydown", _.bind(function () { - // // Not strictly a change; but this suppresses changes until some time - // // after the last key down (be it an actual change or a just a cursor - // // movement etc). - // this.debouncedEmitChange(); - // }, this)); - - this.updateTitle(); - this.updateState(); - ga.proxy('send', { - hitType: 'event', - eventCategory: 'OpenViewPane', - eventAction: 'Editor', - }); - ga.proxy('send', { - hitType: 'event', - eventCategory: 'LanguageChange', - eventAction: this.currentLanguage.id, - }); -} - -Editor.prototype.onMotd = function (motd) { - this.extraDecorations = motd.decorations; - this.updateExtraDecorations(); -}; - -Editor.prototype.updateExtraDecorations = function () { - var decorationsDirty = false; - _.each( - this.extraDecorations, - _.bind(function (decoration) { - if (decoration.filter && decoration.filter.indexOf(this.currentLanguage.name.toLowerCase()) < 0) return; - var match = this.editor.getModel().findNextMatch( - decoration.regex, - { - column: 1, - lineNumber: 1, - }, - true, - true, - null, - false - ); - if (match !== this.decorations[decoration.name]) { - decorationsDirty = true; - this.decorations[decoration.name] = match - ? [{range: match.range, options: decoration.decoration}] - : null; - } - }, this) - ); - if (decorationsDirty) this.updateDecorations(); -}; - -// If compilerId is undefined, every compiler will be pinged -Editor.prototype.maybeEmitChange = function (force, compilerId) { - var source = this.getSource(); - if (!force && source === this.lastChangeEmitted) return; - - this.updateExtraDecorations(); - - this.lastChangeEmitted = source; - this.eventHub.emit('editorChange', this.id, this.lastChangeEmitted, this.currentLanguage.id, compilerId); -}; - -Editor.prototype.updateState = function () { - var state = { - id: this.id, - source: this.getSource(), - lang: this.currentLanguage.id, - selection: this.selection, - filename: this.filename, - }; - this.fontScale.addState(state); - this.container.setState(state); - - this.updateButtons(); -}; - -Editor.prototype.setSource = function (newSource) { - this.updateSource(newSource); - - if (window.compilerExplorerOptions.mobileViewer) { - $(this.domRoot.find('.monaco-placeholder textarea')).hide(); - } -}; - -Editor.prototype.onNewSource = function (editorId, newSource) { - if (this.id === editorId) { - this.setSource(newSource); - } -}; - -Editor.prototype.getSource = function () { - return this.editor.getModel().getValue(); -}; - -Editor.prototype.initLanguage = function (state) { - this.currentLanguage = languages[this.langKeys[0]]; - this.waitingForLanguage = state.source && !state.lang; - if (languages[this.settings.defaultLanguage]) { - this.currentLanguage = languages[this.settings.defaultLanguage]; - } - if (languages[state.lang]) { - this.currentLanguage = languages[state.lang]; - } else if (this.settings.newEditorLastLang && languages[this.hub.lastOpenedLangId]) { - this.currentLanguage = languages[this.hub.lastOpenedLangId]; - } -}; - -Editor.prototype.initCallbacks = function () { - this.fontScale.on('change', _.bind(this.updateState, this)); - this.eventHub.on( - 'broadcastFontScale', - _.bind(function (scale) { - this.fontScale.setScale(scale); - this.updateState(); - }, this) - ); - - this.container.on('resize', this.resize, this); - this.container.on('shown', this.resize, this); - this.container.on( - 'open', - _.bind(function () { - this.eventHub.emit('editorOpen', this.id, this); - }, this) - ); - this.container.on('destroy', this.close, this); - this.container.layoutManager.on( - 'initialised', - function () { - // Once initialized, let everyone know what text we have. - this.maybeEmitChange(); - // And maybe ask for a compilation (Will hit the cache most of the time) - this.requestCompilation(); - }, - this - ); - - this.eventHub.on('treeCompilerEditorIncludeChange', this.onTreeCompilerEditorIncludeChange, this); - this.eventHub.on('treeCompilerEditorExcludeChange', this.onTreeCompilerEditorExcludeChange, this); - this.eventHub.on('coloursForEditor', this.onColoursForEditor, this); - this.eventHub.on('compilerOpen', this.onCompilerOpen, this); - this.eventHub.on('executorOpen', this.onExecutorOpen, this); - this.eventHub.on('executorClose', this.onExecutorClose, this); - this.eventHub.on('compilerClose', this.onCompilerClose, this); - this.eventHub.on('compiling', this.onCompiling, this); - this.eventHub.on('compileResult', this.onCompileResponse, this); - this.eventHub.on('executeResult', this.onExecuteResponse, this); - this.eventHub.on('selectLine', this.onSelectLine, this); - this.eventHub.on('editorSetDecoration', this.onEditorSetDecoration, this); - this.eventHub.on('editorDisplayFlow', this.onEditorDisplayFlow, this); - this.eventHub.on('editorLinkLine', this.onEditorLinkLine, this); - this.eventHub.on('settingsChange', this.onSettingsChange, this); - this.eventHub.on('conformanceViewOpen', this.onConformanceViewOpen, this); - this.eventHub.on('conformanceViewClose', this.onConformanceViewClose, this); - this.eventHub.on('resize', this.resize, this); - this.eventHub.on('newSource', this.onNewSource, this); - this.eventHub.on('motd', this.onMotd, this); - this.eventHub.on('findEditors', this.sendEditor, this); - this.eventHub.emit('requestMotd'); - - this.editor.getModel().onDidChangeContent( - _.bind(function () { - this.debouncedEmitChange(); - this.updateState(); - }, this) - ); - - this.mouseMoveThrottledFunction = _.throttle(_.bind(this.onMouseMove, this), 50); - - this.editor.onMouseMove( - _.bind(function (e) { - this.mouseMoveThrottledFunction(e); - }, this) - ); - - if (window.compilerExplorerOptions.mobileViewer) { - // workaround for issue with contextmenu not going away when tapping somewhere else on the screen - this.editor.onDidChangeCursorSelection( - _.bind(function () { - var contextmenu = $('div.context-view.monaco-menu-container'); - if (contextmenu.css('display') !== 'none') { - contextmenu.hide(); - } - }, this) - ); - } - - this.cursorSelectionThrottledFunction = _.throttle(_.bind(this.onDidChangeCursorSelection, this), 500); - this.editor.onDidChangeCursorSelection( - _.bind(function (e) { - this.cursorSelectionThrottledFunction(e); - }, this) - ); - - this.editor.onDidFocusEditorText(_.bind(this.onDidFocusEditorText, this)); - this.editor.onDidBlurEditorText(_.bind(this.onDidBlurEditorText, this)); - this.editor.onDidChangeCursorPosition(_.bind(this.onDidChangeCursorPosition, this)); - - this.eventHub.on('initialised', this.maybeEmitChange, this); - - $(document).on( - 'keyup.editable', - _.bind(function (e) { - if (e.target === this.domRoot.find('.monaco-placeholder .inputarea')[0]) { - if (e.which === 27) { - this.onEscapeKey(e); - } else if (e.which === 45) { - this.onInsertKey(e); - } - } - }, this) - ); -}; - -Editor.prototype.sendEditor = function () { - this.eventHub.emit('editorOpen', this.id, this); -}; - -Editor.prototype.onMouseMove = function (e) { - if (e !== null && e.target !== null && this.settings.hoverShowSource && e.target.position !== null) { - this.clearLinkedLine(); - var pos = e.target.position; - this.tryPanesLinkLine(pos.lineNumber, pos.column, false); - } -}; - -Editor.prototype.onDidChangeCursorSelection = function (e) { - if (this.awaitingInitialResults) { - this.selection = e.selection; - this.updateState(); - } -}; - -Editor.prototype.onDidChangeCursorPosition = function (e) { - if (e.position) { - this.currentCursorPosition.text('(' + e.position.lineNumber + ', ' + e.position.column + ')'); - } -}; - -Editor.prototype.onDidFocusEditorText = function () { - var position = this.editor.getPosition(); - if (position) { - this.currentCursorPosition.text('(' + position.lineNumber + ', ' + position.column + ')'); - } - this.currentCursorPosition.show(); -}; - -Editor.prototype.onDidBlurEditorText = function () { - this.currentCursorPosition.text(''); - this.currentCursorPosition.hide(); -}; - -Editor.prototype.onEscapeKey = function () { - if (this.editor.vimInUse) { - var currentState = monacoVim.VimMode.Vim.maybeInitVimState_(this.vimMode); - if (currentState.insertMode) { - monacoVim.VimMode.Vim.exitInsertMode(this.vimMode); - } else if (currentState.visualMode) { - monacoVim.VimMode.Vim.exitVisualMode(this.vimMode, false); - } - } -}; - -Editor.prototype.onInsertKey = function (event) { - if (this.editor.vimInUse) { - var currentState = monacoVim.VimMode.Vim.maybeInitVimState_(this.vimMode); - if (!currentState.insertMode) { - var insertEvent = { - preventDefault: event.preventDefault, - stopPropagation: event.stopPropagation, - browserEvent: { - key: 'i', - defaultPrevented: false, - }, - keyCode: 39, - }; - this.vimMode.handleKeyDown(insertEvent); - } - } -}; - -Editor.prototype.enableVim = function () { - this.vimMode = monacoVim.initVimMode(this.editor, this.domRoot.find('#v-status')[0]); - this.vimFlag.prop('class', 'btn btn-info'); - this.editor.vimInUse = true; -}; - -Editor.prototype.disableVim = function () { - this.vimMode.dispose(); - this.domRoot.find('#v-status').html(''); - this.vimFlag.prop('class', 'btn btn-light'); - this.editor.vimInUse = false; -}; - -Editor.prototype.initButtons = function (state) { - this.fontScale = new FontScale(this.domRoot, state, this.editor); - this.languageBtn = this.domRoot.find('.change-language'); - // Ensure that the button is disabled if we don't have anything to select - // Note that is might be disabled for other reasons beforehand - if (this.langKeys.length <= 1) { - this.languageBtn.prop('disabled', true); - } - this.topBar = this.domRoot.find('.top-bar'); - this.hideable = this.domRoot.find('.hideable'); - - this.loadSaveButton = this.domRoot.find('.load-save'); - var paneAdderDropdown = this.domRoot.find('.add-pane'); - var addCompilerButton = this.domRoot.find('.btn.add-compiler'); - this.addExecutorButton = this.domRoot.find('.btn.add-executor'); - this.conformanceViewerButton = this.domRoot.find('.btn.conformance'); - var addEditorButton = this.domRoot.find('.btn.add-editor'); - var toggleVimButton = this.domRoot.find('#vim-flag'); - this.vimFlag = this.domRoot.find('#vim-flag'); - toggleVimButton.on( - 'click', - _.bind(function () { - if (this.editor.vimInUse) { - this.disableVim(); - } else { - this.enableVim(); - } - }, this) - ); - - // NB a new compilerConfig needs to be created every time; else the state is shared - // between all compilers created this way. That leads to some nasty-to-find state - // bugs e.g. https://github.com/compiler-explorer/compiler-explorer/issues/225 - var getCompilerConfig = _.bind(function () { - return Components.getCompiler(this.id, this.currentLanguage.id); - }, this); - - var getExecutorConfig = _.bind(function () { - return Components.getExecutor(this.id, this.currentLanguage.id); - }, this); - - var getConformanceConfig = _.bind(function () { - // TODO: this doesn't pass any treeid introduced by #3360 - return Components.getConformanceView(this.id, undefined, this.getSource(), this.currentLanguage.id); - }, this); - - var getEditorConfig = _.bind(function () { - return Components.getEditor(); - }, this); - - var addPaneOpener = _.bind(function (dragSource, dragConfig) { - this.container.layoutManager - .createDragSource(dragSource, dragConfig) - ._dragListener.on('dragStart', function () { - paneAdderDropdown.dropdown('toggle'); - }); - - dragSource.on( - 'click', - _.bind(function () { - var insertPoint = - this.hub.findParentRowOrColumn(this.container) || this.container.layoutManager.root.contentItems[0]; - insertPoint.addChild(dragConfig); - }, this) - ); - }, this); - - addPaneOpener(addCompilerButton, getCompilerConfig); - addPaneOpener(this.addExecutorButton, getExecutorConfig); - addPaneOpener(this.conformanceViewerButton, getConformanceConfig); - addPaneOpener(addEditorButton, getEditorConfig); - - this.initLoadSaver(); - $(this.domRoot).on( - 'keydown', - _.bind(function (event) { - if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') { - this.handleCtrlS(event); - } - }, this) - ); - - if (options.thirdPartyIntegrationEnabled) { - this.cppInsightsButton = this.domRoot.find('.open-in-cppinsights'); - this.cppInsightsButton.on( - 'mousedown', - _.bind(function () { - this.updateOpenInCppInsights(); - }, this) - ); - - this.quickBenchButton = this.domRoot.find('.open-in-quickbench'); - this.quickBenchButton.on( - 'mousedown', - _.bind(function () { - this.updateOpenInQuickBench(); - }, this) - ); - } - - this.currentCursorPosition = this.domRoot.find('.currentCursorPosition'); - this.currentCursorPosition.hide(); -}; - -Editor.prototype.handleCtrlS = function (event) { - event.preventDefault(); - if (this.settings.enableCtrlStree && this.hub.hasTree()) { - var trees = this.hub.trees; - // todo: change when multiple trees are used - if (trees && trees.length > 0) { - trees[0].multifileService.includeByEditorId(this.id).then( - _.bind(function () { - trees[0].refresh(); - }, this) - ); - } - } else { - if (this.settings.enableCtrlS === 'true') { - loadSave.setMinimalOptions(this.getSource(), this.currentLanguage); - if (!loadSave.onSaveToFile(this.id)) { - this.showLoadSaver(); - } - } else if (this.settings.enableCtrlS === 'false') { - this.emitShortLinkEvent(); - } else if (this.settings.enableCtrlS === '2') { - this.runFormatDocumentAction(); - } else if (this.settings.enableCtrlS === '3') { - this.handleCtrlSDoNothing(); - } - } -}; - -Editor.prototype.handleCtrlSDoNothing = function () { - if (this.nothingCtrlSTimes === undefined) { - this.nothingCtrlSTimes = 0; - this.nothingCtrlSSince = Date.now(); - } else { - if (Date.now() - this.nothingCtrlSSince > 5000) { - this.nothingCtrlSTimes = undefined; - } else if (this.nothingCtrlSTimes === 4) { - var element = this.domRoot.find('.ctrlSNothing'); - element.show(100); - setTimeout(function () { - element.hide(); - }, 2000); - this.nothingCtrlSTimes = undefined; - } else { - this.nothingCtrlSTimes++; - } - } -}; - -Editor.prototype.updateButtons = function () { - if (options.thirdPartyIntegrationEnabled) { - if (this.currentLanguage.id === 'c++') { - this.cppInsightsButton.show(); - this.quickBenchButton.show(); - } else { - this.cppInsightsButton.hide(); - this.quickBenchButton.hide(); - } - } - - this.addExecutorButton.prop('disabled', !this.currentLanguage.supportsExecute); -}; - -Editor.prototype.b64UTFEncode = function (str) { - return btoa( - encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, v) { - return String.fromCharCode(parseInt(v, 16)); - }) - ); -}; - -Editor.prototype.asciiEncodeJsonText = function (json) { - return json.replace(/[\u007F-\uFFFF]/g, function (chr) { - // json unicode escapes must always be 4 characters long, so pad with leading zeros - return '\\u' + ('0000' + chr.charCodeAt(0).toString(16)).substr(-4); - }); -}; - -Editor.prototype.getCompilerStates = function () { - var states = []; - - _.each( - this.ourCompilers, - _.bind(function (val, compilerIdStr) { - var compilerId = parseInt(compilerIdStr); - - var glCompiler = _.find(this.container.layoutManager.root.getComponentsByName('compiler'), function (c) { - return c.id === compilerId; - }); - - if (glCompiler) { - var state = glCompiler.currentState(); - states.push(state); - } - }, this) - ); - - return states; -}; - -Editor.prototype.updateOpenInCppInsights = function () { - if (options.thirdPartyIntegrationEnabled) { - var cppStd = 'cpp2a'; - - var compilers = this.getCompilerStates(); - _.each( - compilers, - _.bind(function (compiler) { - if (compiler.options.indexOf('-std=c++11') !== -1 || compiler.options.indexOf('-std=gnu++11') !== -1) { - cppStd = 'cpp11'; - } else if ( - compiler.options.indexOf('-std=c++14') !== -1 || - compiler.options.indexOf('-std=gnu++14') !== -1 - ) { - cppStd = 'cpp14'; - } else if ( - compiler.options.indexOf('-std=c++17') !== -1 || - compiler.options.indexOf('-std=gnu++17') !== -1 - ) { - cppStd = 'cpp17'; - } else if ( - compiler.options.indexOf('-std=c++2a') !== -1 || - compiler.options.indexOf('-std=gnu++2a') !== -1 - ) { - cppStd = 'cpp2a'; - } else if (compiler.options.indexOf('-std=c++98') !== -1) { - cppStd = 'cpp98'; - } - }, this) - ); - - var maxURL = 8177; // apache's default maximum url length - var maxCode = maxURL - ('/lnk?code=&std=' + cppStd + '&rev=1.0').length; - var codeData = this.b64UTFEncode(this.getSource()); - if (codeData.length > maxCode) { - codeData = this.b64UTFEncode('/** Source too long to fit in a URL */\n'); - } - - var link = 'https://cppinsights.io/lnk?code=' + codeData + '&std=' + cppStd + '&rev=1.0'; - - this.cppInsightsButton.attr('href', link); - } -}; - -Editor.prototype.cleanupSemVer = function (semver) { - if (semver) { - var semverStr = semver.toString(); - if (semverStr !== '' && semverStr.indexOf('(') === -1) { - var vercomps = semverStr.split('.'); - return vercomps[0] + '.' + (vercomps[1] ? vercomps[1] : '0'); - } - } - - return false; -}; - -Editor.prototype.updateOpenInQuickBench = function () { - if (options.thirdPartyIntegrationEnabled) { - var quickBenchState = { - text: this.getSource(), - }; - - var compilers = this.getCompilerStates(); - - _.each( - compilers, - _.bind(function (compiler) { - var knownCompiler = false; - - var compilerExtInfo = this.hub.compilerService.findCompiler(this.currentLanguage.id, compiler.compiler); - var semver = this.cleanupSemVer(compilerExtInfo.semver); - var groupOrName = compilerExtInfo.baseName || compilerExtInfo.groupName || compilerExtInfo.name; - if (semver && groupOrName) { - groupOrName = groupOrName.toLowerCase(); - if (groupOrName.indexOf('gcc') !== -1) { - quickBenchState.compiler = 'gcc-' + semver; - knownCompiler = true; - } else if (groupOrName.indexOf('clang') !== -1) { - quickBenchState.compiler = 'clang-' + semver; - knownCompiler = true; - } - } - - if (knownCompiler) { - var match = compiler.options.match(/-(O([0-3sg]|fast))/); - if (match !== null) { - if (match[2] === 'fast') { - quickBenchState.optim = 'F'; - } else { - quickBenchState.optim = match[2].toUpperCase(); - } - } - - if ( - compiler.options.indexOf('-std=c++11') !== -1 || - compiler.options.indexOf('-std=gnu++11') !== -1 - ) { - quickBenchState.cppVersion = '11'; - } else if ( - compiler.options.indexOf('-std=c++14') !== -1 || - compiler.options.indexOf('-std=gnu++14') !== -1 - ) { - quickBenchState.cppVersion = '14'; - } else if ( - compiler.options.indexOf('-std=c++17') !== -1 || - compiler.options.indexOf('-std=gnu++17') !== -1 - ) { - quickBenchState.cppVersion = '17'; - } else if ( - compiler.options.indexOf('-std=c++2a') !== -1 || - compiler.options.indexOf('-std=gnu++2a') !== -1 - ) { - quickBenchState.cppVersion = '20'; - } - - if (compiler.options.indexOf('-stdlib=libc++') !== -1) { - quickBenchState.lib = 'llvm'; - } - } - }, this) - ); - - var link = 'https://quick-bench.com/#' + btoa(this.asciiEncodeJsonText(JSON.stringify(quickBenchState))); - this.quickBenchButton.attr('href', link); - } -}; - -Editor.prototype.changeLanguage = function (newLang) { - if (newLang === 'cmake') { - this.selectize.addOption(languages.cmake); - } - this.selectize.setValue(newLang); -}; - -Editor.prototype.clearLinkedLine = function () { - this.decorations.linkedCode = []; - this.updateDecorations(); -}; - -Editor.prototype.tryPanesLinkLine = function (thisLineNumber, column, reveal) { - var selectedToken = this.getTokenSpan(thisLineNumber, column); - _.each( - this.asmByCompiler, - _.bind(function (asms, compilerId) { - this.eventHub.emit( - 'panesLinkLine', - compilerId, - thisLineNumber, - selectedToken.colBegin, - selectedToken.colEnd, - reveal, - undefined, - this.id - ); - }, this) - ); -}; - -Editor.prototype.requestCompilation = function () { - this.eventHub.emit('requestCompilation', this.id); - if (this.settings.formatOnCompile) { - this.runFormatDocumentAction(); - } - - _.each( - this.hub.trees, - _.bind(function (tree) { - if (tree.multifileService.isEditorPartOfProject(this.id)) { - this.eventHub.emit('requestCompilation', this.id, tree.id); - } - }, this) - ); -}; - -Editor.prototype.initEditorActions = function () { - this.editor.addAction({ - id: 'compile', - label: 'Compile', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], - keybindingContext: null, - contextMenuGroupId: 'navigation', - contextMenuOrder: 1.5, - run: _.bind(function () { - // This change request is mostly superfluous - this.maybeEmitChange(); - this.requestCompilation(); - }, this), - }); - - this.revealJumpStackHasElementsCtxKey = this.editor.createContextKey('hasRevealJumpStackElements', false); - - this.editor.addAction({ - id: 'returnfromreveal', - label: 'Return from reveal jump', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter], - contextMenuGroupId: 'navigation', - contextMenuOrder: 1.4, - precondition: 'hasRevealJumpStackElements', - run: _.bind(function () { - this.popAndRevealJump(); - }, this), - }); - - this.editor.addAction({ - id: 'toggleCompileOnChange', - label: 'Toggle compile on change', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter], - keybindingContext: null, - run: _.bind(function () { - this.eventHub.emit('modifySettings', { - compileOnChange: !this.settings.compileOnChange, - }); - this.alertSystem.notify( - 'Compile on change has been toggled ' + (this.settings.compileOnChange ? 'ON' : 'OFF'), - { - group: 'togglecompile', - alertClass: this.settings.compileOnChange ? 'notification-on' : 'notification-off', - dismissTime: 3000, - } - ); - }, this), - }); - - this.editor.addAction({ - id: 'toggleColourisation', - label: 'Toggle colourisation', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.F1], - keybindingContext: null, - run: _.bind(function () { - this.eventHub.emit('modifySettings', { - colouriseAsm: !this.settings.colouriseAsm, - }); - }, this), - }); - - this.editor.addAction({ - id: 'viewasm', - label: 'Reveal linked code', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F10], - keybindingContext: null, - contextMenuGroupId: 'navigation', - contextMenuOrder: 1.5, - run: _.bind(function (ed) { - var pos = ed.getPosition(); - if (pos != null) { - this.tryPanesLinkLine(pos.lineNumber, pos.column, true); - } - }, this), - }); - - this.isCpp = this.editor.createContextKey('isCpp', true); - this.isCpp.set(this.currentLanguage.id === 'c++'); - - this.isClean = this.editor.createContextKey('isClean', true); - this.isClean.set(this.currentLanguage.id === 'clean'); - - this.editor.addAction({ - id: 'cpprefsearch', - label: 'Search on Cppreference', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F8], - keybindingContext: null, - contextMenuGroupId: 'help', - contextMenuOrder: 1.5, - precondition: 'isCpp', - run: _.bind(this.searchOnCppreference, this), - }); - - this.editor.addAction({ - id: 'clooglesearch', - label: 'Search on Cloogle', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F8], - keybindingContext: null, - contextMenuGroupId: 'help', - contextMenuOrder: 1.5, - precondition: 'isClean', - run: _.bind(this.searchOnCloogle, this), - }); - - this.editor.addCommand( - monaco.KeyMod.CtrlCmd | monaco.KeyCode.F9, - _.bind(function () { - this.runFormatDocumentAction(); - }, this) - ); - - this.editor.addCommand( - monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD, - _.bind(function () { - this.editor.getAction('editor.action.duplicateSelection').run(); - }, this) - ); -}; - -Editor.prototype.emitShortLinkEvent = function () { - if (this.settings.enableSharingPopover) { - this.eventHub.emit('displaySharingPopover'); - } else { - this.eventHub.emit('copyShortLinkToClip'); - } -}; - -Editor.prototype.runFormatDocumentAction = function () { - this.editor.getAction('editor.action.formatDocument').run(); -}; - -Editor.prototype.searchOnCppreference = function (ed) { - var pos = ed.getPosition(); - if (!pos || !ed.getModel()) return; - var word = ed.getModel().getWordAtPosition(pos); - if (!word || !word.word) return; - var preferredLanguage = this.getPreferredLanguageTag(); - // This list comes from the footer of the page - var cpprefLangs = ['ar', 'cs', 'de', 'en', 'es', 'fr', 'it', 'ja', 'ko', 'pl', 'pt', 'ru', 'tr', 'zh']; - // If navigator.languages is supported, we could be a bit more clever and look for a match there too - var langTag = 'en'; - if (cpprefLangs.indexOf(preferredLanguage) !== -1) { - langTag = preferredLanguage; - } - var url = 'https://' + langTag + '.cppreference.com/mwiki/index.php?search=' + encodeURIComponent(word.word); - window.open(url, '_blank', 'noopener'); -}; - -Editor.prototype.searchOnCloogle = function (ed) { - var pos = ed.getPosition(); - if (!pos || !ed.getModel()) return; - var word = ed.getModel().getWordAtPosition(pos); - if (!word || !word.word) return; - var url = 'https://cloogle.org/#' + encodeURIComponent(word.word); - window.open(url, '_blank', 'noopener'); -}; - -Editor.prototype.getPreferredLanguageTag = function () { - var result = 'en'; - var lang = 'en'; - if (navigator) { - if (navigator.languages && navigator.languages.length) { - lang = navigator.languages[0]; - } else if (navigator.language) { - lang = navigator.language; - } - } - // navigator.language[s] is supposed to return strings, but hey, you never know - if (lang !== result && _.isString(lang)) { - var primaryLanguageSubtagIdx = lang.indexOf('-'); - result = lang.substr(0, primaryLanguageSubtagIdx).toLowerCase(); - } - return result; -}; - -Editor.prototype.doesMatchEditor = function (otherSource) { - return otherSource === this.getSource(); -}; - -Editor.prototype.confirmOverwrite = function (yes) { - this.alertSystem.ask( - 'Changes were made to the code', - 'Changes were made to the code while it was being processed. Overwrite changes?', - {yes: yes, no: null} - ); -}; - -Editor.prototype.updateSource = function (newSource) { - // Create something that looks like an edit operation for the whole text - var operation = { - range: this.editor.getModel().getFullModelRange(), - forceMoveMarkers: true, - text: newSource, - }; - var nullFn = function () { - return null; - }; - var viewState = this.editor.saveViewState(); - // Add an undo stop so we don't go back further than expected - this.editor.pushUndoStop(); - // Apply de edit. Note that we lose cursor position, but I've not found a better alternative yet - this.editor.getModel().pushEditOperations(viewState.cursorState, [operation], nullFn); - this.numberUsedLines(); - - if (!this.awaitingInitialResults) { - if (this.selection) { - /* - * this setTimeout is a really crap workaround to fix #2150 - * the TL;DR; is that we reach this point *before* GL has laid - * out the window, so we have no height - * - * If we revealLinesInCenter at this point the editor "does the right thing" - * and scrolls itself all the way to the line we requested. - * - * Unfortunately the editor thinks it is very small, so the "center" - * is the first line, and when the editor does resize eventually things are off. - * - * The workaround is to just delay things "long enough" - * - * This is bad and I feel bad. - */ - setTimeout( - _.bind(function () { - this.editor.setSelection(this.selection); - this.editor.revealLinesInCenter(this.selection.startLineNumber, this.selection.endLineNumber); - }, this), - 500 - ); - } - this.awaitingInitialResults = true; - } -}; - -Editor.prototype.formatCurrentText = function () { - var previousSource = this.getSource(); - var lang = this.currentLanguage; - - if (!Object.prototype.hasOwnProperty.call(lang, 'formatter')) { - return this.alertSystem.notify('This language does not support in-editor formatting', { - group: 'formatting', - alertClass: 'notification-error', - }); - } - - $.ajax({ - type: 'POST', - url: window.location.origin + this.httpRoot + 'api/format/' + lang.formatter, - dataType: 'json', // Expected - contentType: 'application/json', // Sent - data: JSON.stringify({ - source: previousSource, - base: this.settings.formatBase, - }), - success: _.bind(function (result) { - if (result.exit === 0) { - if (this.doesMatchEditor(previousSource)) { - this.updateSource(result.answer); - } else { - this.confirmOverwrite( - _.bind(function () { - this.updateSource(result.answer); - }, this), - null - ); - } - } else { - // Ops, the formatter itself failed! - this.alertSystem.notify('We encountered an error formatting your code: ' + result.answer, { - group: 'formatting', - alertClass: 'notification-error', - }); - } - }, this), - error: _.bind(function (xhr, e_status, error) { - // Hopefully we have not exploded! - if (xhr.responseText) { - try { - var res = JSON.parse(xhr.responseText); - error = res.answer || error; - } catch (e) { - // continue regardless of error - } - } - error = error || 'Unknown error'; - this.alertSystem.notify('We ran into some issues while formatting your code: ' + error, { - group: 'formatting', - alertClass: 'notification-error', - }); - }, this), - cache: true, - }); -}; - -Editor.prototype.resize = function () { - var topBarHeight = utils.updateAndCalcTopBarHeight(this.domRoot, this.topBar, this.hideable); - - this.editor.layout({ - width: this.domRoot.width(), - height: this.domRoot.height() - topBarHeight, - }); - - // Only update the options if needed - if (this.settings.wordWrap) { - this.editor.updateOptions({ - wordWrapColumn: this.editor.getLayoutInfo().viewportColumn, - }); - } -}; - -Editor.prototype.onSettingsChange = function (newSettings) { - var before = this.settings; - var after = newSettings; - this.settings = _.clone(newSettings); - - this.editor.updateOptions({ - autoIndent: this.settings.autoIndent ? 'advanced' : 'none', - autoClosingBrackets: this.settings.autoCloseBrackets, - useVim: this.settings.useVim, - quickSuggestions: this.settings.showQuickSuggestions, - contextmenu: this.settings.useCustomContextMenu, - minimap: { - enabled: this.settings.showMinimap && !options.embedded, - }, - fontFamily: this.settings.editorsFFont, - fontLigatures: this.settings.editorsFLigatures, - wordWrap: this.settings.wordWrap ? 'bounded' : 'off', - wordWrapColumn: this.editor.getLayoutInfo().viewportColumn, // Ensure the column count is up to date - }); - - // Unconditionally send editor changes. The compiler only compiles when needed - this.debouncedEmitChange = _.debounce( - _.bind(function () { - this.maybeEmitChange(); - }, this), - after.delayAfterChange - ); - - if (before.hoverShowSource && !after.hoverShowSource) { - this.onEditorSetDecoration(this.id, -1, false); - } - - if (after.useVim && !before.useVim) { - this.enableVim(); - } else if (!after.useVim && before.useVim) { - this.disableVim(); - } - - if (this.editor.getModel()) { - this.editor.getModel().updateOptions({ - tabSize: this.settings.tabWidth, - insertSpaces: this.settings.useSpaces, - }); - } - - this.numberUsedLines(); -}; - -Editor.prototype.numberUsedLines = function () { - if (_.any(this.busyCompilers)) return; - - if (!this.settings.colouriseAsm) { - this.updateColours([]); - return; - } - - if (this.hub.hasTree()) { - return; - } - - var result = {}; - // First, note all lines used. - _.each( - this.asmByCompiler, - _.bind(function (asm, compilerId) { - _.each( - asm, - _.bind(function (asmLine) { - var foundInTrees = false; - - _.each( - this.treeCompilers, - _.bind(function (compilerIds, treeId) { - if (compilerIds[compilerId]) { - var tree = this.hub.getTreeById(Number(treeId)); - if (tree) { - var defaultFile = this.defaultFileByCompiler[compilerId]; - foundInTrees = true; - - if (asmLine.source && asmLine.source.line > 0) { - var sourcefilename = asmLine.source.file ? asmLine.source.file : defaultFile; - if (this.id === tree.multifileService.getEditorIdByFilename(sourcefilename)) { - result[asmLine.source.line - 1] = true; - } - } - } - } - }, this) - ); - - if (!foundInTrees) { - if ( - asmLine.source && - (asmLine.source.file === null || asmLine.source.mainsource) && - asmLine.source.line > 0 - ) { - result[asmLine.source.line - 1] = true; - } - } - }, this) - ); - }, this) - ); - // Now assign an ordinal to each used line. - var ordinal = 0; - _.each(result, function (v, k) { - result[k] = ordinal++; - }); - - this.updateColours(result); -}; - -Editor.prototype.updateColours = function (colours) { - this.colours = colour.applyColours(this.editor, colours, this.settings.colourScheme, this.colours); - this.eventHub.emit('colours', this.id, colours, this.settings.colourScheme); -}; - -Editor.prototype.onCompilerOpen = function (compilerId, editorId, treeId) { - if (editorId === this.id) { - // On any compiler open, rebroadcast our state in case they need to know it. - if (this.waitingForLanguage) { - var glCompiler = _.find(this.container.layoutManager.root.getComponentsByName('compiler'), function (c) { - return c.id === compilerId; - }); - if (glCompiler) { - var selected = _.find(options.compilers, function (compiler) { - return compiler.id === glCompiler.originalCompilerId; - }); - if (selected) { - this.changeLanguage(selected.lang); - } - } - } - - if (treeId > 0) { - if (!this.treeCompilers[treeId]) { - this.treeCompilers[treeId] = {}; - } - this.treeCompilers[treeId][compilerId] = true; - } - this.ourCompilers[compilerId] = true; - - if (!treeId) { - this.maybeEmitChange(true, compilerId); - } - } -}; - -Editor.prototype.onTreeCompilerEditorIncludeChange = function (treeId, editorId, compilerId) { - if (this.id === editorId) { - this.onCompilerOpen(compilerId, editorId, treeId); - } -}; - -Editor.prototype.onTreeCompilerEditorExcludeChange = function (treeId, editorId, compilerId) { - if (this.id === editorId) { - this.onCompilerClose(compilerId); - } -}; - -Editor.prototype.onColoursForEditor = function (editorId, colours, scheme) { - if (this.id === editorId) { - this.colours = colour.applyColours(this.editor, colours, scheme, this.colours); - } -}; - -Editor.prototype.onExecutorOpen = function (executorId, editorId) { - if (editorId === this.id) { - this.maybeEmitChange(true); - this.ourExecutors[executorId] = true; - } -}; - -Editor.prototype.onCompilerClose = function (compilerId, unused, treeId) { - if (this.treeCompilers[treeId]) { - delete this.treeCompilers[treeId][compilerId]; - } - - if (this.ourCompilers[compilerId]) { - monaco.editor.setModelMarkers(this.editor.getModel(), compilerId, []); - delete this.asmByCompiler[compilerId]; - delete this.busyCompilers[compilerId]; - delete this.ourCompilers[compilerId]; - delete this.defaultFileByCompiler[compilerId]; - this.numberUsedLines(); - } -}; - -Editor.prototype.onExecutorClose = function (id) { - if (this.ourExecutors[id]) { - delete this.ourExecutors[id]; - monaco.editor.setModelMarkers(this.editor.getModel(), 'Executor ' + id, []); - } -}; - -Editor.prototype.onCompiling = function (compilerId) { - if (!this.ourCompilers[compilerId]) return; - this.busyCompilers[compilerId] = true; -}; - -Editor.prototype.addSource = function (arr, source) { - arr.forEach(function (element) { - element.source = source; - }); - return arr; -}; - -Editor.prototype.getAllOutputAndErrors = function (result, compilerName, compilerId) { - var compilerTitle = compilerName + ' #' + compilerId; - var all = this.addSource(result.stdout || [], compilerTitle); - - if (result.buildsteps) { - _.each(result.buildsteps, step => { - all = all.concat(this.addSource(step.stdout, compilerTitle)); - all = all.concat(this.addSource(step.stderr, compilerTitle)); - }); - } - if (result.tools) { - _.each(result.tools, tool => { - all = all.concat(this.addSource(tool.stdout, tool.name + ' #' + compilerId)); - all = all.concat(this.addSource(tool.stderr, tool.name + ' #' + compilerId)); - }); - } - all = all.concat(this.addSource(result.stderr || [], compilerTitle)); - - return all; -}; - -Editor.prototype.collectOutputWidgets = function (output) { - var fixes = []; - var editorModel = this.editor.getModel(); - var widgets = _.compact( - _.map( - output, - function (obj) { - if (!obj.tag) return; - - var trees = this.hub.trees; - if (trees && trees.length > 0) { - if (obj.tag.file) { - if (this.id !== trees[0].multifileService.getEditorIdByFilename(obj.tag.file)) { - return; - } - } else { - if (this.id !== trees[0].multifileService.getMainSourceEditorId()) { - return; - } - } - } - - var colBegin = 0; - var colEnd = Infinity; - var lineBegin = obj.tag.line; - var lineEnd = obj.tag.line; - if (obj.tag.column) { - if (obj.tag.endcolumn) { - colBegin = obj.tag.column; - colEnd = obj.tag.endcolumn; - lineBegin = obj.tag.line; - lineEnd = obj.tag.endline; - } else { - var span = this.getTokenSpan(obj.tag.line, obj.tag.column); - colBegin = obj.tag.column; - colEnd = span.colEnd; - if (colEnd === obj.tag.column) colEnd = -1; - } - } - var link; - if (obj.tag.link) { - link = { - value: obj.tag.link.text, - target: obj.tag.link.url, - }; - } - var diag = { - severity: obj.tag.severity, - message: obj.tag.text, - source: obj.source, - startLineNumber: lineBegin, - startColumn: colBegin, - endLineNumber: lineEnd, - endColumn: colEnd, - code: link, - }; - if (obj.tag.fixes) { - fixes = fixes.concat( - obj.tag.fixes.map(function (fs, ind) { - return { - title: fs.title, - diagnostics: [diag], - kind: 'quickfix', - edit: { - edits: fs.edits.map(function (f) { - return { - resource: editorModel.uri, - edit: { - range: new monaco.Range(f.line, f.column, f.endline, f.endcolumn), - text: f.text, - }, - }; - }), - }, - isPreferred: ind === 0, - }; - }) - ); - } - return diag; - }, - this - ) - ); - return { - fixes: fixes, - widgets: widgets, - }; -}; - -Editor.prototype.setDecorationTags = function (widgets, ownerId) { - monaco.editor.setModelMarkers(this.editor.getModel(), ownerId, widgets); - - this.decorations.tags = _.map( - widgets, - function (tag) { - return { - range: new monaco.Range(tag.startLineNumber, tag.startColumn, tag.startLineNumber + 1, 1), - options: { - isWholeLine: false, - inlineClassName: 'error-code', - }, - }; - }, - this - ); - - this.updateDecorations(); -}; - -Editor.prototype.setQuickFixes = function (fixes) { - if (fixes.length) { - var editorModel = this.editor.getModel(); - quickFixesHandler.registerQuickFixesForCompiler(this.id, editorModel, fixes); - quickFixesHandler.registerProviderForLanguage(editorModel.getLanguageId()); - } else { - quickFixesHandler.unregister(this.id); - } -}; - -Editor.prototype.onCompileResponse = function (compilerId, compiler, result) { - if (!compiler || !this.ourCompilers[compilerId]) return; - - this.busyCompilers[compilerId] = false; - - var collectedOutput = this.collectOutputWidgets(this.getAllOutputAndErrors(result, compiler.name, compilerId)); - - this.setDecorationTags(collectedOutput.widgets, compilerId); - this.setQuickFixes(collectedOutput.fixes); - - if (result.result && result.result.asm) { - this.asmByCompiler[compilerId] = result.result.asm; - } else { - this.asmByCompiler[compilerId] = result.asm; - } - - if (result.inputFilename) { - this.defaultFileByCompiler[compilerId] = result.inputFilename; - } else { - this.defaultFileByCompiler[compilerId] = 'example' + this.currentLanguage.extensions[0]; - } - - this.numberUsedLines(); -}; - -Editor.prototype.onExecuteResponse = function (executorId, compiler, result) { - if (this.ourExecutors[executorId]) { - var output = this.getAllOutputAndErrors(result, compiler.name, 'Execution ' + executorId); - if (result.buildResult) { - output = output.concat( - this.getAllOutputAndErrors(result.buildResult, compiler.name, 'Executor ' + executorId) - ); - } - this.setDecorationTags(this.collectOutputWidgets(output).widgets, 'Executor ' + executorId); - - this.numberUsedLines(); - } -}; - -Editor.prototype.onSelectLine = function (id, lineNum) { - if (Number(id) === this.id) { - this.editor.setSelection(new monaco.Selection(lineNum - 1, 0, lineNum, 0)); - } -}; - -// Returns a half-segment [a, b) for the token on the line lineNum -// that spans across the column. -// a - colStart points to the first character of the token -// b - colEnd points to the character immediately following the token -// e.g.: "this->callableMethod ( x, y );" -// ^a ^column ^b -Editor.prototype.getTokenSpan = function (lineNum, column) { - var model = this.editor.getModel(); - if (lineNum < 1 || lineNum > model.getLineCount()) { - // #3592 Be forgiving towards parsing errors - return {colBegin: 0, colEnd: 0}; - } - if (lineNum <= model.getLineCount()) { - var line = model.getLineContent(lineNum); - if (0 < column && column <= line.length) { - var tokens = monaco.editor.tokenize(line, model.getLanguageId()); - if (tokens.length > 0) { - var lastOffset = 0; - var lastWasString = false; - for (var i = 0; i < tokens[0].length; ++i) { - // Treat all the contiguous string tokens as one, - // For example "hello \" world" is treated as one token - // instead of 3 "string.cpp", "string.escape.cpp", "string.cpp" - if (tokens[0][i].type.startsWith('string')) { - if (lastWasString) { - continue; - } - lastWasString = true; - } else { - lastWasString = false; - } - var currentOffset = tokens[0][i].offset; - if (column <= currentOffset) { - return {colBegin: lastOffset + 1, colEnd: currentOffset + 1}; - } else { - lastOffset = currentOffset; - } - } - return {colBegin: lastOffset + 1, colEnd: line.length + 1}; - } - } - } - return {colBegin: column, colEnd: column + 1}; -}; - -Editor.prototype.pushRevealJump = function () { - this.revealJumpStack.push(this.editor.saveViewState()); - this.revealJumpStackHasElementsCtxKey.set(true); -}; - -Editor.prototype.popAndRevealJump = function () { - if (this.revealJumpStack.length > 0) { - this.editor.restoreViewState(this.revealJumpStack.pop()); - this.revealJumpStackHasElementsCtxKey.set(this.revealJumpStack.length > 0); - } -}; - -Editor.prototype.onEditorLinkLine = function (editorId, lineNum, columnBegin, columnEnd, reveal) { - if (Number(editorId) === this.id) { - if (reveal && lineNum) { - this.pushRevealJump(); - this.hub.activateTabForContainer(this.container); - this.editor.revealLineInCenter(lineNum); - } - this.decorations.linkedCode = []; - if (lineNum && lineNum !== -1) { - this.decorations.linkedCode.push({ - range: new monaco.Range(lineNum, 1, lineNum, 1), - options: { - isWholeLine: true, - linesDecorationsClassName: 'linked-code-decoration-margin', - className: 'linked-code-decoration-line', - }, - }); - } - - if (lineNum > 0 && columnBegin !== -1) { - var lastTokenSpan = this.getTokenSpan(lineNum, columnEnd); - this.decorations.linkedCode.push({ - range: new monaco.Range(lineNum, columnBegin, lineNum, lastTokenSpan.colEnd), - options: { - isWholeLine: false, - inlineClassName: 'linked-code-decoration-column', - }, - }); - } - - if (this.fadeTimeoutId !== -1) { - clearTimeout(this.fadeTimeoutId); - } - this.fadeTimeoutId = setTimeout( - _.bind(function () { - this.clearLinkedLine(); - this.fadeTimeoutId = -1; - }, this), - 5000 - ); - - this.updateDecorations(); - } -}; - -Editor.prototype.onEditorSetDecoration = function (id, lineNum, reveal, column) { - if (Number(id) === this.id) { - if (reveal && lineNum) { - this.pushRevealJump(); - this.editor.revealLineInCenter(lineNum); - this.editor.focus(); - this.editor.setPosition({column: column || 0, lineNumber: lineNum}); - } - this.decorations.linkedCode = []; - if (lineNum && lineNum !== -1) { - this.decorations.linkedCode.push({ - range: new monaco.Range(lineNum, 1, lineNum, 1), - options: { - isWholeLine: true, - linesDecorationsClassName: 'linked-code-decoration-margin', - inlineClassName: 'linked-code-decoration-inline', - }, - }); - } - this.updateDecorations(); - } -}; - -Editor.prototype.onEditorDisplayFlow = function (id, flow) { - if (Number(id) === this.id) { - if (this.decorations.flows && this.decorations.flows.length) { - this.decorations.flows = []; - } else { - this.decorations.flows = flow.map((ri, ind) => { - return { - range: new monaco.Range(ri.line, ri.column, ri.endline || ri.line, ri.endcolumn || ri.column), - options: { - before: { - content: ' ' + (ind + 1).toString() + ' ', - inlineClassName: 'flow-decoration', - cursorStops: monaco.editor.InjectedTextCursorStops.None, - }, - inlineClassName: 'flow-highlight', - isWholeLine: false, - hoverMessage: {value: ri.text}, - }, - }; - }); - } - this.updateDecorations(); - } -}; - -Editor.prototype.updateDecorations = function () { - this.prevDecorations = this.editor.deltaDecorations( - this.prevDecorations, - _.compact(_.flatten(_.values(this.decorations))) - ); -}; - -Editor.prototype.onConformanceViewOpen = function (editorId) { - if (editorId === this.id) { - this.conformanceViewerButton.attr('disabled', true); - } -}; - -Editor.prototype.onConformanceViewClose = function (editorId) { - if (editorId === this.id) { - this.conformanceViewerButton.attr('disabled', false); - } -}; - -Editor.prototype.showLoadSaver = function () { - this.loadSaveButton.click(); -}; - -Editor.prototype.initLoadSaver = function () { - this.loadSaveButton.off('click').click( - _.bind(function () { - loadSave.run( - _.bind(function (text, filename) { - this.setSource(text); - this.setFilename(filename); - this.updateState(); - this.maybeEmitChange(true); - this.requestCompilation(); - }, this), - this.getSource(), - this.currentLanguage - ); - }, this) - ); -}; - -Editor.prototype.onLanguageChange = function (newLangId) { - if (languages[newLangId]) { - if (newLangId !== this.currentLanguage.id) { - var oldLangId = this.currentLanguage.id; - this.currentLanguage = languages[newLangId]; - if (!this.waitingForLanguage && !this.settings.keepSourcesOnLangChange && newLangId !== 'cmake') { - this.editorSourceByLang[oldLangId] = this.getSource(); - this.updateEditorCode(); - } - this.initLoadSaver(); - monaco.editor.setModelLanguage(this.editor.getModel(), this.currentLanguage.monaco); - this.isCpp.set(this.currentLanguage.id === 'c++'); - this.isClean.set(this.currentLanguage.id === 'clean'); - this.updateTitle(); - this.updateState(); - // Broadcast the change to other panels - this.eventHub.emit('languageChange', this.id, newLangId); - this.decorations = {}; - this.maybeEmitChange(true); - this.requestCompilation(); - ga.proxy('send', { - hitType: 'event', - eventCategory: 'LanguageChange', - eventAction: newLangId, - }); - } - this.waitingForLanguage = false; - } -}; - -Editor.prototype.getPaneName = function () { - if (this.filename) { - return this.filename; - } else { - return this.currentLanguage.name + ' source #' + this.id; - } -}; - -Editor.prototype.setFilename = function (name) { - this.filename = name; - this.updateTitle(); - this.updateState(); -}; - -Editor.prototype.updateTitle = function () { - var name = this.getPaneName(); - var customName = this.paneName ? this.paneName : name; - if (name.endsWith('CMakeLists.txt')) { - this.changeLanguage('cmake'); - } - this.container.setTitle(_.escape(customName)); -}; - -// Called every time we change language, so we get the relevant code -Editor.prototype.updateEditorCode = function () { - this.setSource(this.editorSourceByLang[this.currentLanguage.id] || languages[this.currentLanguage.id].example); -}; - -Editor.prototype.close = function () { - this.eventHub.unsubscribe(); - this.eventHub.emit('editorClose', this.id); - this.editor.dispose(); - this.hub.removeEditor(this.id); -}; - -function getSelectizeRenderHtml(data, escape, width, height) { - var result = - '
' + - '
' + - ''; - if (data.logoDataDark) { - result += - ''; - } - - result += '
' + escape(data.name) + '
'; - return result; -} - -function renderSelectizeOption(data, escape) { - return getSelectizeRenderHtml(data, escape, 23, 23); -} - -function renderSelectizeItem(data, escape) { - return getSelectizeRenderHtml(data, escape, 20, 20); -} - -module.exports = { - Editor: Editor, -}; diff --git a/static/panes/editor.ts b/static/panes/editor.ts new file mode 100644 index 000000000..111e125bc --- /dev/null +++ b/static/panes/editor.ts @@ -0,0 +1,1914 @@ +// Copyright (c) 2016, Compiler Explorer Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +import _ from 'underscore'; +import $ from 'jquery'; +import * as colour from '../colour'; +import * as loadSaveLib from '../widgets/load-save'; +import {FontScale} from '../widgets/fontscale'; +import * as Components from '../components'; +import * as monaco from 'monaco-editor'; +import {options} from '../options'; +import {Alert} from '../alert'; +import {ga} from '../analytics'; +import monacoVim from 'monaco-vim'; +import * as monacoConfig from '../monaco-config'; +import * as quickFixesHandler from '../quick-fixes-handler'; +import TomSelect from 'tom-select'; +import {Settings, SiteSettings} from '../settings'; +import '../formatter-registry'; +import '../modes/_all'; +import {MonacoPane} from './pane'; +import {Hub} from '../hub'; +import {MonacoPaneState} from './pane.interfaces'; +import {Container} from 'golden-layout'; +import {EditorState} from './editor.interfaces'; +import {Language, LanguageKey} from '../../types/languages.interfaces'; +import {editor} from 'monaco-editor'; +import IModelDeltaDecoration = editor.IModelDeltaDecoration; +import {MessageWithLocation, ResultLine} from '../../types/resultline/resultline.interfaces'; +import {CompilerInfo} from '../../types/compiler.interfaces'; +import {CompilationResult} from '../../types/compilation/compilation.interfaces'; +import {Decoration, Motd} from '../motd.interfaces'; +import type {escape_html} from 'tom-select/dist/types/utils'; +import ICursorSelectionChangedEvent = editor.ICursorSelectionChangedEvent; + +const loadSave = new loadSaveLib.LoadSave(); +const languages = options.languages as Record; + +// eslint-disable-next-line max-statements +export class Editor extends MonacoPane { + private readonly id: number; + private hub: Hub; + private readonly ourCompilers: Record; + private readonly ourExecutors: Record; + private readonly httpRoot: string; + private readonly asmByCompiler: Record; + private readonly defaultFileByCompiler: Record; + private readonly busyCompilers: Record; + private colours: string[]; + private readonly treeCompilers: Record | undefined>; + private decorations: Record; + private prevDecorations: string[]; + private extraDecorations?: Decoration[]; + private fadeTimeoutId: NodeJS.Timeout | null; + private readonly editorSourceByLang: Record; + private alertSystem: Alert; + private filename: string | null; + private awaitingInitialResults: boolean; + private revealJumpStack: editor.ICodeEditorViewState[]; + private readonly langKeys: string[]; + private readonly legacyReadOnly?: boolean; + private selectize: TomSelect; + private lastChangeEmitted: string | null; + private readonly languageBtn: JQuery; + public currentLanguage?: Language; + private waitingForLanguage: boolean; + private currentCursorPosition: JQuery; + private mouseMoveThrottledFunction?: ((e: monaco.editor.IEditorMouseEvent) => void) & _.Cancelable; + private cursorSelectionThrottledFunction?: (e: monaco.editor.ICursorSelectionChangedEvent) => void & _.Cancelable; + private vimMode: any; + private vimFlag: JQuery; + private loadSaveButton: JQuery; + private addExecutorButton: JQuery; + private conformanceViewerButton: JQuery; + private cppInsightsButton: JQuery; + private quickBenchButton: JQuery; + private nothingCtrlSSince?: number; + private nothingCtrlSTimes?: number; + private isCpp: editor.IContextKey; + private isClean: editor.IContextKey; + private debouncedEmitChange: (() => void) & _.Cancelable; + private revealJumpStackHasElementsCtxKey: editor.IContextKey; + + constructor(hub: Hub, state: MonacoPaneState & EditorState, container: Container) { + super(hub, container, state); + this.id = state.id || hub.nextEditorId(); + this.hub = hub; + // Should probably be its own function somewhere + this.settings = Settings.getStoredSettings(); + this.ourCompilers = {}; + this.ourExecutors = {}; + this.httpRoot = window.httpRoot; + this.asmByCompiler = {}; + this.defaultFileByCompiler = {}; + this.busyCompilers = {}; + this.colours = []; + this.treeCompilers = {}; + + this.decorations = {}; + this.prevDecorations = []; + this.extraDecorations = []; + + this.fadeTimeoutId = null; + + this.editorSourceByLang = {} as Record; + this.alertSystem = new Alert(); + this.alertSystem.prefixMessage = 'Editor #' + this.id; + + this.filename = state.filename || null; + + this.awaitingInitialResults = false; + this.selection = state.selection; + + this.revealJumpStack = []; + + this.langKeys = Object.keys(languages); + this.initLanguage(state); + + this.legacyReadOnly = state.options && !!state.options.readOnly; + + if (state.source !== undefined) { + this.setSource(state.source); + } else { + this.updateEditorCode(); + } + + const startFolded = /^[/*#;]+\s*setup.*/; + if (state.source && state.source.match(startFolded)) { + // With reference to https://github.com/Microsoft/monaco-editor/issues/115 + // I tried that and it didn't work, but a delay of 500 seems to "be enough". + // FIXME: Currently not working - No folding is performed + setTimeout(() => { + this.editor.setSelection(new monaco.Selection(1, 1, 1, 1)); + this.editor.focus(); + this.editor.getAction('editor.fold').run(); + //this.editor.clearSelection(); + }, 500); + } + + this.initEditorActions(); + this.initButtons(state); + this.initCallbacks(); + + if (this.settings.useVim) { + this.enableVim(); + } + + const usableLanguages = Object.values(languages).filter(language => { + return hub.compilerService.compilersByLang[language?.id ?? '']; + }); + + this.languageBtn = this.domRoot.find('.change-language'); + this.selectize = new TomSelect(this.languageBtn as any, { + sortField: 'name', + valueField: 'id', + labelField: 'name', + searchField: ['name'], + placeholder: '🔍 Select a language...', + options: _.map(usableLanguages, _.identity), + items: this.currentLanguage?.id ? [this.currentLanguage.id] : [], + dropdownParent: 'body', + plugins: ['dropdown_input'], + onChange: _.bind(this.onLanguageChange, this), + closeAfterSelect: true, + render: { + option: this.renderSelectizeOption.bind(this), + item: this.renderSelectizeItem.bind(this), + }, + }); + + // We suppress posting changes until the user has stopped typing by: + // * Using _.debounce() to run emitChange on any key event or change + // only after a delay. + // * Only actually triggering a change if the document text has changed from + // the previous emitted. + this.lastChangeEmitted = null; + this.onSettingsChange(this.settings); + // this.editor.on("keydown", _.bind(function () { + // // Not strictly a change; but this suppresses changes until some time + // // after the last key down (be it an actual change or a just a cursor + // // movement etc). + // this.debouncedEmitChange(); + // }, this)); + + this.updateTitle(); + this.updateState(); + } + + override registerOpeningAnalyticsEvent(): void { + ga.proxy('send', { + hitType: 'event', + eventCategory: 'OpenViewPane', + eventAction: 'Editor', + }); + ga.proxy('send', { + hitType: 'event', + eventCategory: 'LanguageChange', + eventAction: this.currentLanguage?.id, + }); + } + + override getInitialHTML(): string { + return $('#codeEditor').html(); + } + + override createEditor(editorRoot: HTMLElement): editor.IStandaloneCodeEditor { + const editor = monaco.editor.create( + editorRoot, + // @ts-expect-error: options.readOnly and anything inside window.compilerExplorerOptions is unknown + monacoConfig.extendConfig( + { + language: this.currentLanguage?.monaco, + readOnly: + !!options.readOnly || + this.legacyReadOnly || + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (window.compilerExplorerOptions && window.compilerExplorerOptions.mobileViewer), + glyphMargin: !options.embedded, + }, + this.settings as SiteSettings + ) + ); + + editor.getModel()?.setEOL(monaco.editor.EndOfLineSequence.LF); + return editor; + } + + onMotd(motd: Motd): void { + this.extraDecorations = motd.decorations; + this.updateExtraDecorations(); + } + + updateExtraDecorations(): void { + let decorationsDirty = false; + this.extraDecorations?.forEach(decoration => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if ( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + decoration.filter && + this.currentLanguage?.name && + decoration.filter.indexOf(this.currentLanguage.name.toLowerCase()) < 0 + ) + return; + const match = this.editor.getModel()?.findNextMatch( + decoration.regex, + { + column: 1, + lineNumber: 1, + }, + true, + true, + null, + false + ); + + if (match !== this.decorations[decoration.name]) { + decorationsDirty = true; + this.decorations[decoration.name] = match + ? [{range: match.range, options: decoration.decoration}] + : undefined; + } + }); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (decorationsDirty) this.updateDecorations(); + } + + // If compilerId is undefined, every compiler will be pinged + maybeEmitChange(force?: boolean, compilerId?: number): void { + const source = this.getSource(); + if (!force && source === this.lastChangeEmitted) return; + + this.updateExtraDecorations(); + + this.lastChangeEmitted = source ?? null; + this.eventHub.emit( + 'editorChange', + this.id, + this.lastChangeEmitted ?? '', + this.currentLanguage?.id ?? '', + compilerId + ); + } + + override updateState(): void { + const state = { + id: this.id, + source: this.getSource(), + lang: this.currentLanguage?.id, + selection: this.selection, + filename: this.filename, + }; + this.fontScale.addState(state); + this.container.setState(state); + + this.updateButtons(); + } + + setSource(newSource: string): void { + this.updateSource(newSource); + + if (window.compilerExplorerOptions.mobileViewer) { + $(this.domRoot.find('.monaco-placeholder textarea')).hide(); + } + } + + onNewSource(editorId: number, newSource: string): void { + if (this.id === editorId) { + this.setSource(newSource); + } + } + + getSource(): string | undefined { + return this.editor.getModel()?.getValue(); + } + + initLanguage(state: MonacoPaneState & EditorState): void { + this.currentLanguage = languages[this.langKeys[0]]; + this.waitingForLanguage = Boolean(state.source && !state.lang); + if (languages[this.settings.defaultLanguage ?? '']) { + this.currentLanguage = languages[this.settings.defaultLanguage ?? '']; + } + + if (languages[state.lang ?? '']) { + this.currentLanguage = languages[state.lang ?? '']; + } else if (this.settings.newEditorLastLang && languages[this.hub.lastOpenedLangId ?? '']) { + this.currentLanguage = languages[this.hub.lastOpenedLangId ?? '']; + } + } + + initCallbacks(): void { + this.fontScale.on('change', _.bind(this.updateState, this)); + this.eventHub.on('broadcastFontScale', scale => { + this.fontScale.setScale(scale); + this.updateState(); + }); + + this.container.on('resize', this.resize, this); + this.container.on('shown', this.resize, this); + this.container.on('open', () => { + this.eventHub.emit('editorOpen', this.id); + }); + this.container.on('destroy', this.close, this); + this.container.layoutManager.on('initialised', () => { + // Once initialized, let everyone know what text we have. + this.maybeEmitChange(); + // And maybe ask for a compilation (Will hit the cache most of the time) + this.requestCompilation(); + }); + + this.eventHub.on('treeCompilerEditorIncludeChange', this.onTreeCompilerEditorIncludeChange, this); + this.eventHub.on('treeCompilerEditorExcludeChange', this.onTreeCompilerEditorExcludeChange, this); + this.eventHub.on('coloursForEditor', this.onColoursForEditor, this); + this.eventHub.on('compilerOpen', this.onCompilerOpen, this); + this.eventHub.on('executorOpen', this.onExecutorOpen, this); + this.eventHub.on('executorClose', this.onExecutorClose, this); + this.eventHub.on('compilerClose', this.onCompilerClose, this); + this.eventHub.on('compiling', this.onCompiling, this); + this.eventHub.on('compileResult', this.onCompileResult, this); + this.eventHub.on('executeResult', this.onExecuteResponse, this); + this.eventHub.on('selectLine', this.onSelectLine, this); + this.eventHub.on('editorSetDecoration', this.onEditorSetDecoration, this); + this.eventHub.on('editorDisplayFlow', this.onEditorDisplayFlow, this); + this.eventHub.on('editorLinkLine', this.onEditorLinkLine, this); + this.eventHub.on('settingsChange', this.onSettingsChange, this); + this.eventHub.on('conformanceViewOpen', this.onConformanceViewOpen, this); + this.eventHub.on('conformanceViewClose', this.onConformanceViewClose, this); + this.eventHub.on('resize', this.resize, this); + this.eventHub.on('newSource', this.onNewSource, this); + this.eventHub.on('motd', this.onMotd, this); + this.eventHub.on('findEditors', this.sendEditor, this); + this.eventHub.emit('requestMotd'); + + this.editor.getModel()?.onDidChangeContent(() => { + this.debouncedEmitChange(); + this.updateState(); + }); + + this.mouseMoveThrottledFunction = _.throttle(this.onMouseMove.bind(this), 50); + + this.editor.onMouseMove(e => { + if (this.mouseMoveThrottledFunction) this.mouseMoveThrottledFunction(e); + }); + + if (window.compilerExplorerOptions.mobileViewer) { + // workaround for issue with contextmenu not going away when tapping somewhere else on the screen + this.editor.onDidChangeCursorSelection(() => { + const contextmenu = $('div.context-view.monaco-menu-container'); + if (contextmenu.css('display') !== 'none') { + contextmenu.hide(); + } + }); + } + + this.cursorSelectionThrottledFunction = _.throttle( + this.onDidChangeCursorSelection.bind(this) as ( + e: editor.ICursorSelectionChangedEvent + ) => void & _.Cancelable, + 500 + ); + this.editor.onDidChangeCursorSelection(e => { + if (this.cursorSelectionThrottledFunction) this.cursorSelectionThrottledFunction(e); + }); + + this.editor.onDidFocusEditorText(_.bind(this.onDidFocusEditorText, this)); + this.editor.onDidBlurEditorText(_.bind(this.onDidBlurEditorText, this)); + this.editor.onDidChangeCursorPosition(_.bind(this.onDidChangeCursorPosition, this)); + + this.eventHub.on('initialised', this.maybeEmitChange, this); + + $(document).on('keyup.editable', e => { + // @ts-expect-error: Document and JQuery have no overlap + if (e.target === this.domRoot.find('.monaco-placeholder .inputarea')[0]) { + if (e.which === 27) { + this.onEscapeKey(); + } else if (e.which === 45) { + this.onInsertKey(e); + } + } + }); + } + + sendEditor(): void { + this.eventHub.emit('editorOpen', this.id); + } + + onMouseMove(e: editor.IEditorMouseEvent): void { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (e !== null && e.target !== null && this.settings.hoverShowSource && e.target.position !== null) { + this.clearLinkedLine(); + const pos = e.target.position; + this.tryPanesLinkLine(pos.lineNumber, pos.column, false); + } + } + + override onDidChangeCursorSelection(e: ICursorSelectionChangedEvent): void { + if (this.awaitingInitialResults) { + this.selection = e.selection; + this.updateState(); + } + } + + onDidChangeCursorPosition(e: ICursorSelectionChangedEvent): void { + // @ts-expect-error: 'position' is not a property of 'e' + if (e.position) { + // @ts-expect-error: 'position' is not a property of 'e' + this.currentCursorPosition.text('(' + e.position.lineNumber + ', ' + e.position.column + ')'); + } + } + + onDidFocusEditorText(): void { + const position = this.editor.getPosition(); + if (position) { + this.currentCursorPosition.text('(' + position.lineNumber + ', ' + position.column + ')'); + } + this.currentCursorPosition.show(); + } + + onDidBlurEditorText(): void { + this.currentCursorPosition.text(''); + this.currentCursorPosition.hide(); + } + + onEscapeKey(): void { + // @ts-expect-error: IStandaloneCodeEditor is missing this property + if (this.editor.vimInUse) { + const currentState = monacoVim.VimMode.Vim.maybeInitVimState_(this.vimMode); + if (currentState.insertMode) { + monacoVim.VimMode.Vim.exitInsertMode(this.vimMode); + } else if (currentState.visualMode) { + monacoVim.VimMode.Vim.exitVisualMode(this.vimMode, false); + } + } + } + + onInsertKey(event: JQuery.TriggeredEvent): void { + // @ts-expect-error: IStandaloneCodeEditor is missing this property + if (this.editor.vimInUse) { + const currentState = monacoVim.VimMode.Vim.maybeInitVimState_(this.vimMode); + if (!currentState.insertMode) { + const insertEvent = { + preventDefault: event.preventDefault, + stopPropagation: event.stopPropagation, + browserEvent: { + key: 'i', + defaultPrevented: false, + }, + keyCode: 39, + }; + this.vimMode.handleKeyDown(insertEvent); + } + } + } + + enableVim(): void { + this.vimMode = monacoVim.initVimMode(this.editor, this.domRoot.find('#v-status')[0]); + this.vimFlag.prop('class', 'btn btn-info'); + // @ts-expect-error: IStandaloneCodeEditor is missing this property + this.editor.vimInUse = true; + } + + disableVim(): void { + this.vimMode.dispose(); + this.domRoot.find('#v-status').html(''); + this.vimFlag.prop('class', 'btn btn-light'); + // @ts-expect-error: IStandaloneCodeEditor is missing this property + this.editor.vimInUse = false; + } + + initButtons(state: MonacoPaneState & EditorState): void { + this.fontScale = new FontScale(this.domRoot, state, this.editor); + // Ensure that the button is disabled if we don't have anything to select + // Note that is might be disabled for other reasons beforehand + if (this.langKeys.length <= 1) { + this.languageBtn.prop('disabled', true); + } + this.topBar = this.domRoot.find('.top-bar'); + this.hideable = this.domRoot.find('.hideable'); + + this.loadSaveButton = this.domRoot.find('.load-save'); + const paneAdderDropdown = this.domRoot.find('.add-pane'); + const addCompilerButton = this.domRoot.find('.btn.add-compiler'); + this.addExecutorButton = this.domRoot.find('.btn.add-executor'); + this.conformanceViewerButton = this.domRoot.find('.btn.conformance'); + const addEditorButton = this.domRoot.find('.btn.add-editor'); + const toggleVimButton = this.domRoot.find('#vim-flag'); + this.vimFlag = this.domRoot.find('#vim-flag'); + toggleVimButton.on('click', () => { + // @ts-expect-error: IStandaloneCodeEditor is missing this property + if (this.editor.vimInUse) { + this.disableVim(); + } else { + this.enableVim(); + } + }); + + // NB a new compilerConfig needs to be created every time; else the state is shared + // between all compilers created this way. That leads to some nasty-to-find state + // bugs e.g. https://github.com/compiler-explorer/compiler-explorer/issues/225 + const getCompilerConfig = () => { + return Components.getCompiler(this.id, this.currentLanguage?.id ?? ''); + }; + + const getExecutorConfig = () => { + return Components.getExecutor(this.id, this.currentLanguage?.id ?? ''); + }; + + const getConformanceConfig = () => { + // TODO: this doesn't pass any treeid introduced by #3360 + return Components.getConformanceView(this.id, 0, this.getSource() ?? '', this.currentLanguage?.id ?? ''); + }; + + const getEditorConfig = () => { + return Components.getEditor(); + }; + + const addPaneOpener = (dragSource, dragConfig) => { + this.container.layoutManager + .createDragSource(dragSource, dragConfig) + // @ts-expect-error: createDragSource returns not void + ._dragListener.on('dragStart', () => { + paneAdderDropdown.dropdown('toggle'); + }); + + dragSource.on('click', () => { + const insertPoint = + this.hub.findParentRowOrColumn(this.container.parent) || + this.container.layoutManager.root.contentItems[0]; + insertPoint.addChild(dragConfig); + }); + }; + + addPaneOpener(addCompilerButton, getCompilerConfig); + addPaneOpener(this.addExecutorButton, getExecutorConfig); + addPaneOpener(this.conformanceViewerButton, getConformanceConfig); + addPaneOpener(addEditorButton, getEditorConfig); + + this.initLoadSaver(); + $(this.domRoot).on('keydown', event => { + if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') { + this.handleCtrlS(event); + } + }); + + if (options.thirdPartyIntegrationEnabled) { + this.cppInsightsButton = this.domRoot.find('.open-in-cppinsights'); + this.cppInsightsButton.on('mousedown', () => { + this.updateOpenInCppInsights(); + }); + + this.quickBenchButton = this.domRoot.find('.open-in-quickbench'); + this.quickBenchButton.on('mousedown', () => { + this.updateOpenInQuickBench(); + }); + } + + this.currentCursorPosition = this.domRoot.find('.currentCursorPosition'); + this.currentCursorPosition.hide(); + } + + handleCtrlS(event: JQuery.KeyDownEvent): void { + event.preventDefault(); + if (this.settings.enableCtrlStree && this.hub.hasTree()) { + const trees = this.hub.trees; + // todo: change when multiple trees are used + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (trees && trees.length > 0) { + trees[0].multifileService.includeByEditorId(this.id).then(() => { + trees[0].refresh(); + }); + } + } else { + if (this.settings.enableCtrlS === 'true') { + if (this.currentLanguage) loadSave.setMinimalOptions(this.getSource() ?? '', this.currentLanguage); + // @ts-expect-error: this.id is not a string + if (!loadSave.onSaveToFile(this.id)) { + this.showLoadSaver(); + } + } else if (this.settings.enableCtrlS === 'false') { + this.emitShortLinkEvent(); + } else if (this.settings.enableCtrlS === '2') { + this.runFormatDocumentAction(); + } else if (this.settings.enableCtrlS === '3') { + this.handleCtrlSDoNothing(); + } + } + } + + handleCtrlSDoNothing(): void { + if (this.nothingCtrlSTimes === undefined) { + this.nothingCtrlSTimes = 0; + this.nothingCtrlSSince = Date.now(); + } else { + if (Date.now() - (this.nothingCtrlSSince ?? 0) > 5000) { + this.nothingCtrlSTimes = undefined; + } else if (this.nothingCtrlSTimes === 4) { + const element = this.domRoot.find('.ctrlSNothing'); + element.show(100); + setTimeout(function () { + element.hide(); + }, 2000); + this.nothingCtrlSTimes = undefined; + } else { + this.nothingCtrlSTimes++; + } + } + } + + updateButtons(): void { + if (options.thirdPartyIntegrationEnabled) { + if (this.currentLanguage?.id === 'c++') { + this.cppInsightsButton.show(); + this.quickBenchButton.show(); + } else { + this.cppInsightsButton.hide(); + this.quickBenchButton.hide(); + } + } + + this.addExecutorButton.prop('disabled', !this.currentLanguage?.supportsExecute); + } + + b64UTFEncode(str: string): string { + return Buffer.from( + encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, v) => { + return String.fromCharCode(parseInt(v, 16)); + }) + ).toString('base64'); + } + + asciiEncodeJsonText(json: string): string { + return json.replace(/[\u007F-\uFFFF]/g, chr => { + // json unicode escapes must always be 4 characters long, so pad with leading zeros + return '\\u' + ('0000' + chr.charCodeAt(0).toString(16)).substring(-4); + }); + } + + getCompilerStates(): any[] { + const states: any[] = []; + + for (const compilerIdStr of Object.keys(this.ourCompilers)) { + const compilerId = parseInt(compilerIdStr); + + const glCompiler = _.find(this.container.layoutManager.root.getComponentsByName('compiler'), function (c) { + return c.id === compilerId; + }); + + if (glCompiler) { + const state = glCompiler.currentState(); + states.push(state); + } + } + + return states; + } + + updateOpenInCppInsights(): void { + if (options.thirdPartyIntegrationEnabled) { + let cppStd = 'cpp2a'; + + const compilers = this.getCompilerStates(); + compilers.forEach(compiler => { + if (compiler.options.indexOf('-std=c++11') !== -1 || compiler.options.indexOf('-std=gnu++11') !== -1) { + cppStd = 'cpp11'; + } else if ( + compiler.options.indexOf('-std=c++14') !== -1 || + compiler.options.indexOf('-std=gnu++14') !== -1 + ) { + cppStd = 'cpp14'; + } else if ( + compiler.options.indexOf('-std=c++17') !== -1 || + compiler.options.indexOf('-std=gnu++17') !== -1 + ) { + cppStd = 'cpp17'; + } else if ( + compiler.options.indexOf('-std=c++2a') !== -1 || + compiler.options.indexOf('-std=gnu++2a') !== -1 + ) { + cppStd = 'cpp2a'; + } else if (compiler.options.indexOf('-std=c++98') !== -1) { + cppStd = 'cpp98'; + } + }); + + const maxURL = 8177; // apache's default maximum url length + const maxCode = maxURL - ('/lnk?code=&std=' + cppStd + '&rev=1.0').length; + let codeData = this.b64UTFEncode(this.getSource() ?? ''); + if (codeData.length > maxCode) { + codeData = this.b64UTFEncode('/** Source too long to fit in a URL */\n'); + } + + const link = 'https://cppinsights.io/lnk?code=' + codeData + '&std=' + cppStd + '&rev=1.0'; + + this.cppInsightsButton.attr('href', link); + } + } + + cleanupSemVer(semver: string): string | null { + if (semver) { + const semverStr = semver.toString(); + if (semverStr !== '' && semverStr.indexOf('(') === -1) { + const vercomps = semverStr.split('.'); + return vercomps[0] + '.' + (vercomps[1] ? vercomps[1] : '0'); + } + } + + return null; + } + + updateOpenInQuickBench(): void { + if (options.thirdPartyIntegrationEnabled) { + type QuickBenchState = { + text?: string; + compiler?: string; + optim?: string; + cppVersion?: string; + lib?: string; + }; + + const quickBenchState: QuickBenchState = { + text: this.getSource(), + }; + + const compilers = this.getCompilerStates(); + + compilers.forEach(compiler => { + let knownCompiler = false; + + const compilerExtInfo = this.hub.compilerService.findCompiler( + this.currentLanguage?.id ?? '', + compiler.compiler + ); + const semver = this.cleanupSemVer(compilerExtInfo.semver); + let groupOrName = compilerExtInfo.baseName || compilerExtInfo.groupName || compilerExtInfo.name; + if (semver && groupOrName) { + groupOrName = groupOrName.toLowerCase(); + if (groupOrName.indexOf('gcc') !== -1) { + quickBenchState.compiler = 'gcc-' + semver; + knownCompiler = true; + } else if (groupOrName.indexOf('clang') !== -1) { + quickBenchState.compiler = 'clang-' + semver; + knownCompiler = true; + } + } + + if (knownCompiler) { + const match = compiler.options.match(/-(O([0-3sg]|fast))/); + if (match !== null) { + if (match[2] === 'fast') { + quickBenchState.optim = 'F'; + } else { + quickBenchState.optim = match[2].toUpperCase(); + } + } + + if ( + compiler.options.indexOf('-std=c++11') !== -1 || + compiler.options.indexOf('-std=gnu++11') !== -1 + ) { + quickBenchState.cppVersion = '11'; + } else if ( + compiler.options.indexOf('-std=c++14') !== -1 || + compiler.options.indexOf('-std=gnu++14') !== -1 + ) { + quickBenchState.cppVersion = '14'; + } else if ( + compiler.options.indexOf('-std=c++17') !== -1 || + compiler.options.indexOf('-std=gnu++17') !== -1 + ) { + quickBenchState.cppVersion = '17'; + } else if ( + compiler.options.indexOf('-std=c++2a') !== -1 || + compiler.options.indexOf('-std=gnu++2a') !== -1 + ) { + quickBenchState.cppVersion = '20'; + } + + if (compiler.options.indexOf('-stdlib=libc++') !== -1) { + quickBenchState.lib = 'llvm'; + } + } + }); + + const link = + 'https://quick-bench.com/#' + + Buffer.from(this.asciiEncodeJsonText(JSON.stringify(quickBenchState))).toString('base64'); + this.quickBenchButton.attr('href', link); + } + } + + changeLanguage(newLang: string): void { + if (newLang === 'cmake' && languages.cmake) { + this.selectize.addOption(languages.cmake); + } + this.selectize.setValue(newLang); + } + + clearLinkedLine() { + this.decorations.linkedCode = []; + this.updateDecorations(); + } + + tryPanesLinkLine(thisLineNumber: number, column: number, reveal: boolean): void { + const selectedToken = this.getTokenSpan(thisLineNumber, column); + for (const compilerId of Object.keys(this.asmByCompiler)) { + this.eventHub.emit( + 'panesLinkLine', + Number(compilerId), + thisLineNumber, + selectedToken.colBegin, + selectedToken.colEnd, + reveal, + this.id + '' + ); + } + } + + requestCompilation(): void { + this.eventHub.emit('requestCompilation', this.id, false); + if (this.settings.formatOnCompile) { + this.runFormatDocumentAction(); + } + + this.hub.trees.forEach(tree => { + if (tree.multifileService.isEditorPartOfProject(this.id)) { + this.eventHub.emit('requestCompilation', this.id, tree.id); + } + }); + } + + initEditorActions(): void { + this.editor.addAction({ + id: 'compile', + label: 'Compile', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], + keybindingContext: undefined, + contextMenuGroupId: 'navigation', + contextMenuOrder: 1.5, + run: () => { + // This change request is mostly superfluous + this.maybeEmitChange(); + this.requestCompilation(); + }, + }); + + this.revealJumpStackHasElementsCtxKey = this.editor.createContextKey('hasRevealJumpStackElements', false); + + this.editor.addAction({ + id: 'returnfromreveal', + label: 'Return from reveal jump', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter], + contextMenuGroupId: 'navigation', + contextMenuOrder: 1.4, + precondition: 'hasRevealJumpStackElements', + run: () => { + this.popAndRevealJump(); + }, + }); + + this.editor.addAction({ + id: 'toggleCompileOnChange', + label: 'Toggle compile on change', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter], + keybindingContext: undefined, + run: () => { + this.eventHub.emit('modifySettings', { + compileOnChange: !this.settings.compileOnChange, + }); + this.alertSystem.notify( + 'Compile on change has been toggled ' + (this.settings.compileOnChange ? 'ON' : 'OFF'), + { + group: 'togglecompile', + alertClass: this.settings.compileOnChange ? 'notification-on' : 'notification-off', + dismissTime: 3000, + } + ); + }, + }); + + this.editor.addAction({ + id: 'toggleColourisation', + label: 'Toggle colourisation', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.F1], + keybindingContext: undefined, + run: () => { + this.eventHub.emit('modifySettings', { + colouriseAsm: !this.settings.colouriseAsm, + }); + }, + }); + + this.editor.addAction({ + id: 'viewasm', + label: 'Reveal linked code', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F10], + keybindingContext: undefined, + contextMenuGroupId: 'navigation', + contextMenuOrder: 1.5, + run: ed => { + const pos = ed.getPosition(); + if (pos != null) { + this.tryPanesLinkLine(pos.lineNumber, pos.column, true); + } + }, + }); + + this.isCpp = this.editor.createContextKey('isCpp', true); + this.isCpp.set(this.currentLanguage?.id === 'c++'); + + this.isClean = this.editor.createContextKey('isClean', true); + this.isClean.set(this.currentLanguage?.id === 'clean'); + + this.editor.addAction({ + id: 'cpprefsearch', + label: 'Search on Cppreference', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F8], + keybindingContext: undefined, + contextMenuGroupId: 'help', + contextMenuOrder: 1.5, + precondition: 'isCpp', + run: this.searchOnCppreference.bind(this), + }); + + this.editor.addAction({ + id: 'clooglesearch', + label: 'Search on Cloogle', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F8], + keybindingContext: undefined, + contextMenuGroupId: 'help', + contextMenuOrder: 1.5, + precondition: 'isClean', + run: this.searchOnCloogle.bind(this), + }); + + this.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.F9, () => { + this.runFormatDocumentAction(); + }); + + this.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD, () => { + this.editor.getAction('editor.action.duplicateSelection').run(); + }); + } + + emitShortLinkEvent(): void { + if (this.settings.enableSharingPopover) { + this.eventHub.emit('displaySharingPopover'); + } else { + this.eventHub.emit('copyShortLinkToClip'); + } + } + + runFormatDocumentAction(): void { + this.editor.getAction('editor.action.formatDocument').run(); + } + + searchOnCppreference(ed: monaco.editor.ICodeEditor): void { + const pos = ed.getPosition(); + if (!pos || !ed.getModel()) return; + const word = ed.getModel()?.getWordAtPosition(pos); + if (!word || !word.word) return; + const preferredLanguage = this.getPreferredLanguageTag(); + // This list comes from the footer of the page + const cpprefLangs = ['ar', 'cs', 'de', 'en', 'es', 'fr', 'it', 'ja', 'ko', 'pl', 'pt', 'ru', 'tr', 'zh']; + // If navigator.languages is supported, we could be a bit more clever and look for a match there too + let langTag = 'en'; + if (cpprefLangs.indexOf(preferredLanguage) !== -1) { + langTag = preferredLanguage; + } + const url = 'https://' + langTag + '.cppreference.com/mwiki/index.php?search=' + encodeURIComponent(word.word); + window.open(url, '_blank', 'noopener'); + } + + searchOnCloogle(ed: monaco.editor.ICodeEditor): void { + const pos = ed.getPosition(); + if (!pos || !ed.getModel()) return; + const word = ed.getModel()?.getWordAtPosition(pos); + if (!word || !word.word) return; + const url = 'https://cloogle.org/#' + encodeURIComponent(word.word); + window.open(url, '_blank', 'noopener'); + } + + getPreferredLanguageTag(): string { + let result = 'en'; + let lang = 'en'; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (navigator) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (navigator.languages && navigator.languages.length) { + lang = navigator.languages[0]; + } else if (navigator.language) { + lang = navigator.language; + } + } + // navigator.language[s] is supposed to return strings, but hey, you never know + if (lang !== result && _.isString(lang)) { + const primaryLanguageSubtagIdx = lang.indexOf('-'); + result = lang.substring(0, primaryLanguageSubtagIdx).toLowerCase(); + } + return result; + } + + doesMatchEditor(otherSource?: string): boolean { + return otherSource === this.getSource(); + } + + confirmOverwrite(yes: () => void): void { + this.alertSystem.ask( + 'Changes were made to the code', + 'Changes were made to the code while it was being processed. Overwrite changes?', + {yes: yes, no: undefined} + ); + } + + updateSource(newSource: string): void { + // Create something that looks like an edit operation for the whole text + const operation = { + range: this.editor.getModel()?.getFullModelRange(), + forceMoveMarkers: true, + text: newSource, + }; + const nullFn = () => { + return null; + }; + + const viewState = this.editor.saveViewState(); + // Add an undo stop so we don't go back further than expected + this.editor.pushUndoStop(); + // Apply de edit. Note that we lose cursor position, but I've not found a better alternative yet + // @ts-expect-error: See above comment maybe + this.editor.getModel()?.pushEditOperations(viewState?.cursorState ?? null, [operation], nullFn); + this.numberUsedLines(); + + if (!this.awaitingInitialResults) { + if (this.selection) { + /* + * this setTimeout is a really crap workaround to fix #2150 + * the TL;DR; is that we reach this point *before* GL has laid + * out the window, so we have no height + * + * If we revealLinesInCenter at this point the editor "does the right thing" + * and scrolls itself all the way to the line we requested. + * + * Unfortunately the editor thinks it is very small, so the "center" + * is the first line, and when the editor does resize eventually things are off. + * + * The workaround is to just delay things "long enough" + * + * This is bad and I feel bad. + */ + setTimeout(() => { + if (this.selection) { + this.editor.setSelection(this.selection); + this.editor.revealLinesInCenter(this.selection.startLineNumber, this.selection.endLineNumber); + } + }, 500); + } + this.awaitingInitialResults = true; + } + } + + formatCurrentText(): void { + const previousSource = this.getSource(); + const lang = this.currentLanguage; + + if (!Object.prototype.hasOwnProperty.call(lang, 'formatter')) { + return this.alertSystem.notify('This language does not support in-editor formatting', { + group: 'formatting', + alertClass: 'notification-error', + }); + } + + $.ajax({ + type: 'POST', + url: window.location.origin + this.httpRoot + 'api/format/' + lang?.formatter, + dataType: 'json', // Expected + contentType: 'application/json', // Sent + data: JSON.stringify({ + source: previousSource, + base: this.settings.formatBase, + }), + success: result => { + if (result.exit === 0) { + if (this.doesMatchEditor(previousSource)) { + this.updateSource(result.answer); + } else { + this.confirmOverwrite(this.updateSource.bind(this, result.answer)); + } + } else { + // Ops, the formatter itself failed! + this.alertSystem.notify('We encountered an error formatting your code: ' + result.answer, { + group: 'formatting', + alertClass: 'notification-error', + }); + } + }, + error: (xhr, e_status, error) => { + // Hopefully we have not exploded! + if (xhr.responseText) { + try { + const res = JSON.parse(xhr.responseText); + error = res.answer || error; + } catch (e) { + // continue regardless of error + } + } + error = error || 'Unknown error'; + this.alertSystem.notify('We ran into some issues while formatting your code: ' + error, { + group: 'formatting', + alertClass: 'notification-error', + }); + }, + cache: true, + }); + } + + override resize(): void { + super.resize(); + + // Only update the options if needed + if (this.settings.wordWrap) { + this.editor.updateOptions({ + wordWrapColumn: this.editor.getLayoutInfo().viewportColumn, + }); + } + } + + override onSettingsChange(newSettings: SiteSettings): void { + const before = this.settings; + const after = newSettings; + this.settings = _.clone(newSettings); + + this.editor.updateOptions({ + autoIndent: this.settings.autoIndent ? 'advanced' : 'none', + // @ts-expect-error: boolean is not assignable to editor.EditorAutoClosingStrategy + autoClosingBrackets: this.settings.autoCloseBrackets, + useVim: this.settings.useVim, + quickSuggestions: this.settings.showQuickSuggestions, + contextmenu: this.settings.useCustomContextMenu, + minimap: { + enabled: this.settings.showMinimap && !options.embedded, + }, + fontFamily: this.settings.editorsFFont, + fontLigatures: this.settings.editorsFLigatures, + wordWrap: this.settings.wordWrap ? 'bounded' : 'off', + wordWrapColumn: this.editor.getLayoutInfo().viewportColumn, // Ensure the column count is up to date + }); + + // Unconditionally send editor changes. The compiler only compiles when needed + this.debouncedEmitChange = _.debounce(() => { + this.maybeEmitChange(); + }, after.delayAfterChange); + + if (before.hoverShowSource && !after.hoverShowSource) { + this.onEditorSetDecoration(this.id, -1, false); + } + + if (after.useVim && !before.useVim) { + this.enableVim(); + } else if (!after.useVim && before.useVim) { + this.disableVim(); + } + + this.editor.getModel()?.updateOptions({ + tabSize: this.settings.tabWidth, + insertSpaces: this.settings.useSpaces, + }); + + this.numberUsedLines(); + } + + numberUsedLines(): void { + if (_.any(this.busyCompilers)) return; + + if (!this.settings.colouriseAsm) { + this.updateColours([]); + return; + } + + if (this.hub.hasTree()) { + return; + } + + const result: Record = {}; + // First, note all lines used. + for (const [compilerId, asm] of Object.entries(this.asmByCompiler)) { + asm?.forEach(asmLine => { + let foundInTrees = false; + + for (const [treeId, compilerIds] of Object.entries(this.treeCompilers)) { + if (compilerIds && compilerIds[compilerId]) { + const tree = this.hub.getTreeById(Number(treeId)); + if (tree) { + const defaultFile = this.defaultFileByCompiler[compilerId]; + foundInTrees = true; + + // @ts-expect-error: Property 'source' does not exist on type 'ResultLine' + if (asmLine.source && asmLine.source.line > 0) { + // @ts-expect-error: Property 'source' does not exist on type 'ResultLine' + const sourcefilename = asmLine.source.file ? asmLine.source.file : defaultFile; + if (this.id === tree.multifileService.getEditorIdByFilename(sourcefilename)) { + // @ts-expect-error: Property 'source' does not exist on type 'ResultLine' + result[asmLine.source.line - 1] = true; + } + } + } + } + } + + if (!foundInTrees) { + if ( + // @ts-expect-error: Property 'source' does not exist on type 'ResultLine' + asmLine.source && + // @ts-expect-error: Property 'source' does not exist on type 'ResultLine' + (asmLine.source.file === null || asmLine.source.mainsource) && + // @ts-expect-error: Property 'source' does not exist on type 'ResultLine' + asmLine.source.line > 0 + ) { + // @ts-expect-error: Property 'source' does not exist on type 'ResultLine' + result[asmLine.source.line - 1] = true; + } + } + }); + } + // Now assign an ordinal to each used line. + let ordinal = 0; + Object.keys(result).forEach(k => { + result[k] = ordinal++; + }); + + this.updateColours(result); + } + + updateColours(colours) { + this.colours = colour.applyColours(this.editor, colours, this.settings.colourScheme, this.colours); + this.eventHub.emit('colours', this.id, colours, this.settings.colourScheme); + } + + onCompilerOpen(compilerId: number, editorId: number, treeId: number | boolean): void { + if (editorId === this.id) { + // On any compiler open, rebroadcast our state in case they need to know it. + if (this.waitingForLanguage) { + const glCompiler = _.find( + this.container.layoutManager.root.getComponentsByName('compiler'), + function (c) { + return c.id === compilerId; + } + ); + if (glCompiler) { + const selected = options.compilers.find(compiler => { + return compiler.id === glCompiler.originalCompilerId; + }); + if (selected) { + this.changeLanguage(selected.lang); + } + } + } + + if (typeof treeId === 'number' && treeId > 0) { + if (!this.treeCompilers[treeId]) { + this.treeCompilers[treeId] = {}; + } + + // @ts-expect-error: this.treeCompilers[treeId] is never undefined at this point + this.treeCompilers[treeId][compilerId] = true; + } + this.ourCompilers[compilerId] = true; + + if (!treeId) { + this.maybeEmitChange(true, compilerId); + } + } + } + + onTreeCompilerEditorIncludeChange(treeId: number, editorId: number, compilerId: number): void { + if (this.id === editorId) { + this.onCompilerOpen(compilerId, editorId, treeId); + } + } + + onTreeCompilerEditorExcludeChange(treeId: number, editorId: number, compilerId: number): void { + if (this.id === editorId) { + this.onCompilerClose(compilerId); + } + } + + onColoursForEditor(editorId: number, colours: Record, scheme: string): void { + if (this.id === editorId) { + this.colours = colour.applyColours(this.editor, colours, scheme, this.colours); + } + } + + onExecutorOpen(executorId: number, editorId: boolean | number): void { + if (editorId === this.id) { + this.maybeEmitChange(true); + this.ourExecutors[executorId] = true; + } + } + + override onCompilerClose(compilerId: number): void { + /*if (this.treeCompilers[treeId]) { + delete this.treeCompilers[treeId][compilerId]; + }*/ + + if (this.ourCompilers[compilerId]) { + const model = this.editor.getModel(); + if (model) monaco.editor.setModelMarkers(model, String(compilerId), []); + delete this.asmByCompiler[compilerId]; + delete this.busyCompilers[compilerId]; + delete this.ourCompilers[compilerId]; + delete this.defaultFileByCompiler[compilerId]; + this.numberUsedLines(); + } + } + + onExecutorClose(id: number): void { + if (this.ourExecutors[id]) { + delete this.ourExecutors[id]; + const model = this.editor.getModel(); + if (model) monaco.editor.setModelMarkers(model, 'Executor ' + id, []); + } + } + + onCompiling(compilerId: number): void { + if (!this.ourCompilers[compilerId]) return; + this.busyCompilers[compilerId] = true; + } + + addSource(arr: (ResultLine & {source?: string})[] | undefined, source: string): (ResultLine & {source: string})[] { + arr?.forEach(element => { + element.source = source; + }); + + return (arr as (ResultLine & {source: string})[] | undefined) ?? []; + } + + getAllOutputAndErrors( + result: CompilationResult, + compilerName: string, + compilerId: number | string + ): (ResultLine & {source: string})[] { + const compilerTitle = compilerName + ' #' + compilerId; + let all = this.addSource(result.stdout, compilerTitle); + + // @ts-expect-error: Property 'buildsteps' does not exist on type 'CompilationResult' + if (result.buildsteps) { + // @ts-expect-error: Property 'buildsteps' does not exist on type 'CompilationResult' + _.each(result.buildsteps, step => { + all = all.concat(this.addSource(step.stdout, compilerTitle)); + all = all.concat(this.addSource(step.stderr, compilerTitle)); + }); + } + if (result.tools) { + _.each(result.tools, tool => { + all = all.concat(this.addSource(tool.stdout, tool.name + ' #' + compilerId)); + all = all.concat(this.addSource(tool.stderr, tool.name + ' #' + compilerId)); + }); + } + all = all.concat(this.addSource(result.stderr, compilerTitle)); + + return all; + } + + collectOutputWidgets(output: (ResultLine & {source: string})[]): { + fixes: monaco.languages.CodeAction[]; + widgets: editor.IMarkerData[]; + } { + let fixes: monaco.languages.CodeAction[] = []; + const editorModel = this.editor.getModel(); + const widgets = _.compact( + output.map(obj => { + if (!obj.tag) return; + + const trees = this.hub.trees; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (trees && trees.length > 0) { + if (obj.tag.file) { + if (this.id !== trees[0].multifileService.getEditorIdByFilename(obj.tag.file)) { + return; + } + } else { + if (this.id !== trees[0].multifileService.getMainSourceEditorId()) { + return; + } + } + } + + let colBegin = 0; + let colEnd = Infinity; + let lineBegin = obj.tag.line; + let lineEnd = obj.tag.line; + if (obj.tag.column) { + if (obj.tag.endcolumn) { + colBegin = obj.tag.column; + colEnd = obj.tag.endcolumn; + lineBegin = obj.tag.line; + lineEnd = obj.tag.endline; + } else { + const span = this.getTokenSpan(obj.tag.line ?? 0, obj.tag.column); + colBegin = obj.tag.column; + colEnd = span.colEnd; + if (colEnd === obj.tag.column) colEnd = -1; + } + } + let link; + if (obj.tag.link) { + link = { + value: obj.tag.link.text, + target: obj.tag.link.url, + }; + } + + const diag: monaco.editor.IMarkerData = { + severity: obj.tag.severity, + message: obj.tag.text, + source: obj.source, + startLineNumber: lineBegin ?? 0, + startColumn: colBegin, + endLineNumber: lineEnd ?? 0, + endColumn: colEnd, + code: link, + }; + + if (obj.tag.fixes && editorModel) { + fixes = fixes.concat( + obj.tag.fixes.map((fs, ind) => { + return { + title: fs.title, + diagnostics: [diag], + kind: 'quickfix', + edit: { + edits: fs.edits.map(f => { + return { + resource: editorModel.uri, + edit: { + range: new monaco.Range( + f.line ?? 0, + f.column ?? 0, + f.endline ?? 0, + f.endcolumn ?? 0 + ), + text: f.text, + }, + }; + }), + }, + isPreferred: ind === 0, + }; + }) + ); + } + return diag; + }) + ); + + return { + fixes: fixes, + widgets: widgets, + }; + } + + setDecorationTags(widgets: editor.IMarkerData[], ownerId: string): void { + const editorModel = this.editor.getModel(); + if (editorModel) monaco.editor.setModelMarkers(editorModel, ownerId, widgets); + + this.decorations.tags = _.map( + widgets, + function (tag) { + return { + range: new monaco.Range(tag.startLineNumber, tag.startColumn, tag.startLineNumber + 1, 1), + options: { + isWholeLine: false, + inlineClassName: 'error-code', + }, + }; + }, + this + ); + + this.updateDecorations(); + } + + setQuickFixes(fixes: monaco.languages.CodeAction[]): void { + if (fixes.length) { + const editorModel = this.editor.getModel(); + if (editorModel) { + quickFixesHandler.registerQuickFixesForCompiler(this.id, editorModel, fixes); + quickFixesHandler.registerProviderForLanguage(editorModel.getLanguageId()); + } + } else { + quickFixesHandler.unregister(this.id); + } + } + + override onCompileResult(compilerId: number, compiler: CompilerInfo, result: CompilationResult): void { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!compiler || !this.ourCompilers[compilerId]) return; + + this.busyCompilers[compilerId] = false; + + const collectedOutput = this.collectOutputWidgets( + this.getAllOutputAndErrors(result, compiler.name, compilerId) + ); + + this.setDecorationTags(collectedOutput.widgets, String(compilerId)); + this.setQuickFixes(collectedOutput.fixes); + + // @ts-expect-error: result has no property 'result' + if (result.result && result.result.asm) { + // @ts-expect-error: result has no property 'result' + this.asmByCompiler[compilerId] = result.result.asm; + } else { + this.asmByCompiler[compilerId] = result.asm; + } + + if (result.inputFilename) { + this.defaultFileByCompiler[compilerId] = result.inputFilename; + } else { + this.defaultFileByCompiler[compilerId] = 'example' + this.currentLanguage?.extensions[0]; + } + + this.numberUsedLines(); + } + + onExecuteResponse(executorId: number, compiler: CompilerInfo, result: CompilationResult): void { + if (this.ourExecutors[executorId]) { + let output = this.getAllOutputAndErrors(result, compiler.name, 'Execution ' + executorId); + if (result.buildResult) { + output = output.concat( + // @ts-expect-error: buildResult is 'unknown' + this.getAllOutputAndErrors(result.buildResult, compiler.name, 'Executor ' + executorId) + ); + } + this.setDecorationTags(this.collectOutputWidgets(output).widgets, 'Executor ' + executorId); + + this.numberUsedLines(); + } + } + + onSelectLine(id: number, lineNum: number): void { + if (Number(id) === this.id) { + this.editor.setSelection(new monaco.Selection(lineNum - 1, 0, lineNum, 0)); + } + } + + // Returns a half-segment [a, b) for the token on the line lineNum + // that spans across the column. + // a - colStart points to the first character of the token + // b - colEnd points to the character immediately following the token + // e.g.: "this->callableMethod ( x, y );" + // ^a ^column ^b + getTokenSpan(lineNum: number, column: number): {colBegin: number; colEnd: number} { + const model = this.editor.getModel(); + if (model && (lineNum < 1 || lineNum > model.getLineCount())) { + // #3592 Be forgiving towards parsing errors + return {colBegin: 0, colEnd: 0}; + } + + if (model && lineNum <= model.getLineCount()) { + const line = model.getLineContent(lineNum); + if (0 < column && column <= line.length) { + const tokens = monaco.editor.tokenize(line, model.getLanguageId()); + if (tokens.length > 0) { + let lastOffset = 0; + let lastWasString = false; + for (let i = 0; i < tokens[0].length; ++i) { + // Treat all the contiguous string tokens as one, + // For example "hello \" world" is treated as one token + // instead of 3 "string.cpp", "string.escape.cpp", "string.cpp" + if (tokens[0][i].type.startsWith('string')) { + if (lastWasString) { + continue; + } + lastWasString = true; + } else { + lastWasString = false; + } + const currentOffset = tokens[0][i].offset; + if (column <= currentOffset) { + return {colBegin: lastOffset + 1, colEnd: currentOffset + 1}; + } else { + lastOffset = currentOffset; + } + } + return {colBegin: lastOffset + 1, colEnd: line.length + 1}; + } + } + } + return {colBegin: column, colEnd: column + 1}; + } + + pushRevealJump(): void { + const state = this.editor.saveViewState(); + if (state) this.revealJumpStack.push(state); + this.revealJumpStackHasElementsCtxKey.set(true); + } + + popAndRevealJump(): void { + if (this.revealJumpStack.length > 0) { + const state = this.revealJumpStack.pop(); + if (state) this.editor.restoreViewState(state); + this.revealJumpStackHasElementsCtxKey.set(this.revealJumpStack.length > 0); + } + } + + onEditorLinkLine(editorId: number, lineNum: number, columnBegin: number, columnEnd: number, reveal: boolean): void { + if (Number(editorId) === this.id) { + if (reveal && lineNum) { + this.pushRevealJump(); + this.hub.activateTabForContainer(this.container); + this.editor.revealLineInCenter(lineNum); + } + this.decorations.linkedCode = []; + if (lineNum && lineNum !== -1) { + this.decorations.linkedCode.push({ + range: new monaco.Range(lineNum, 1, lineNum, 1), + options: { + isWholeLine: true, + linesDecorationsClassName: 'linked-code-decoration-margin', + className: 'linked-code-decoration-line', + }, + }); + } + + if (lineNum > 0 && columnBegin !== -1) { + const lastTokenSpan = this.getTokenSpan(lineNum, columnEnd); + this.decorations.linkedCode.push({ + range: new monaco.Range(lineNum, columnBegin, lineNum, lastTokenSpan.colEnd), + options: { + isWholeLine: false, + inlineClassName: 'linked-code-decoration-column', + }, + }); + } + + if (this.fadeTimeoutId !== null) { + clearTimeout(this.fadeTimeoutId); + } + this.fadeTimeoutId = setTimeout(() => { + this.clearLinkedLine(); + this.fadeTimeoutId = null; + }, 5000); + + this.updateDecorations(); + } + } + + onEditorSetDecoration(id: number, lineNum: number, reveal: boolean, column?: number): void { + if (Number(id) === this.id) { + if (reveal && lineNum) { + this.pushRevealJump(); + this.editor.revealLineInCenter(lineNum); + this.editor.focus(); + this.editor.setPosition({column: column || 0, lineNumber: lineNum}); + } + this.decorations.linkedCode = []; + if (lineNum && lineNum !== -1) { + this.decorations.linkedCode.push({ + range: new monaco.Range(lineNum, 1, lineNum, 1), + options: { + isWholeLine: true, + linesDecorationsClassName: 'linked-code-decoration-margin', + inlineClassName: 'linked-code-decoration-inline', + }, + }); + } + this.updateDecorations(); + } + } + + onEditorDisplayFlow(id: number, flow: MessageWithLocation[]): void { + if (Number(id) === this.id) { + if (this.decorations.flows && this.decorations.flows.length) { + this.decorations.flows = []; + } else { + this.decorations.flows = flow.map((ri, ind) => { + return { + range: new monaco.Range( + ri.line ?? 0, + ri.column ?? 0, + (ri.endline || ri.line) ?? 0, + (ri.endcolumn || ri.column) ?? 0 + ), + options: { + before: { + content: ' ' + (ind + 1).toString() + ' ', + inlineClassName: 'flow-decoration', + cursorStops: monaco.editor.InjectedTextCursorStops.None, + }, + inlineClassName: 'flow-highlight', + isWholeLine: false, + hoverMessage: {value: ri.text}, + }, + }; + }); + } + this.updateDecorations(); + } + } + + updateDecorations(): void { + this.prevDecorations = this.editor.deltaDecorations( + this.prevDecorations, + _.compact(_.flatten(_.values(this.decorations))) + ); + } + + onConformanceViewOpen(editorId: number): void { + if (editorId === this.id) { + this.conformanceViewerButton.attr('disabled', 1); + } + } + + onConformanceViewClose(editorId: number): void { + if (editorId === this.id) { + this.conformanceViewerButton.attr('disabled', null); + } + } + + showLoadSaver(): void { + this.loadSaveButton.trigger('click'); + } + + initLoadSaver(): void { + this.loadSaveButton.off('click').on('click', () => { + if (this.currentLanguage) { + loadSave.run( + (text, filename) => { + this.setSource(text); + this.setFilename(filename); + this.updateState(); + this.maybeEmitChange(true); + this.requestCompilation(); + }, + this.getSource(), + this.currentLanguage + ); + } + }); + } + + onLanguageChange(newLangId: string): void { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (languages[newLangId]) { + if (newLangId !== this.currentLanguage?.id) { + const oldLangId = this.currentLanguage?.id; + this.currentLanguage = languages[newLangId]; + if (!this.waitingForLanguage && !this.settings.keepSourcesOnLangChange && newLangId !== 'cmake') { + this.editorSourceByLang[oldLangId ?? ''] = this.getSource(); + this.updateEditorCode(); + } + this.initLoadSaver(); + const editorModel = this.editor.getModel(); + if (editorModel && this.currentLanguage) + monaco.editor.setModelLanguage(editorModel, this.currentLanguage.monaco); + this.isCpp.set(this.currentLanguage?.id === 'c++'); + this.isClean.set(this.currentLanguage?.id === 'clean'); + this.updateTitle(); + this.updateState(); + // Broadcast the change to other panels + this.eventHub.emit('languageChange', this.id, newLangId); + this.decorations = {}; + this.maybeEmitChange(true); + this.requestCompilation(); + ga.proxy('send', { + hitType: 'event', + eventCategory: 'LanguageChange', + eventAction: newLangId, + }); + } + this.waitingForLanguage = false; + } + } + + override getDefaultPaneName(): string { + return ''; + } + + override getPaneName(): string { + if (this.filename) { + return this.filename; + } else { + return this.currentLanguage?.name + ' source #' + this.id; + } + } + + setFilename(name: string): void { + this.filename = name; + this.updateTitle(); + this.updateState(); + } + + override updateTitle(): void { + const name = this.getPaneName(); + const customName = this.paneName ? this.paneName : name; + if (name.endsWith('CMakeLists.txt')) { + this.changeLanguage('cmake'); + } + this.container.setTitle(_.escape(customName)); + } + + // Called every time we change language, so we get the relevant code + updateEditorCode(): void { + this.setSource( + this.editorSourceByLang[this.currentLanguage?.id ?? ''] || + languages[this.currentLanguage?.id ?? '']?.example + ); + } + + override close(): void { + this.eventHub.unsubscribe(); + this.eventHub.emit('editorClose', this.id); + this.editor.dispose(); + this.hub.removeEditor(this.id); + } + + getSelectizeRenderHtml(data: any, escape: typeof escape_html, width: number, height: number): string { + let result = + '
' + + '
' + + ''; + if (data.logoDataDark) { + result += + ''; + } + + result += '
' + escape(data.name) + '
'; + return result; + } + + renderSelectizeOption(data: any, escape: typeof escape_html) { + return this.getSelectizeRenderHtml(data, escape, 23, 23); + } + + renderSelectizeItem(data: any, escape: typeof escape_html) { + return this.getSelectizeRenderHtml(data, escape, 20, 20); + } + + onCompiler(compilerId: number, compiler: unknown, options: string, editorId: number, treeId: number): void {} +} diff --git a/static/panes/tree.ts b/static/panes/tree.ts index 9f37eee80..6505bd44f 100644 --- a/static/panes/tree.ts +++ b/static/panes/tree.ts @@ -291,8 +291,8 @@ export class Tree { if (file) { file.isOpen = false; const editor = this.hub.getEditorById(editorId); - file.langId = editor.currentLanguage.id; - file.content = editor.getSource(); + file.langId = editor?.currentLanguage?.id ?? ''; + file.content = editor?.getSource() ?? ''; file.editorId = -1; } @@ -380,7 +380,7 @@ export class Tree { (file.isIncluded ? this.namedItems : this.unnamedItems).append(item); } - private refresh() { + refresh() { this.updateState(); this.namedItems.html(''); @@ -399,7 +399,7 @@ export class Tree { this.hub.addInEditorStackIfPossible(dragConfig); } else { const editor = this.hub.getEditorById(file.editorId); - this.hub.activateTabForContainer(editor.container); + this.hub.activateTabForContainer(editor?.container); } this.sendChangesToAllEditors(); diff --git a/static/widgets/load-save.ts b/static/widgets/load-save.ts index e1f59d700..7b27e0119 100644 --- a/static/widgets/load-save.ts +++ b/static/widgets/load-save.ts @@ -257,13 +257,13 @@ export class LoadSave { } } - private setMinimalOptions(editorText: string, currentLanguage: Language) { + setMinimalOptions(editorText: string, currentLanguage: Language) { this.editorText = editorText; this.currentLanguage = currentLanguage; this.extension = currentLanguage.extensions[0] || '.txt'; } - private onSaveToFile(fileEditor?: string) { + onSaveToFile(fileEditor?: string) { try { const fileLang = this.currentLanguage?.name ?? ''; const name = fileLang && fileEditor !== undefined ? fileLang + ' Editor #' + fileEditor + ' ' : '';