/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/editor/Editor.js
2 785 строк
115 KB
Swagatam Mitra
Make remote files read only (#14332)
15 май 2018, 15:52
15 май 2018, 15:52
79b7a78
Код
Авторство
О чём код?
/* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /** * Editor is a 1-to-1 wrapper for a CodeMirror editor instance. It layers on Brackets-specific * functionality and provides APIs that cleanly pass through the bits of CodeMirror that the rest * of our codebase may want to interact with. An Editor is always backed by a Document, and stays * in sync with its content; because Editor keeps the Document alive, it's important to always * destroy() an Editor that's going away so it can release its Document ref. * * For now, there's a distinction between the "master" Editor for a Document - which secretly acts * as the Document's internal model of the text state - and the multitude of "slave" secondary Editors * which, via Document, sync their changes to and from that master. * * For now, direct access to the underlying CodeMirror object is still possible via `_codeMirror` -- * but this is considered deprecated and may go away. * * The Editor object dispatches the following events: * - keydown, keypress, keyup -- When any key event happens in the editor (whether it changes the * text or not). Handlers are passed `(BracketsEvent, Editor, KeyboardEvent)`. The 3nd arg is the * raw DOM event. Note: most listeners will only want to listen for "keypress". * - cursorActivity -- When the user moves the cursor or changes the selection, or an edit occurs. * Note: do not listen to this in order to be generally informed of edits--listen to the * "change" event on Document instead. * - scroll -- When the editor is scrolled, either by user action or programmatically. * - lostContent -- When the backing Document changes in such a way that this Editor is no longer * able to display accurate text. This occurs if the Document's file is deleted, or in certain * Document->editor syncing edge cases that we do not yet support (the latter cause will * eventually go away). * - optionChange -- Triggered when an option for the editor is changed. The 2nd arg to the listener * is a string containing the editor option that is changing. The 3rd arg, which can be any * data type, is the new value for the editor option. * - beforeDestroy - Triggered before the object is about to dispose of all its internal state data * so that listeners can cache things like scroll pos, etc... * * The Editor also dispatches "change" events internally, but you should listen for those on * Documents, not Editors. * * To listen for events, do something like this: (see EventDispatcher for details on this pattern) * `editorInstance.on("eventname", handler);` */ define(function (require, exports, module) { "use strict"; var AnimationUtils = require("utils/AnimationUtils"), Async = require("utils/Async"), CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"), LanguageManager = require("language/LanguageManager"), EventDispatcher = require("utils/EventDispatcher"), Menus = require("command/Menus"), PerfUtils = require("utils/PerfUtils"), PopUpManager = require("widgets/PopUpManager"), PreferencesManager = require("preferences/PreferencesManager"), Strings = require("strings"), TextRange = require("document/TextRange").TextRange, TokenUtils = require("utils/TokenUtils"), ValidationUtils = require("utils/ValidationUtils"), HTMLUtils = require("language/HTMLUtils"), ViewUtils = require("utils/ViewUtils"), MainViewManager = require("view/MainViewManager"), _ = require("thirdparty/lodash"); /** Editor preferences */ var CLOSE_BRACKETS = "closeBrackets", CLOSE_TAGS = "closeTags", DRAG_DROP = "dragDropText", HIGHLIGHT_MATCHES = "highlightMatches", LINEWISE_COPY_CUT = "lineWiseCopyCut", SCROLL_PAST_END = "scrollPastEnd", SHOW_CURSOR_SELECT = "showCursorWhenSelecting", SHOW_LINE_NUMBERS = "showLineNumbers", SMART_INDENT = "smartIndent", SOFT_TABS = "softTabs", SPACE_UNITS = "spaceUnits", STYLE_ACTIVE_LINE = "styleActiveLine", TAB_SIZE = "tabSize", UPPERCASE_COLORS = "uppercaseColors", USE_TAB_CHAR = "useTabChar", WORD_WRAP = "wordWrap", AUTO_HIDE_SEARCH = "autoHideSearch", INDENT_LINE_COMMENT = "indentLineComment", INDENT_LINE_COMMENT = "indentLineComment", INPUT_STYLE = "inputStyle"; /** * A list of gutter name and priorities currently registered for editors. * The line number gutter is defined as { name: LINE_NUMBER_GUTTER, priority: 100 } * @type {Array.<{name: string, priority: number, languageIds: Array}} */ var registeredGutters = []; var cmOptions = {}; /** * Constants * @type {number} */ var MIN_SPACE_UNITS = 1, MIN_TAB_SIZE = 1, DEFAULT_SPACE_UNITS = 4, DEFAULT_TAB_SIZE = 4, MAX_SPACE_UNITS = 10, MAX_TAB_SIZE = 10; var LINE_NUMBER_GUTTER = "CodeMirror-linenumbers", LINE_NUMBER_GUTTER_PRIORITY = 100, CODE_FOLDING_GUTTER_PRIORITY = 1000; // Mappings from Brackets preferences to CodeMirror options cmOptions[CLOSE_BRACKETS] = "autoCloseBrackets"; cmOptions[CLOSE_TAGS] = "autoCloseTags"; cmOptions[DRAG_DROP] = "dragDrop"; cmOptions[HIGHLIGHT_MATCHES] = "highlightSelectionMatches"; cmOptions[LINEWISE_COPY_CUT] = "lineWiseCopyCut"; cmOptions[SCROLL_PAST_END] = "scrollPastEnd"; cmOptions[SHOW_CURSOR_SELECT] = "showCursorWhenSelecting"; cmOptions[SHOW_LINE_NUMBERS] = "lineNumbers"; cmOptions[SMART_INDENT] = "smartIndent"; cmOptions[SPACE_UNITS] = "indentUnit"; cmOptions[STYLE_ACTIVE_LINE] = "styleActiveLine"; cmOptions[TAB_SIZE] = "tabSize"; cmOptions[USE_TAB_CHAR] = "indentWithTabs"; cmOptions[WORD_WRAP] = "lineWrapping"; cmOptions[INPUT_STYLE] = "inputStyle"; PreferencesManager.definePreference(CLOSE_BRACKETS, "boolean", true, { description: Strings.DESCRIPTION_CLOSE_BRACKETS }); // CodeMirror, html mode, set some tags do not close automatically. // We do not initialize "dontCloseTags" because otherwise we would overwrite the default behavior of CodeMirror. PreferencesManager.definePreference(CLOSE_TAGS, "object", { whenOpening: true, whenClosing: true, indentTags: [] }, { description: Strings.DESCRIPTION_CLOSE_TAGS, keys: { dontCloseTags: { type: "array", description: Strings.DESCRIPTION_CLOSE_TAGS_DONT_CLOSE_TAGS }, whenOpening: { type: "boolean", description: Strings.DESCRIPTION_CLOSE_TAGS_WHEN_OPENING, initial: true }, whenClosing: { type: "boolean", description: Strings.DESCRIPTION_CLOSE_TAGS_WHEN_CLOSING, initial: true }, indentTags: { type: "array", description: Strings.DESCRIPTION_CLOSE_TAGS_INDENT_TAGS } } }); PreferencesManager.definePreference(DRAG_DROP, "boolean", false, { description: Strings.DESCRIPTION_DRAG_DROP_TEXT }); PreferencesManager.definePreference(HIGHLIGHT_MATCHES, "boolean", false, { description: Strings.DESCRIPTION_HIGHLIGHT_MATCHES, keys: { showToken: { type: "boolean", description: Strings.DESCRIPTION_HIGHLIGHT_MATCHES_SHOW_TOKEN, initial: false }, wordsOnly: { type: "boolean", description: Strings.DESCRIPTION_HIGHLIGHT_MATCHES_WORDS_ONLY, initial: false } } }); PreferencesManager.definePreference(LINEWISE_COPY_CUT, "boolean", true, { description: Strings.DESCRIPTION_LINEWISE_COPY_CUT }); PreferencesManager.definePreference(SCROLL_PAST_END, "boolean", false, { description: Strings.DESCRIPTION_SCROLL_PAST_END }); PreferencesManager.definePreference(SHOW_CURSOR_SELECT, "boolean", false, { description: Strings.DESCRIPTION_SHOW_CURSOR_WHEN_SELECTING }); PreferencesManager.definePreference(SHOW_LINE_NUMBERS, "boolean", true, { description: Strings.DESCRIPTION_SHOW_LINE_NUMBERS }); PreferencesManager.definePreference(SMART_INDENT, "boolean", true, { description: Strings.DESCRIPTION_SMART_INDENT }); PreferencesManager.definePreference(SOFT_TABS, "boolean", true, { description: Strings.DESCRIPTION_SOFT_TABS }); PreferencesManager.definePreference(SPACE_UNITS, "number", DEFAULT_SPACE_UNITS, { validator: _.partialRight(ValidationUtils.isIntegerInRange, MIN_SPACE_UNITS, MAX_SPACE_UNITS), description: Strings.DESCRIPTION_SPACE_UNITS }); PreferencesManager.definePreference(STYLE_ACTIVE_LINE, "boolean", false, { description: Strings.DESCRIPTION_STYLE_ACTIVE_LINE }); PreferencesManager.definePreference(TAB_SIZE, "number", DEFAULT_TAB_SIZE, { validator: _.partialRight(ValidationUtils.isIntegerInRange, MIN_TAB_SIZE, MAX_TAB_SIZE), description: Strings.DESCRIPTION_TAB_SIZE }); PreferencesManager.definePreference(UPPERCASE_COLORS, "boolean", false, { description: Strings.DESCRIPTION_UPPERCASE_COLORS }); PreferencesManager.definePreference(USE_TAB_CHAR, "boolean", false, { description: Strings.DESCRIPTION_USE_TAB_CHAR }); PreferencesManager.definePreference(WORD_WRAP, "boolean", true, { description: Strings.DESCRIPTION_WORD_WRAP }); PreferencesManager.definePreference(AUTO_HIDE_SEARCH, "boolean", true, { description: Strings.DESCRIPTION_SEARCH_AUTOHIDE }); PreferencesManager.definePreference(INDENT_LINE_COMMENT, "boolean", false, { description: Strings.DESCRIPTION_INDENT_LINE_COMMENT }); PreferencesManager.definePreference(INPUT_STYLE, "string", "textarea", { description: Strings.DESCRIPTION_INPUT_STYLE }); var editorOptions = Object.keys(cmOptions); /** Editor preferences */ /** * Guard flag to prevent focus() reentrancy (via blur handlers), even across Editors * @type {boolean} */ var _duringFocus = false; /** * Constant: ignore upper boundary when centering text * @type {number} */ var BOUNDARY_CHECK_NORMAL = 0, BOUNDARY_IGNORE_TOP = 1; /** * @private * Create a copy of the given CodeMirror position * @param {!CodeMirror.Pos} pos * @return {CodeMirror.Pos} */ function _copyPos(pos) { return new CodeMirror.Pos(pos.line, pos.ch); } /** * Helper functions to check options. * @param {number} options BOUNDARY_CHECK_NORMAL or BOUNDARY_IGNORE_TOP */ function _checkTopBoundary(options) { return (options !== BOUNDARY_IGNORE_TOP); } function _checkBottomBoundary(options) { return true; } /** * Helper function to build preferences context based on the full path of * the file. * * @param {string} fullPath Full path of the file * * @return {*} A context for the specified file name */ function _buildPreferencesContext(fullPath) { return PreferencesManager._buildContext(fullPath, fullPath ? LanguageManager.getLanguageForPath(fullPath).getId() : undefined); } /** * List of all current (non-destroy()ed) Editor instances. Needed when changing global preferences * that affect all editors, e.g. tabbing or color scheme settings. * @type {Array.<Editor>} */ var _instances = []; /** * Creates a new CodeMirror editor instance bound to the given Document. The Document need not have * a "master" Editor realized yet, even if makeMasterEditor is false; in that case, the first time * an edit occurs we will automatically ask EditorManager to create a "master" editor to render the * Document modifiable. * * ALWAYS call destroy() when you are done with an Editor - otherwise it will leak a Document ref. * * @constructor * * @param {!Document} document * @param {!boolean} makeMasterEditor If true, this Editor will set itself as the (secret) "master" * Editor for the Document. If false, this Editor will attach to the Document as a "slave"/ * secondary editor. * @param {!jQueryObject|DomNode} container Container to add the editor to. * @param {{startLine: number, endLine: number}=} range If specified, range of lines within the document * to display in this editor. Inclusive. * @param {!Object} options If specified, contains editor options that can be passed to CodeMirror */ function Editor(document, makeMasterEditor, container, range, options) { var self = this; var isReadOnly = (options && options.isReadOnly) || !document.editable; _instances.push(this); // Attach to document: add ref & handlers this.document = document; document.addRef(); if (container.jquery) { // CodeMirror wants a DOM element, not a jQuery wrapper container = container.get(0); } var $container = $(container); if (range) { // attach this first: want range updated before we process a change this._visibleRange = new TextRange(document, range.startLine, range.endLine); } // store this-bound version of listeners so we can remove them later this._handleDocumentChange = this._handleDocumentChange.bind(this); this._handleDocumentDeleted = this._handleDocumentDeleted.bind(this); this._handleDocumentLanguageChanged = this._handleDocumentLanguageChanged.bind(this); this._doWorkingSetSync = this._doWorkingSetSync.bind(this); document.on("change", this._handleDocumentChange); document.on("deleted", this._handleDocumentDeleted); document.on("languageChanged", this._handleDocumentLanguageChanged); // To sync working sets if the view is for same doc across panes document.on("_dirtyFlagChange", this._doWorkingSetSync); var mode = this._getModeFromDocument(); // (if makeMasterEditor, we attach the Doc back to ourselves below once we're fully initialized) this._inlineWidgets = []; this._inlineWidgetQueues = {}; this._hideMarks = []; this._lastEditorWidth = null; this._$messagePopover = null; // To track which pane the editor is being attached to if it's a full editor this._paneId = null; // To track the parent editor ( host editor at that time of creation) of an inline editor this._hostEditor = null; // Editor supplies some standard keyboard behavior extensions of its own var codeMirrorKeyMap = { "Tab": function () { self._handleTabKey(); }, "Shift-Tab": "indentLess", "Left": function (instance) { self._handleSoftTabNavigation(-1, "moveH"); }, "Right": function (instance) { self._handleSoftTabNavigation(1, "moveH"); }, "Backspace": function (instance) { self._handleSoftTabNavigation(-1, "deleteH"); }, "Delete": function (instance) { self._handleSoftTabNavigation(1, "deleteH"); }, "Esc": function (instance) { if (self.getSelections().length > 1) { CodeMirror.commands.singleSelection(instance); } else { self.removeAllInlineWidgets(); } }, "Home": "goLineLeftSmart", "Cmd-Left": "goLineLeftSmart", "End": "goLineRight", "Cmd-Right": "goLineRight" }; var currentOptions = this._currentOptions = _.zipObject( editorOptions, _.map(editorOptions, function (prefName) { return self._getOption(prefName); }) ); // When panes are created *after* the showLineNumbers option has been turned off // we need to apply the show-line-padding class or the text will be juxtaposed // to the edge of the editor which makes it not easy to read. The code below to handle // that the option change only applies the class to panes that have already been created // This line ensures that the class is applied to any editor created after the fact $container.toggleClass("show-line-padding", Boolean(!this._getOption("showLineNumbers"))); // Create the CodeMirror instance // (note: CodeMirror doesn't actually require using 'new', but jslint complains without it) this._codeMirror = new CodeMirror(container, { autoCloseBrackets : currentOptions[CLOSE_BRACKETS], autoCloseTags : currentOptions[CLOSE_TAGS], coverGutterNextToScrollbar : true, cursorScrollMargin : 3, dragDrop : currentOptions[DRAG_DROP], electricChars : true, extraKeys : codeMirrorKeyMap, highlightSelectionMatches : currentOptions[HIGHLIGHT_MATCHES], indentUnit : currentOptions[USE_TAB_CHAR] ? currentOptions[TAB_SIZE] : currentOptions[SPACE_UNITS], indentWithTabs : currentOptions[USE_TAB_CHAR], inputStyle : currentOptions[INPUT_STYLE], lineNumbers : currentOptions[SHOW_LINE_NUMBERS], lineWiseCopyCut : currentOptions[LINEWISE_COPY_CUT], lineWrapping : currentOptions[WORD_WRAP], matchBrackets : { maxScanLineLength: 50000, maxScanLines: 1000 }, matchTags : { bothTags: true }, scrollPastEnd : !range && currentOptions[SCROLL_PAST_END], showCursorWhenSelecting : currentOptions[SHOW_CURSOR_SELECT], smartIndent : currentOptions[SMART_INDENT], styleActiveLine : currentOptions[STYLE_ACTIVE_LINE], tabSize : currentOptions[TAB_SIZE], readOnly : isReadOnly }); // Can't get CodeMirror's focused state without searching for // CodeMirror-focused. Instead, track focus via onFocus and onBlur // options and track state with this._focused this._focused = false; this._installEditorListeners(); this._renderGutters(); this.on("cursorActivity", function (event, editor) { self._handleCursorActivity(event); }); this.on("keypress", function (event, editor, domEvent) { self._handleKeypressEvents(domEvent); }); this.on("change", function (event, editor, changeList) { self._handleEditorChange(changeList); }); this.on("focus", function (event, editor) { if (self._hostEditor) { // Mark the host editor as the master editor for the hosting document self._hostEditor.document._toggleMasterEditor(self._hostEditor); } else { // Set this full editor as master editor for the document self.document._toggleMasterEditor(self); } }); // Set code-coloring mode BEFORE populating with text, to avoid a flash of uncolored text this._codeMirror.setOption("mode", mode); // Initially populate with text. This will send a spurious change event, so need to make // sure this is understood as a 'sync from document' case, not a genuine edit this._duringSync = true; this._resetText(document.getText()); this._duringSync = false; if (range) { this._updateHiddenLines(); this.setCursorPos(range.startLine, 0); } // Now that we're fully initialized, we can point the document back at us if needed if (makeMasterEditor) { document._makeEditable(this); } // Add scrollTop property to this object for the scroll shadow code to use Object.defineProperty(this, "scrollTop", { get: function () { return this._codeMirror.getScrollInfo().top; } }); // Add an $el getter for Pane Views Object.defineProperty(this, "$el", { get: function () { return $(this.getRootElement()); } }); } EventDispatcher.makeEventDispatcher(Editor.prototype); EventDispatcher.markDeprecated(Editor.prototype, "keyEvent", "'keydown/press/up'"); Editor.prototype.markPaneId = function (paneId) { this._paneId = paneId; // Also add this to the pool of full editors this.document._associateEditor(this); // In case this Editor is initialized not as the first full editor for the document // and the document is already dirty and present in another working set, make sure // to add this documents to the new panes working set. this._doWorkingSetSync(null, this.document); }; Editor.prototype._doWorkingSetSync = function (event, doc) { if (doc === this.document && this._paneId && this.document.isDirty) { MainViewManager.addToWorkingSet(this._paneId, this.document.file, -1, false); } }; /** * Removes this editor from the DOM and detaches from the Document. If this is the "master" * Editor that is secretly providing the Document's backing state, then the Document reverts to * a read-only string-backed mode. */ Editor.prototype.destroy = function () { this.trigger("beforeDestroy", this); // CodeMirror docs for getWrapperElement() say all you have to do is "Remove this from your // tree to delete an editor instance." $(this.getRootElement()).remove(); _instances.splice(_instances.indexOf(this), 1); // Disconnect from Document this.document.releaseRef(); this.document.off("change", this._handleDocumentChange); this.document.off("deleted", this._handleDocumentDeleted); this.document.off("languageChanged", this._handleDocumentLanguageChanged); this.document.off("_dirtyFlagChange", this._doWorkingSetSync); if (this._visibleRange) { // TextRange also refs the Document this._visibleRange.dispose(); } // If we're the Document's master editor, disconnecting from it has special meaning if (this.document._masterEditor === this) { this.document._makeNonEditable(); } else { this.document._disassociateEditor(this); } // Destroying us destroys any inline widgets we're hosting. Make sure their closeCallbacks // run, at least, since they may also need to release Document refs var self = this; this._inlineWidgets.forEach(function (inlineWidget) { self._removeInlineWidgetInternal(inlineWidget); }); }; /** * @private * Handle any cursor movement in editor, including selecting and unselecting text. * @param {!Event} event */ Editor.prototype._handleCursorActivity = function (event) { this._updateStyleActiveLine(); }; /** * @private * Removes any whitespace after one of ]{}) to prevent trailing whitespace when auto-indenting */ Editor.prototype._handleWhitespaceForElectricChars = function () { var self = this, instance = this._codeMirror, selections, lineStr; selections = this.getSelections().map(function (sel) { lineStr = instance.getLine(sel.end.line); if (lineStr && !/\S/.test(lineStr)) { // if the line is all whitespace, move the cursor to the end of the line // before indenting so that embedded whitespace such as indents are not // orphaned to the right of the electric char being inserted sel.end.ch = self.document.getLine(sel.end.line).length; } return sel; }); this.setSelections(selections); }; /** * @private * Handle CodeMirror key events. * @param {!Event} event */ Editor.prototype._handleKeypressEvents = function (event) { var keyStr = String.fromCharCode(event.which || event.keyCode); if (/[\]\{\}\)]/.test(keyStr)) { this._handleWhitespaceForElectricChars(); } }; /** * @private * Helper function for `_handleTabKey()` (case 2) - see comment in that function. * @param {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>} selections * The selections to indent. */ Editor.prototype._addIndentAtEachSelection = function (selections) { var instance = this._codeMirror, usingTabs = instance.getOption("indentWithTabs"), indentUnit = instance.getOption("indentUnit"), edits = []; _.each(selections, function (sel) { var indentStr = "", i, numSpaces; if (usingTabs) { indentStr = "\t"; } else { numSpaces = indentUnit - (sel.start.ch % indentUnit); for (i = 0; i < numSpaces; i++) { indentStr += " "; } } edits.push({edit: {text: indentStr, start: sel.start}}); }); this.document.doMultipleEdits(edits); }; /** * @private * Helper function for `_handleTabKey()` (case 3) - see comment in that function. * @param {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>} selections * The selections to indent. */ Editor.prototype._autoIndentEachSelection = function (selections) { // Capture all the line lengths, so we can tell if anything changed. // Note that this function should only be called if all selections are within a single line. var instance = this._codeMirror, lineLengths = {}; _.each(selections, function (sel) { lineLengths[sel.start.line] = instance.getLine(sel.start.line).length; }); // First, try to do a smart indent on all selections. CodeMirror.commands.indentAuto(instance); // If there were no code or selection changes, then indent each selection one more indent. var changed = false, newSelections = this.getSelections(); if (newSelections.length === selections.length) { _.each(selections, function (sel, index) { var newSel = newSelections[index]; if (CodeMirror.cmpPos(sel.start, newSel.start) !== 0 || CodeMirror.cmpPos(sel.end, newSel.end) !== 0 || instance.getLine(sel.start.line).length !== lineLengths[sel.start.line]) { changed = true; // Bail - we don't need to look any further once we've found a change. return false; } }); } else { changed = true; } if (!changed) { CodeMirror.commands.indentMore(instance); } }; /** * @private * Handle Tab key press. */ Editor.prototype._handleTabKey = function () { // Tab key handling is done as follows: // 1. If any of the selections are multiline, just add one indent level to the // beginning of all lines that intersect any selection. // 2. Otherwise, if any of the selections is a cursor or single-line range that // ends at or after the first non-whitespace character in a line: // - if indentation is set to tabs, just insert a hard tab before each selection. // - if indentation is set to spaces, insert the appropriate number of spaces before // each selection to get to its next soft tab stop. // 3. Otherwise (all selections are cursors or single-line, and are in the whitespace // before their respective lines), try to autoindent each line based on the mode. // If none of the cursors moved and no space was added, then add one indent level // to the beginning of all lines. // Note that in case 2, we do the "dumb" insertion even if the cursor is immediately // before the first non-whitespace character in a line. It might seem more convenient // to do autoindent in that case. However, the problem is if that line is already // indented past its "proper" location. In that case, we don't want Tab to // *outdent* the line. If we had more control over the autoindent algorithm or // implemented it ourselves, we could handle that case separately. var instance = this._codeMirror, selectionType = "indentAuto", selections = this.getSelections(); _.each(selections, function (sel) { if (sel.start.line !== sel.end.line) { // Case 1 - we found a multiline selection. We can bail as soon as we find one of these. selectionType = "indentAtBeginning"; return false; } else if (sel.end.ch > 0 && sel.end.ch >= instance.getLine(sel.end.line).search(/\S/)) { // Case 2 - we found a selection that ends at or after the first non-whitespace // character on the line. We need to keep looking in case we find a later multiline // selection though. selectionType = "indentAtSelection"; } }); switch (selectionType) { case "indentAtBeginning": // Case 1 CodeMirror.commands.indentMore(instance); break; case "indentAtSelection": // Case 2 this._addIndentAtEachSelection(selections); break; case "indentAuto": // Case 3 this._autoIndentEachSelection(selections); break; } }; /** * @private * Handle left arrow, right arrow, backspace and delete keys when soft tabs are used. * @param {number} direction Direction of movement: 1 for forward, -1 for backward * @param {string} functionName name of the CodeMirror function to call if we handle the key */ Editor.prototype._handleSoftTabNavigation = function (direction, functionName) { var instance = this._codeMirror, overallJump = null; if (!instance.getOption("indentWithTabs") && PreferencesManager.get(SOFT_TABS)) { var indentUnit = instance.getOption("indentUnit"); _.each(this.getSelections(), function (sel) { if (CodeMirror.cmpPos(sel.start, sel.end) !== 0) { // This is a range - it will just collapse/be deleted regardless of the jump we set, so // we can just ignore it and continue. (We don't want to return false in this case since // we want to keep looking at other ranges.) return; } var cursor = sel.start, jump = (indentUnit === 0) ? 1 : cursor.ch % indentUnit, line = instance.getLine(cursor.line); // Don't do any soft tab handling if there are non-whitespace characters before the cursor in // any of the selections. if (line.substr(0, cursor.ch).search(/\S/) !== -1) { jump = null; } else if (direction === 1) { // right if (indentUnit) { jump = indentUnit - jump; } // Don't jump if it would take us past the end of the line, or if there are // non-whitespace characters within the jump distance. if (cursor.ch + jump > line.length || line.substr(cursor.ch, jump).search(/\S/) !== -1) { jump = null; } } else { // left // If we are on the tab boundary, jump by the full amount, // but not beyond the start of the line. if (jump === 0) { jump = indentUnit; } if (cursor.ch - jump < 0) { jump = null; } else { // We're moving left, so negate the jump. jump = -jump; } } // Did we calculate a jump, and is this jump value either the first one or // consistent with all the other jumps? If so, we're good. Otherwise, bail // out of the foreach, since as soon as we hit an inconsistent jump we don't // have to look any further. if (jump !== null && (overallJump === null || overallJump === jump)) { overallJump = jump; } else { overallJump = null; return false; } }); } if (overallJump === null) { // Just do the default move, which is one char in the given direction. overallJump = direction; } instance[functionName](overallJump, "char"); }; /** * Determine the mode to use from the document's language * Uses "text/plain" if the language does not define a mode * @return {string} The mode to use */ Editor.prototype._getModeFromDocument = function () { // We'd like undefined/null/"" to mean plain text mode. CodeMirror defaults to plaintext for any // unrecognized mode, but it complains on the console in that fallback case: so, convert // here so we're always explicit, avoiding console noise. return this.document.getLanguage().getMode() || "text/plain"; }; /** * Selects all text and maintains the current scroll position. */ Editor.prototype.selectAllNoScroll = function () { var cm = this._codeMirror, info = this._codeMirror.getScrollInfo(); // Note that we do not have to check for the visible range here. This // concern is handled internally by code mirror. cm.operation(function () { cm.scrollTo(info.left, info.top); cm.execCommand("selectAll"); }); }; /** * @return {boolean} True if editor is not showing the entire text of the document (i.e. an inline editor) */ Editor.prototype.isTextSubset = function () { return Boolean(this._visibleRange); }; /** * Ensures that the lines that are actually hidden in the inline editor correspond to * the desired visible range. */ Editor.prototype._updateHiddenLines = function () { if (this._visibleRange) { var cm = this._codeMirror, self = this; cm.operation(function () { self._hideMarks.forEach(function (mark) { if (mark) { mark.clear(); } }); self._hideMarks = []; self._hideMarks.push(self._hideLines(0, self._visibleRange.startLine)); self._hideMarks.push(self._hideLines(self._visibleRange.endLine + 1, self.lineCount())); }); } }; Editor.prototype._applyChanges = function (changeList) { // _visibleRange has already updated via its own Document listener. See if this change caused // it to lose sync. If so, our whole view is stale - signal our owner to close us. if (this._visibleRange) { if (this._visibleRange.startLine === null || this._visibleRange.endLine === null) { this.trigger("lostContent"); return; } } // Apply text changes to CodeMirror editor var cm = this._codeMirror; cm.operation(function () { var change, newText, i; for (i = 0; i < changeList.length; i++) { change = changeList[i]; newText = change.text.join('\n'); if (!change.from || !change.to) { if (change.from || change.to) { console.error("Change record received with only one end undefined--replacing entire text"); } cm.setValue(newText); } else { cm.replaceRange(newText, change.from, change.to, change.origin); } } }); // The update above may have inserted new lines - must hide any that fall outside our range this._updateHiddenLines(); }; /** * Responds to changes in the CodeMirror editor's text, syncing the changes to the Document. * There are several cases where we want to ignore a CodeMirror change: * - if we're the master editor, editor changes can be ignored because Document is already listening * for our changes * - if we're a secondary editor, editor changes should be ignored if they were caused by us reacting * to a Document change */ Editor.prototype._handleEditorChange = function (changeList) { // we're currently syncing from the Document, so don't echo back TO the Document if (this._duringSync) { return; } // Secondary editor: force creation of "master" editor backing the model, if doesn't exist yet this.document._ensureMasterEditor(); if (this.document._masterEditor !== this) { // Secondary editor: // we're not the ground truth; if we got here, this was a real editor change (not a // sync from the real ground truth), so we need to sync from us into the document // (which will directly push the change into the master editor). // FUTURE: Technically we should add a replaceRange() method to Document and go through // that instead of talking to its master editor directly. It's not clear yet exactly // what the right Document API would be, though. this._duringSync = true; this.document._masterEditor._applyChanges(changeList); this._duringSync = false; // Update which lines are hidden inside our editor, since we're not going to go through // _applyChanges() in our own editor. this._updateHiddenLines(); } // Else, Master editor: // we're the ground truth; nothing else to do, since Document listens directly to us // note: this change might have been a real edit made by the user, OR this might have // been a change synced from another editor // The "editorChange" event is mostly for the use of the CodeHintManager. // It differs from the normal "change" event, that it's actually publicly usable, // whereas the "change" event should be listened to on the document. Also the // Editor dispatches a change event before this event is dispatched, because // CodeHintManager needs to hook in here when other things are already done. this.trigger("editorChange", this, changeList); }; /** * Responds to changes in the Document's text, syncing the changes into our CodeMirror instance. * There are several cases where we want to ignore a Document change: * - if we're the master editor, Document changes should be ignored because we already have the right * text (either the change originated with us, or it has already been set into us by Document) * - if we're a secondary editor, Document changes should be ignored if they were caused by us sending * the document an editor change that originated with us */ Editor.prototype._handleDocumentChange = function (event, doc, changeList) { // we're currently syncing to the Document, so don't echo back FROM the Document if (this._duringSync) { return; } if (this.document._masterEditor !== this) { // Secondary editor: // we're not the ground truth; and if we got here, this was a Document change that // didn't come from us (e.g. a sync from another editor, a direct programmatic change // to the document, or a sync from external disk changes)... so sync from the Document this._duringSync = true; this._applyChanges(changeList); this._duringSync = false; } // Else, Master editor: // we're the ground truth; nothing to do since Document change is just echoing our // editor changes }; /** * Responds to the Document's underlying file being deleted. The Document is now basically dead, * so we must close. */ Editor.prototype._handleDocumentDeleted = function (event) { // Pass the delete event along as the cause (needed in MultiRangeInlineEditor) this.trigger("lostContent", event); }; /** * Responds to language changes, for instance when the file extension is changed. */ Editor.prototype._handleDocumentLanguageChanged = function (event) { this._codeMirror.setOption("mode", this._getModeFromDocument()); }; /** * Install event handlers on the CodeMirror instance, translating them into * jQuery events on the Editor instance. */ Editor.prototype._installEditorListeners = function () { var self = this; // Redispatch these CodeMirror key events as Editor events function _onKeyEvent(instance, event) { self.trigger("keyEvent", self, event); // deprecated self.trigger(event.type, self, event); return event.defaultPrevented; // false tells CodeMirror we didn't eat the event } this._codeMirror.on("keydown", _onKeyEvent); this._codeMirror.on("keypress", _onKeyEvent); this._codeMirror.on("keyup", _onKeyEvent); // FUTURE: if this list grows longer, consider making this a more generic mapping // NOTE: change is a "private" event--others shouldn't listen to it on Editor, only on // Document // Also, note that we use the new "changes" event in v4, which provides an array of // change objects. Our own event is still called just "change". this._codeMirror.on("changes", function (instance, changeList) { self.trigger("change", self, changeList); }); this._codeMirror.on("beforeChange", function (instance, changeObj) { self.trigger("beforeChange", self, changeObj); }); this._codeMirror.on("cursorActivity", function (instance) { self.trigger("cursorActivity", self); }); this._codeMirror.on("beforeSelectionChange", function (instance, selectionObj) { self.trigger("beforeSelectionChange", selectionObj); }); this._codeMirror.on("scroll", function (instance) { // If this editor is visible, close all dropdowns on scroll. // (We don't want to do this if we're just scrolling in a non-visible editor // in response to some document change event.) if (self.isFullyVisible()) { Menus.closeAll(); } self.trigger("scroll", self); }); // Convert CodeMirror onFocus events to EditorManager activeEditorChanged this._codeMirror.on("focus", function () { self._focused = true; self.trigger("focus", self); }); this._codeMirror.on("blur", function () { self._focused = false; self.trigger("blur", self); }); this._codeMirror.on("update", function (instance) { self.trigger("update", self); }); this._codeMirror.on("overwriteToggle", function (instance, newstate) { self.trigger("overwriteToggle", self, newstate); }); // Disable CodeMirror's drop handling if a file/folder is dropped this._codeMirror.on("drop", function (cm, event) { var files = event.dataTransfer.files; if (files && files.length) { event.preventDefault(); } }); // For word wrap. Code adapted from https://codemirror.net/demo/indentwrap.html# this._codeMirror.on("renderLine", function (cm, line, elt) { var charWidth = self._codeMirror.defaultCharWidth(); var off = CodeMirror.countColumn(line.text, null, cm.getOption("tabSize")) * charWidth; elt.style.textIndent = "-" + off + "px"; elt.style.paddingLeft = off + "px"; }); }; /** * Sets the contents of the editor, clears the undo/redo history and marks the document clean. Dispatches a change event. * Semi-private: only Document should call this. * @param {!string} text */ Editor.prototype._resetText = function (text) { var currentText = this._codeMirror.getValue(); // compare with ignoring line-endings, issue #11826 var textLF = text ? text.replace(/(\r\n|\r|\n)/g, "\n") : null; var currentTextLF = currentText ? currentText.replace(/(\r\n|\r|\n)/g, "\n") : null; if (textLF === currentTextLF) { // there's nothing to reset return; } var perfTimerName = PerfUtils.markStart("Editor._resetText()\t" + (!this.document || this.document.file.fullPath)); var cursorPos = this.getCursorPos(), scrollPos = this.getScrollPos(); // This *will* fire a change event, but we clear the undo immediately afterward this._codeMirror.setValue(text); this._codeMirror.refresh(); // Make sure we can't undo back to the empty state before setValue(), and mark // the document clean. this._codeMirror.clearHistory(); this._codeMirror.markClean(); // restore cursor and scroll positions this.setCursorPos(cursorPos); this.setScrollPos(scrollPos.x, scrollPos.y); PerfUtils.addMeasurement(perfTimerName); }; /** * Gets the file associated with this editor * This is a required Pane-View interface method * @return {!File} the file associated with this editor */ Editor.prototype.getFile = function () { return this.document.file; }; /** * Gets the current cursor position within the editor. * @param {boolean} expandTabs If true, return the actual visual column number instead of the character offset in * the "ch" property. * @param {?string} which Optional string indicating which end of the * selection to return. It may be "start", "end", "head" (the side of the * selection that moves when you press shift+arrow), or "anchor" (the * fixed side of the selection). Omitting the argument is the same as * passing "head". A {line, ch} object will be returned.) * @return {!{line:number, ch:number}} */ Editor.prototype.getCursorPos = function (expandTabs, which) { // Translate "start" and "end" to the official CM names (it actually // supports them as-is, but that isn't documented and we don't want to // rely on it). if (which === "start") { which = "from"; } else if (which === "end") { which = "to"; } var cursor = _copyPos(this._codeMirror.getCursor(which)); if (expandTabs) { cursor.ch = this.getColOffset(cursor); } return cursor; }; /** * Returns the display column (zero-based) for a given string-based pos. Differs from pos.ch only * when the line contains preceding \t chars. Result depends on the current tab size setting. * @param {!{line:number, ch:number}} pos * @return {number} */ Editor.prototype.getColOffset = function (pos) { var line = this._codeMirror.getRange({line: pos.line, ch: 0}, pos), tabSize = null, column = 0, i; for (i = 0; i < line.length; i++) { if (line[i] === '\t') { if (tabSize === null) { tabSize = Editor.getTabSize(); } if (tabSize > 0) { column += (tabSize - (column % tabSize)); } } else { column++; } } return column; }; /** * Returns the string-based pos for a given display column (zero-based) in given line. Differs from column * only when the line contains preceding \t chars. Result depends on the current tab size setting. * @param {number} lineNum Line number * @param {number} column Display column number * @return {number} */ Editor.prototype.getCharIndexForColumn = function (lineNum, column) { var line = this._codeMirror.getLine(lineNum), tabSize = null, iCol = 0, i; for (i = 0; iCol < column; i++) { if (line[i] === '\t') { if (tabSize === null) { tabSize = Editor.getTabSize(); } if (tabSize > 0) { iCol += (tabSize - (iCol % tabSize)); } } else { iCol++; } } return i; }; /** * Sets the cursor position within the editor. Removes any selection. * @param {number} line The 0 based line number. * @param {number} ch The 0 based character position; treated as 0 if unspecified. * @param {boolean=} center True if the view should be centered on the new cursor position. * @param {boolean=} expandTabs If true, use the actual visual column number instead of the character offset as * the "ch" parameter. */ Editor.prototype.setCursorPos = function (line, ch, center, expandTabs) { if (expandTabs) { ch = this.getColOffset({line: line, ch: ch}); } this._codeMirror.setCursor(line, ch); if (center) { this.centerOnCursor(); } }; /** * Set the editor size in pixels or percentage * @param {(number|string)} width * @param {(number|string)} height */ Editor.prototype.setSize = function (width, height) { this._codeMirror.setSize(width, height); }; /** @const */ var CENTERING_MARGIN = 0.15; /** * Scrolls the editor viewport to vertically center the line with the cursor, * but only if the cursor is currently near the edges of the viewport or * entirely outside the viewport. * * This does not alter the horizontal scroll position. * * @param {number} centerOptions Option value, or 0 for no options; one of the BOUNDARY_* constants above. */ Editor.prototype.centerOnCursor = function (centerOptions) { var $scrollerElement = $(this.getScrollerElement()); var editorHeight = $scrollerElement.height(); // we need to make adjustments for the statusbar's padding on the bottom and the menu bar on top. var statusBarHeight = $scrollerElement.outerHeight() - editorHeight; var menuBarHeight = $scrollerElement.offset().top; var documentCursorPosition = this._codeMirror.cursorCoords(null, "local").bottom; var screenCursorPosition = this._codeMirror.cursorCoords(null, "page").bottom - menuBarHeight; // If the cursor is already reasonably centered, we won't // make any change. "Reasonably centered" is defined as // not being within CENTERING_MARGIN of the top or bottom // of the editor (where CENTERING_MARGIN is a percentage // of the editor height). // For finding the first item (i.e. find while typing), do // not center if hit is in first half of screen because this // appears to be an unnecesary scroll. if ((_checkTopBoundary(centerOptions) && (screenCursorPosition < editorHeight * CENTERING_MARGIN)) || (_checkBottomBoundary(centerOptions) && (screenCursorPosition > editorHeight * (1 - CENTERING_MARGIN)))) { var pos = documentCursorPosition - editorHeight / 2 + statusBarHeight; var info = this._codeMirror.getScrollInfo(); pos = Math.min(Math.max(pos, 0), (info.height - info.clientHeight)); this.setScrollPos(null, pos); } }; /** * Given a position, returns its index within the text (assuming \n newlines) * @param {!{line:number, ch:number}} * @return {number} */ Editor.prototype.indexFromPos = function (coords) { return this._codeMirror.indexFromPos(coords); }; Editor.prototype.posFromIndex = function (index) { return this._codeMirror.posFromIndex(index); }; /** * Returns true if pos is between start and end (INclusive at start; EXclusive at end by default, * but overridable via the endInclusive flag). * @param {{line:number, ch:number}} pos * @param {{line:number, ch:number}} start * @param {{line:number, ch:number}} end * @param {boolean} endInclusive * */ Editor.prototype.posWithinRange = function (pos, start, end, endInclusive) { if (start.line <= pos.line && end.line >= pos.line) { if (endInclusive) { return (start.line < pos.line || start.ch <= pos.ch) && // inclusive (end.line > pos.line || end.ch >= pos.ch); // inclusive } else { return (start.line < pos.line || start.ch <= pos.ch) && // inclusive (end.line > pos.line || end.ch > pos.ch); // exclusive } } return false; }; /** * @return {boolean} True if there's a text selection; false if there's just an insertion point */ Editor.prototype.hasSelection = function () { return this._codeMirror.somethingSelected(); }; /** * @private * Takes an anchor/head pair and returns a start/end pair where the start is guaranteed to be <= end, and a "reversed" flag indicating * if the head is before the anchor. * @param {!{line: number, ch: number}} anchorPos * @param {!{line: number, ch: number}} headPos * @return {!{start:{line:number, ch:number}, end:{line:number, ch:number}}, reversed:boolean} the normalized range with start <= end */ function _normalizeRange(anchorPos, headPos) { if (headPos.line < anchorPos.line || (headPos.line === anchorPos.line && headPos.ch < anchorPos.ch)) { return {start: _copyPos(headPos), end: _copyPos(anchorPos), reversed: true}; } else { return {start: _copyPos(anchorPos), end: _copyPos(headPos), reversed: false}; } } /** * Gets the current selection; if there is more than one selection, returns the primary selection * (generally the last one made). Start is inclusive, end is exclusive. If there is no selection, * returns the current cursor position as both the start and end of the range (i.e. a selection * of length zero). If `reversed` is set, then the head of the selection (the end of the selection * that would be changed if the user extended the selection) is before the anchor. * @return {!{start:{line:number, ch:number}, end:{line:number, ch:number}}, reversed:boolean} */ Editor.prototype.getSelection = function () { return _normalizeRange(this.getCursorPos(false, "anchor"), this.getCursorPos(false, "head")); }; /** * Returns an array of current selections, nonoverlapping and sorted in document order. * Each selection is a start/end pair, with the start guaranteed to come before the end. * Cursors are represented as a range whose start is equal to the end. * If `reversed` is set, then the head of the selection * (the end of the selection that would be changed if the user extended the selection) * is before the anchor. * If `primary` is set, then that selection is the primary selection. * @return {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>} */ Editor.prototype.getSelections = function () { var primarySel = this.getSelection(); return _.map(this._codeMirror.listSelections(), function (sel) { var result = _normalizeRange(sel.anchor, sel.head); if (result.start.line === primarySel.start.line && result.start.ch === primarySel.start.ch && result.end.line === primarySel.end.line && result.end.ch === primarySel.end.ch) { result.primary = true; } else { result.primary = false; } return result; }); }; /** * Takes the given selections, and expands each selection so it encompasses whole lines. Merges * adjacent line selections together. Keeps track of each original selection associated with a given * line selection (there might be multiple if individual selections were merged into a single line selection). * Useful for doing multiple-selection-aware line edits. * * @param {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>} selections * The selections to expand. * @param {{expandEndAtStartOfLine: boolean, mergeAdjacent: boolean}} options * expandEndAtStartOfLine: true if a range selection that ends at the beginning of a line should be expanded * to encompass the line. Default false. * mergeAdjacent: true if adjacent line ranges should be merged. Default true. * @return {Array.<{selectionForEdit: {start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}, * selectionsToTrack: Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>}>} * The combined line selections. For each selection, `selectionForEdit` is the line selection, and `selectionsToTrack` is * the set of original selections that combined to make up the given line selection. Note that the selectionsToTrack will * include the original objects passed in `selections`, so if it is later mutated the original passed-in selections will be * mutated as well. */ Editor.prototype.convertToLineSelections = function (selections, options) { var self = this; options = options || {}; _.defaults(options, { expandEndAtStartOfLine: false, mergeAdjacent: true }); // Combine adjacent lines with selections so they don't collide with each other, as they would // if we did them individually. var combinedSelections = [], prevSel; _.each(selections, function (sel) { var newSel = _.cloneDeep(sel); // Adjust selection to encompass whole lines. newSel.start.ch = 0; // The end of the selection becomes the start of the next line, if it isn't already // or if expandEndAtStartOfLine is set. var hasSelection = (newSel.start.line !== newSel.end.line) || (newSel.start.ch !== newSel.end.ch); if (options.expandEndAtStartOfLine || !hasSelection || newSel.end.ch !== 0) { newSel.end = {line: newSel.end.line + 1, ch: 0}; } // If the start of the new selection is within the range of the previous (expanded) selection, merge // the two selections together, but keep track of all the original selections that were related to this // selection, so they can be properly adjusted. (We only have to check for the start being inside the previous // range - it can't be before it because the selections started out sorted.) if (prevSel && self.posWithinRange(newSel.start, prevSel.selectionForEdit.start, prevSel.selectionForEdit.end, options.mergeAdjacent)) { prevSel.selectionForEdit.end.line = newSel.end.line; prevSel.selectionsToTrack.push(sel); } else { prevSel = {selectionForEdit: newSel, selectionsToTrack: [sel]}; combinedSelections.push(prevSel); } }); return combinedSelections; }; /** * Returns the currently selected text, or "" if no selection. Includes \n if the * selection spans multiple lines (does NOT reflect the Document's line-endings style). By * default, returns only the contents of the primary selection, unless `allSelections` is true. * @param {boolean=} allSelections Whether to return the contents of all selections (separated * by newlines) instead of just the primary selection. Default false. * @return {!string} The selected text. */ Editor.prototype.getSelectedText = function (allSelections) { if (allSelections) { return this._codeMirror.getSelection(); } else { var sel = this.getSelection(); return this.document.getRange(sel.start, sel.end); } }; /** * Sets the current selection. Start is inclusive, end is exclusive. Places the cursor at the * end of the selection range. Optionally centers around the cursor after * making the selection * * @param {!{line:number, ch:number}} start * @param {{line:number, ch:number}=} end If not specified, defaults to start. * @param {boolean} center true to center the viewport * @param {number} centerOptions Option value, or 0 for no options; one of the BOUNDARY_* constants above. * @param {?string} origin An optional string that describes what other selection or edit operations this * should be merged with for the purposes of undo. See {@link Document#replaceRange} for more details. */ Editor.prototype.setSelection = function (start, end, center, centerOptions, origin) { this.setSelections([{start: start, end: end || start}], center, centerOptions, origin); }; /** * Sets a multiple selection, with the "primary" selection (the one returned by * getSelection() and getCursorPos()) defaulting to the last if not specified. * Overlapping ranges will be automatically merged, and the selection will be sorted. * Optionally centers around the primary selection after making the selection. * @param {!Array<{start:{line:number, ch:number}, end:{line:number, ch:number}, primary:boolean, reversed: boolean}>} selections * The selection ranges to set. If the start and end of a range are the same, treated as a cursor. * If reversed is true, set the anchor of the range to the end instead of the start. * If primary is true, this is the primary selection. Behavior is undefined if more than * one selection has primary set to true. If none has primary set to true, the last one is primary. * @param {boolean} center true to center the viewport around the primary selection. * @param {number} centerOptions Option value, or 0 for no options; one of the BOUNDARY_* constants above. * @param {?string} origin An optional string that describes what other selection or edit operations this * should be merged with for the purposes of undo. See {@link Document#replaceRange} for more details. */ Editor.prototype.setSelections = function (selections, center, centerOptions, origin) { var primIndex = selections.length - 1, options; if (origin) { options = { origin: origin }; } this._codeMirror.setSelections(_.map(selections, function (sel, index) { if (sel.primary) { primIndex = index; } return { anchor: sel.reversed ? sel.end : sel.start, head: sel.reversed ? sel.start : sel.end }; }), primIndex, options); if (center) { this.centerOnCursor(centerOptions); } }; /** * Sets the editors overwrite mode state. If null is passed, the state is toggled. * * @param {?boolean} start */ Editor.prototype.toggleOverwrite = function (state) { this._codeMirror.toggleOverwrite(state); }; /** * Selects word that the given pos lies within or adjacent to. If pos isn't touching a word * (e.g. within a token like "//"), moves the cursor to pos without selecting a range. * @param {!{line:number, ch:number}} */ Editor.prototype.selectWordAt = function (pos) { var word = this._codeMirror.findWordAt(pos); this.setSelection(word.anchor, word.head); }; /** * Gets the total number of lines in the document (includes lines not visible in the viewport) * @return {!number} */ Editor.prototype.lineCount = function () { return this._codeMirror.lineCount(); }; /** * Deterines if line is fully visible. * @param {number} zero-based index of the line to test * @return {boolean} true if the line is fully visible, false otherwise */ Editor.prototype.isLineVisible = function (line) { var coords = this._codeMirror.charCoords({line: line, ch: 0}, "local"), scrollInfo = this._codeMirror.getScrollInfo(), top = scrollInfo.top, bottom = scrollInfo.top + scrollInfo.clientHeight; // Check top and bottom and return false for partially visible lines. return (coords.top >= top && coords.bottom <= bottom); }; /** * Gets the number of the first visible line in the editor. * @return {number} The 0-based index of the first visible line. */ Editor.prototype.getFirstVisibleLine = function () { return (this._visibleRange ? this._visibleRange.startLine : 0); }; /** * Gets the number of the last visible line in the editor. * @return {number} The 0-based index of the last visible line. */ Editor.prototype.getLastVisibleLine = function () { return (this._visibleRange ? this._visibleRange.endLine : this.lineCount() - 1); }; /* Hides the specified line number in the editor * @param {!from} line to start hiding from (inclusive) * @param {!to} line to end hiding at (exclusive) * @return {TextMarker} The CodeMirror mark object that's hiding the lines */ Editor.prototype._hideLines = function (from, to) { if (to <= from) { return; } // We set clearWhenEmpty: false so that if there's a blank line at the beginning or end of // the document, and that's the only hidden line, we can still actually hide it. Doing so // requires us to create a 0-length marked span, which would ordinarily be cleaned up by CM // if clearWithEmpty is true. See https://groups.google.com/forum/#!topic/codemirror/RB8VNF8ow2w var value = this._codeMirror.markText( {line: from, ch: 0}, {line: to - 1, ch: this._codeMirror.getLine(to - 1).length}, {collapsed: true, inclusiveLeft: true, inclusiveRight: true, clearWhenEmpty: false} ); return value; }; /** * Gets the total height of the document in pixels (not the viewport) * @return {!number} height in pixels */ Editor.prototype.totalHeight = function () { return this.getScrollerElement().scrollHeight; }; /** * Gets the scroller element from the editor. * @return {!HTMLDivElement} scroller */ Editor.prototype.getScrollerElement = function () { return this._codeMirror.getScrollerElement(); }; /** * Gets the root DOM node of the editor. * @return {!HTMLDivElement} The editor's root DOM node. */ Editor.prototype.getRootElement = function () { return this._codeMirror.getWrapperElement(); }; /** * Gets the lineSpace element within the editor (the container around the individual lines of code). * FUTURE: This is fairly CodeMirror-specific. Logic that depends on this may break if we switch * editors. * @return {!HTMLDivElement} The editor's lineSpace element. */ Editor.prototype._getLineSpaceElement = function () { return $(".CodeMirror-lines", this.getScrollerElement()).children().get(0); }; /** * Returns the current scroll position of the editor. * @return {{x:number, y:number}} The x,y scroll position in pixels */ Editor.prototype.getScrollPos = function () { var scrollInfo = this._codeMirror.getScrollInfo(); return { x: scrollInfo.left, y: scrollInfo.top }; }; /** * Restores and adjusts the current scroll position of the editor. * @param {{x:number, y:number}} scrollPos - The x,y scroll position in pixels * @param {!number} heightDelta - The amount of delta H to apply to the scroll position */ Editor.prototype.adjustScrollPos = function (scrollPos, heightDelta) { this._codeMirror.scrollTo(scrollPos.x, scrollPos.y + heightDelta); }; /** * Sets the current scroll position of the editor. * @param {number} x scrollLeft position in pixels * @param {number} y scrollTop position in pixels */ Editor.prototype.setScrollPos = function (x, y) { this._codeMirror.scrollTo(x, y); }; /* * Returns the current text height of the editor. * @return {number} Height of the text in pixels */ Editor.prototype.getTextHeight = function () { return this._codeMirror.defaultTextHeight(); }; /** * Adds an inline widget below the given line. If any inline widget was already open for that * line, it is closed without warning. * @param {!{line:number, ch:number}} pos Position in text to anchor the inline. * @param {!InlineWidget} inlineWidget The widget to add. * @param {boolean=} scrollLineIntoView Scrolls the associated line into view. Default true. * @return {$.Promise} A promise object that is resolved when the widget has been added (but might * still be animating open). Never rejected. */ Editor.prototype.addInlineWidget = function (pos, inlineWidget, scrollLineIntoView) { var self = this, queue = this._inlineWidgetQueues[pos.line], deferred = new $.Deferred(); if (!queue) { queue = new Async.PromiseQueue(); this._inlineWidgetQueues[pos.line] = queue; } queue.add(function () { self._addInlineWidgetInternal(pos, inlineWidget, scrollLineIntoView, deferred); return deferred.promise(); }); return deferred.promise(); }; /** * @private * Does the actual work of addInlineWidget(). */ Editor.prototype._addInlineWidgetInternal = function (pos, inlineWidget, scrollLineIntoView, deferred) { var self = this; this.removeAllInlineWidgetsForLine(pos.line).done(function () { if (scrollLineIntoView === undefined) { scrollLineIntoView = true; } if (scrollLineIntoView) { self._codeMirror.scrollIntoView(pos); } inlineWidget.info = self._codeMirror.addLineWidget(pos.line, inlineWidget.htmlContent, { coverGutter: true, noHScroll: true }); CodeMirror.on(inlineWidget.info.line, "delete", function () { self._removeInlineWidgetInternal(inlineWidget); }); self._inlineWidgets.push(inlineWidget); // Set up the widget to start closed, then animate open when its initial height is set. inlineWidget.$htmlContent.height(0); AnimationUtils.animateUsingClass(inlineWidget.htmlContent, "animating") .done(function () { deferred.resolve(); }); // Callback to widget once parented to the editor. The widget should call back to // setInlineWidgetHeight() in order to set its initial height and animate open. inlineWidget.onAdded(); }); }; /** * Removes all inline widgets */ Editor.prototype.removeAllInlineWidgets = function () { // copy the array because _removeInlineWidgetInternal will modify the original var widgets = [].concat(this.getInlineWidgets()); return Async.doInParallel( widgets, this.removeInlineWidget.bind(this) ); }; /** * Removes the given inline widget. * @param {number} inlineWidget The widget to remove. * @return {$.Promise} A promise that is resolved when the inline widget is fully closed and removed from the DOM. */ Editor.prototype.removeInlineWidget = function (inlineWidget) { var deferred = new $.Deferred(), self = this; function finishRemoving() { self._codeMirror.removeLineWidget(inlineWidget.info); self._removeInlineWidgetInternal(inlineWidget); deferred.resolve(); } if (!inlineWidget.closePromise) { // Remove the inline widget from our internal list immediately, so // everyone external to us knows it's essentially already gone. We // don't want to wait until it's done animating closed (but we do want // the other stuff in _removeInlineWidgetInternal to wait until then). self._removeInlineWidgetFromList(inlineWidget); // If we're not visible (in which case the widget will have 0 client height), // don't try to do the animation, because nothing will happen and we won't get // called back right away. (The animation would happen later when we switch // back to the editor.) if (self.isFullyVisible()) { AnimationUtils.animateUsingClass(inlineWidget.htmlContent, "animating") .done(finishRemoving); inlineWidget.$htmlContent.height(0); } else { finishRemoving(); } inlineWidget.closePromise = deferred.promise(); } return inlineWidget.closePromise; }; /** * Removes all inline widgets for a given line * @param {number} lineNum The line number to modify */ Editor.prototype.removeAllInlineWidgetsForLine = function (lineNum) { var lineInfo = this._codeMirror.lineInfo(lineNum), widgetInfos = (lineInfo && lineInfo.widgets) ? [].concat(lineInfo.widgets) : null, self = this; if (widgetInfos && widgetInfos.length) { // Map from CodeMirror LineWidget to Brackets InlineWidget var inlineWidget, allWidgetInfos = this._inlineWidgets.map(function (w) { return w.info; }); return Async.doInParallel( widgetInfos, function (info) { // Lookup the InlineWidget object using the same index inlineWidget = self._inlineWidgets[allWidgetInfos.indexOf(info)]; if (inlineWidget) { return self.removeInlineWidget(inlineWidget); } else { return new $.Deferred().resolve().promise(); } } ); } else { return new $.Deferred().resolve().promise(); } }; /** * Cleans up the given inline widget from our internal list of widgets. It's okay * to call this multiple times for the same widget--it will just do nothing if * the widget has already been removed. * @param {InlineWidget} inlineWidget an inline widget. */ Editor.prototype._removeInlineWidgetFromList = function (inlineWidget) { var l = this._inlineWidgets.length, i; for (i = 0; i < l; i++) { if (this._inlineWidgets[i] === inlineWidget) { this._inlineWidgets.splice(i, 1); break; } } }; /** * Removes the inline widget from the editor and notifies it to clean itself up. * @param {InlineWidget} inlineWidget an inline widget. */ Editor.prototype._removeInlineWidgetInternal = function (inlineWidget) { if (!inlineWidget.isClosed) { this._removeInlineWidgetFromList(inlineWidget); inlineWidget.onClosed(); inlineWidget.isClosed = true; } }; /** * Returns a list of all inline widgets currently open in this editor. Each entry contains the * inline's id, and the data parameter that was passed to addInlineWidget(). * @return {!Array.<{id:number, data:Object}>} */ Editor.prototype.getInlineWidgets = function () { return this._inlineWidgets; }; /** * Returns the currently focused inline widget, if any. * @return {?InlineWidget} */ Editor.prototype.getFocusedInlineWidget = function () { var result = null; this.getInlineWidgets().forEach(function (widget) { if (widget.hasFocus()) { result = widget; } }); return result; }; /** * Display temporary popover message at current cursor position. Display message above * cursor if space allows, otherwise below. * * @param {string} errorMsg Error message to display */ Editor.prototype.displayErrorMessageAtCursor = function (errorMsg) { var arrowBelow, cursorPos, cursorCoord, popoverRect, top, left, clip, arrowCenter, arrowLeft, self = this, POPOVER_MARGIN = 10, POPOVER_ARROW_HALF_WIDTH = 10, POPOVER_ARROW_HALF_BASE = POPOVER_ARROW_HALF_WIDTH + 3; // 3 is border radius function _removeListeners() { self.off(".msgbox"); } // PopUpManager.removePopUp() callback function _clearMessagePopover() { if (self._$messagePopover && self._$messagePopover.length > 0) { // self._$messagePopover.remove() is done by PopUpManager self._$messagePopover = null; } _removeListeners(); } // PopUpManager.removePopUp() is called either directly by this closure, or by // PopUpManager as a result of another popup being invoked. function _removeMessagePopover() { if (self._$messagePopover) { PopUpManager.removePopUp(self._$messagePopover); } } function _addListeners() { self .on("blur.msgbox", _removeMessagePopover) .on("change.msgbox", _removeMessagePopover) .on("cursorActivity.msgbox", _removeMessagePopover) .on("update.msgbox", _removeMessagePopover); } // Only 1 message at a time if (this._$messagePopover) { _removeMessagePopover(); } // Make sure cursor is in view cursorPos = this.getCursorPos(); this._codeMirror.scrollIntoView(cursorPos); // Determine if arrow is above or below cursorCoord = this._codeMirror.charCoords(cursorPos); // Assume popover height is max of 2 lines arrowBelow = (cursorCoord.top > 100); // Text is dynamic, so build popover first so we can measure final width this._$messagePopover = $("<div/>").addClass("popover-message").appendTo($("body")); if (!arrowBelow) { $("<div/>").addClass("arrowAbove").appendTo(this._$messagePopover); } $("<div/>").addClass("text").appendTo(this._$messagePopover).html(errorMsg); if (arrowBelow) { $("<div/>").addClass("arrowBelow").appendTo(this._$messagePopover); } // Estimate where to position popover. top = (arrowBelow) ? cursorCoord.top - this._$messagePopover.height() - POPOVER_MARGIN : cursorCoord.bottom + POPOVER_MARGIN; left = cursorCoord.left - (this._$messagePopover.width() / 2); popoverRect = { top: top, left: left, height: this._$messagePopover.height(), width: this._$messagePopover.width() }; // See if popover is clipped on any side clip = ViewUtils.getElementClipSize($("#editor-holder"), popoverRect); // Prevent horizontal clipping if (clip.left > 0) { left += clip.left; } else if (clip.right > 0) { left -= clip.right; } // Popover text and arrow are positioned individually this._$messagePopover.css({"top": top, "left": left}); // Position popover arrow centered over/under cursor... arrowCenter = cursorCoord.left - left; // ... but don't let it slide off text box arrowCenter = Math.min(popoverRect.width - POPOVER_ARROW_HALF_BASE, Math.max(arrowCenter, POPOVER_ARROW_HALF_BASE)); arrowLeft = arrowCenter - POPOVER_ARROW_HALF_WIDTH; if (arrowBelow) { this._$messagePopover.find(".arrowBelow").css({"margin-left": arrowLeft}); } else { this._$messagePopover.find(".arrowAbove").css({"margin-left": arrowLeft}); } // Add listeners PopUpManager.addPopUp(this._$messagePopover, _clearMessagePopover, true); _addListeners(); // Animate open AnimationUtils.animateUsingClass(this._$messagePopover[0], "animateOpen").done(function () { // Make sure we still have a popover if (self._$messagePopover && self._$messagePopover.length > 0) { self._$messagePopover.addClass("open"); // Don't add scroll listeners until open so we don't get event // from scrolling cursor into view self.on("scroll.msgbox", _removeMessagePopover); // Animate closed -- which includes delay to show message AnimationUtils.animateUsingClass(self._$messagePopover[0], "animateClose", 6000) .done(_removeMessagePopover); } }); }; /** * Returns the offset of the top of the virtual scroll area relative to the browser window (not the editor * itself). Mainly useful for calculations related to scrollIntoView(), where you're starting with the * offset() of a child widget (relative to the browser window) and need to figure out how far down it is from * the top of the virtual scroll area (excluding the top padding). * @return {number} */ Editor.prototype.getVirtualScrollAreaTop = function () { var topPadding = this._getLineSpaceElement().offsetTop, // padding within mover scroller = this.getScrollerElement(); return $(scroller).offset().top - scroller.scrollTop + topPadding; }; /** * Sets the height of an inline widget in this editor. * @param {!InlineWidget} inlineWidget The widget whose height should be set. * @param {!number} height The height of the widget. * @param {boolean=} ensureVisible Whether to scroll the entire widget into view. Default false. */ Editor.prototype.setInlineWidgetHeight = function (inlineWidget, height, ensureVisible) { var self = this, node = inlineWidget.htmlContent, oldHeight = (node && $(node).height()) || 0, changed = (oldHeight !== height), isAttached = inlineWidget.info !== undefined; function updateHeight() { // Notify CodeMirror for the height change. if (isAttached) { inlineWidget.info.changed(); } } function setOuterHeight() { function finishAnimating(e) { if (e.target === node) { updateHeight(); $(node).off("webkitTransitionEnd", finishAnimating); } } $(node).height(height); if ($(node).hasClass("animating")) { $(node).on("webkitTransitionEnd", finishAnimating); } else { updateHeight(); } } // Make sure we set an explicit height on the widget, so children can use things like // min-height if they want. if (changed || !node.style.height) { // If we're animating, set the wrapper's height on a timeout so the layout is finished before we animate. if ($(node).hasClass("animating")) { window.setTimeout(setOuterHeight, 0); } else { setOuterHeight(); } } if (ensureVisible && isAttached) { var offset = $(node).offset(), // offset relative to document position = $(node).position(), // position within parent linespace scrollerTop = self.getVirtualScrollAreaTop(); self._codeMirror.scrollIntoView({ left: position.left, top: offset.top - scrollerTop, right: position.left, // don't try to make the right edge visible bottom: offset.top + height - scrollerTop }); } }; /** * @private * Get the starting line number for an inline widget. * @param {!InlineWidget} inlineWidget * @return {number} The line number of the widget or -1 if not found. */ Editor.prototype._getInlineWidgetLineNumber = function (inlineWidget) { return this._codeMirror.getLineNumber(inlineWidget.info.line); }; /** Gives focus to the editor control */ Editor.prototype.focus = function () { // Focusing an editor synchronously triggers focus/blur handlers. If a blur handler attemps to focus // another editor, we'll put CM in a bad state (because CM assumes programmatically focusing itself // will always succeed, and if you're in the middle of another focus change that appears to be untrue). // So instead, we simply ignore reentrant focus attempts. // See bug #2951 for an example of this happening and badly hosing things. if (_duringFocus) { return; } _duringFocus = true; try { this._codeMirror.focus(); } finally { _duringFocus = false; } }; /** Returns true if the editor has focus */ Editor.prototype.hasFocus = function () { return this._focused; }; /* * @typedef {scrollPos:{x:number, y:number},Array.<{start:{line:number, ch:number},end:{line:number, ch:number}}>} EditorViewState */ /* * returns the view state for the editor * @return {!EditorViewState} */ Editor.prototype.getViewState = function () { return { selections: this.getSelections(), scrollPos: this.getScrollPos() }; }; /* * Restores the view state * @param {!EditorViewState} viewState - the view state object to restore */ Editor.prototype.restoreViewState = function (viewState) { if (viewState.selection) { // We no longer write out single-selection, but there might be some view state // from an older version. this.setSelection(viewState.selection.start, viewState.selection.end); } if (viewState.selections) { this.setSelections(viewState.selections); } if (viewState.scrollPos) { this.setScrollPos(viewState.scrollPos.x, viewState.scrollPos.y); } }; /** * Re-renders the editor UI * @param {boolean=} handleResize true if this is in response to resizing the editor. Default false. */ Editor.prototype.refresh = function (handleResize) { // If focus is currently in a child of the CodeMirror editor (e.g. in an inline widget), but not in // the CodeMirror input field itself, remember the focused item so we can restore focus after the // refresh (which might cause the widget to be removed from the display list temporarily). var focusedItem = window.document.activeElement, restoreFocus = $.contains(this._codeMirror.getScrollerElement(), focusedItem); this._codeMirror.refresh(); if (restoreFocus) { focusedItem.focus(); } }; /** * Re-renders the editor, and all children inline editors. * @param {boolean=} handleResize true if this is in response to resizing the editor. Default false. */ Editor.prototype.refreshAll = function (handleResize) { this.refresh(handleResize); this.getInlineWidgets().forEach(function (inlineWidget) { inlineWidget.refresh(); }); }; /** Undo the last edit. */ Editor.prototype.undo = function () { this._codeMirror.undo(); }; /** Redo the last un-done edit. */ Editor.prototype.redo = function () { this._codeMirror.redo(); }; /** * View API Visibility Change Notification handler. This is also * called by the native "setVisible" API which refresh can be optimized * @param {boolean} show true to show the editor, false to hide it * @param {boolean} refresh true (default) to refresh the editor, false to skip refreshing it */ Editor.prototype.notifyVisibilityChange = function (show, refresh) { if (show && (refresh || refresh === undefined)) { this.refresh(); } if (show) { this._inlineWidgets.forEach(function (inlineWidget) { inlineWidget.onParentShown(); }); } }; /** * Shows or hides the editor within its parent. Does not force its ancestors to * become visible. * @param {boolean} show true to show the editor, false to hide it * @param {boolean} refresh true (default) to refresh the editor, false to skip refreshing it */ Editor.prototype.setVisible = function (show, refresh) { this.$el.css("display", (show ? "" : "none")); this.notifyVisibilityChange(show, refresh); }; /** * Returns true if the editor is fully visible--i.e., is in the DOM, all ancestors are * visible, and has a non-zero width/height. */ Editor.prototype.isFullyVisible = function () { return $(this.getRootElement()).is(":visible"); }; /** * Gets the syntax-highlighting mode for the given range. * Returns null if the mode at the start of the selection differs from the mode at the end - * an *approximation* of whether the mode is consistent across the whole range (a pattern like * A-B-A would return A as the mode, not null). * * @param {!{line: number, ch: number}} start The start of the range to check. * @param {!{line: number, ch: number}} end The end of the range to check. * @param {boolean=} knownMixed Whether we already know we're in a mixed mode and need to check both * the start and end. * @return {?(Object|string)} Name of syntax-highlighting mode, or object containing a "name" property * naming the mode along with configuration options required by the mode. * @see {@link LanguageManager::#getLanguageForPath} and {@link LanguageManager::Language#getMode}. */ Editor.prototype.getModeForRange = function (start, end, knownMixed) { var outerMode = this._codeMirror.getMode(), startMode = TokenUtils.getModeAt(this._codeMirror, start), endMode = TokenUtils.getModeAt(this._codeMirror, end); if (!knownMixed && outerMode.name === startMode.name) { // Mode does not vary: just use the editor-wide mode name return this._codeMirror.getOption("mode"); } else if (!startMode || !endMode || startMode.name !== endMode.name) { return null; } else { return startMode; } }; /** * Gets the syntax-highlighting mode for the current selection or cursor position. (The mode may * vary within one file due to embedded languages, e.g. JS embedded in an HTML script block). See * `getModeForRange()` for how this is determined for a single selection. * * If there are multiple selections, this will return a mode only if all the selections are individually * consistent and resolve to the same mode. * * @return {?(Object|string)} Name of syntax-highlighting mode, or object containing a "name" property * naming the mode along with configuration options required by the mode. * @see {@link LanguageManager::#getLanguageForPath} and {@link LanguageManager::Language#getMode}. */ Editor.prototype.getModeForSelection = function () { // Check for mixed mode info var self = this, sels = this.getSelections(), primarySel = this.getSelection(), outerMode = this._codeMirror.getMode(), startMode = TokenUtils.getModeAt(this._codeMirror, primarySel.start), isMixed = (outerMode.name !== startMode.name); if (isMixed) { // This is the magic code to let the code view know that we are in 'css' context // if the CodeMirror outermode is 'htmlmixed' and we are in 'style' attributes // value context. This has to be done as CodeMirror doesn't yet think this as 'css' // This magic is executed only when user is having a cursor and not selection // We will enable selection handling one we figure a way out to handle mixed scope selection if (outerMode.name === 'htmlmixed' && primarySel.start.line === primarySel.end.line && primarySel.start.ch === primarySel.end.ch) { var tagInfo = HTMLUtils.getTagInfo(this, primarySel.start, true), tokenType = tagInfo.position.tokenType; if (tokenType === HTMLUtils.ATTR_VALUE && tagInfo.attr.name.toLowerCase() === 'style') { return 'css'; } } // Shortcut the first check to avoid getModeAt(), which can be expensive if (primarySel.start.line !== primarySel.end.line || primarySel.start.ch !== primarySel.end.ch) { var endMode = TokenUtils.getModeAt(this._codeMirror, primarySel.end); if (startMode.name !== endMode.name) { return null; } } // If mixed mode, check that mode is the same at start & end of each selection var hasMixedSel = _.some(sels, function (sel) { if (sels === primarySel) { // We already checked this before, so we know it's not mixed. return false; } var rangeMode = self.getModeForRange(sel.start, sel.end, true); return (!rangeMode || rangeMode.name !== startMode.name); }); if (hasMixedSel) { return null; } return startMode.name; } else { // Mode does not vary: just use the editor-wide mode return this._codeMirror.getOption("mode"); } }; /* * gets the language for the selection. (Javascript selected from an HTML document or CSS selected from an HTML document, etc...) * @return {!Language} */ Editor.prototype.getLanguageForSelection = function () { return this.document.getLanguage().getLanguageForMode(this.getModeForSelection()); }; /** * Gets the syntax-highlighting mode for the document. * * @return {Object|String} Object or Name of syntax-highlighting mode * @see {@link LanguageManager::#getLanguageForPath|LanguageManager.getLanguageForPath} and {@link LanguageManager::Language#getMode|Language.getMode}. */ Editor.prototype.getModeForDocument = function () { return this._codeMirror.getOption("mode"); }; /** * The Document we're bound to * @type {!Document} */ Editor.prototype.document = null; /** * The Editor's last known width. * Used in conjunction with updateLayout to recompute the layout * if the parent container changes its size since our last layout update. * @type {?number} */ Editor.prototype._lastEditorWidth = null; /** * If true, we're in the middle of syncing to/from the Document. Used to ignore spurious change * events caused by us (vs. change events caused by others, which we need to pay attention to). * @type {!boolean} */ Editor.prototype._duringSync = false; /** * @private * NOTE: this is actually "semi-private": EditorManager also accesses this field... as well as * a few other modules. However, we should try to gradually move most code away from talking to * CodeMirror directly. * @type {!CodeMirror} */ Editor.prototype._codeMirror = null; /** * @private * @type {!Array.<{id:number, data:Object}>} */ Editor.prototype._inlineWidgets = null; /** * @private * @type {?TextRange} */ Editor.prototype._visibleRange = null; /** * @private * @type {Object} * Promise queues for inline widgets being added to a given line. */ Editor.prototype._inlineWidgetQueues = null; /** * @private * @type {Array} * A list of objects corresponding to the markers that are hiding lines in the current editor. */ Editor.prototype._hideMarks = null; /** * @private * * Retrieve the value of the named preference for this document. * * @param {string} prefName Name of preference to retrieve. * @return {*} current value of that pref */ Editor.prototype._getOption = function (prefName) { return PreferencesManager.get(prefName, PreferencesManager._buildContext(this.document.file.fullPath, this.document.getLanguage().getId())); }; /** * @private * * Updates the editor to the current value of prefName for the file being edited. * * @param {string} prefName Name of the preference to visibly update */ Editor.prototype._updateOption = function (prefName) { var oldValue = this._currentOptions[prefName], newValue = this._getOption(prefName); if (oldValue !== newValue) { this._currentOptions[prefName] = newValue; if (prefName === USE_TAB_CHAR) { this._codeMirror.setOption(cmOptions[prefName], newValue); this._codeMirror.setOption("indentUnit", newValue === true ? this._currentOptions[TAB_SIZE] : this._currentOptions[SPACE_UNITS] ); } else if (prefName === STYLE_ACTIVE_LINE) { this._updateStyleActiveLine(); } else if (prefName === SCROLL_PAST_END && this._visibleRange) { // Do not apply this option to inline editors return; } else if (prefName === SHOW_LINE_NUMBERS) { Editor._toggleLinePadding(!newValue); this._codeMirror.setOption(cmOptions[SHOW_LINE_NUMBERS], newValue); if (newValue) { Editor.registerGutter(LINE_NUMBER_GUTTER, LINE_NUMBER_GUTTER_PRIORITY); } else { Editor.unregisterGutter(LINE_NUMBER_GUTTER); } this.refreshAll(); } else { this._codeMirror.setOption(cmOptions[prefName], newValue); } this.trigger("optionChange", prefName, newValue); } }; /** * @private * * Used to ensure that "style active line" is turned off when there is a selection. */ Editor.prototype._updateStyleActiveLine = function () { if (this.hasSelection()) { if (this._codeMirror.getOption("styleActiveLine")) { this._codeMirror.setOption("styleActiveLine", false); } } else { this._codeMirror.setOption("styleActiveLine", this._currentOptions[STYLE_ACTIVE_LINE]); } }; /** * resizes the editor to fill its parent container * should not be used on inline editors * @param {boolean=} forceRefresh - forces the editor to update its layout * even if it already matches the container's height / width */ Editor.prototype.updateLayout = function (forceRefresh) { var curRoot = this.getRootElement(), curWidth = $(curRoot).width(), $editorHolder = this.$el.parent(), editorAreaHt = $editorHolder.height(); if (!curRoot.style.height || $(curRoot).height() !== editorAreaHt) { // Call setSize() instead of $.height() to allow CodeMirror to // check for options like line wrapping this.setSize(null, editorAreaHt); if (forceRefresh === undefined) { forceRefresh = true; } } else if (curWidth !== this._lastEditorWidth) { if (forceRefresh === undefined) { forceRefresh = true; } } this._lastEditorWidth = curWidth; if (forceRefresh) { this.refreshAll(forceRefresh); } }; /** * Clears all marks from the gutter with the specified name. * @param {string} name The name of the gutter to clear. */ Editor.prototype.clearGutter = function (name) { this._codeMirror.clearGutter(name); }; /** * Renders all registered gutters * @private */ Editor.prototype._renderGutters = function () { var languageId = this.document.getLanguage().getId(); function _filterByLanguages(gutter) { return !gutter.languages || gutter.languages.indexOf(languageId) > -1; } function _sortByPriority(a, b) { return a.priority - b.priority; } function _getName(gutter) { return gutter.name; } var gutters = registeredGutters.map(_getName), rootElement = this.getRootElement(); // If the line numbers gutter has not been explicitly registered and the CodeMirror lineNumbes option is // set to true, we explicitly add the line numbers gutter. This case occurs the first time the editor loads // and showLineNumbers is set to true in preferences if (gutters.indexOf(LINE_NUMBER_GUTTER) < 0 && this._codeMirror.getOption(cmOptions[SHOW_LINE_NUMBERS])) { registeredGutters.push({name: LINE_NUMBER_GUTTER, priority: LINE_NUMBER_GUTTER_PRIORITY}); } gutters = registeredGutters.sort(_sortByPriority) .filter(_filterByLanguages) .map(_getName); this._codeMirror.setOption("gutters", gutters); this._codeMirror.refresh(); if (gutters.indexOf(LINE_NUMBER_GUTTER) < 0) { $(rootElement).addClass("linenumber-disabled"); } else { $(rootElement).removeClass("linenumber-disabled"); } }; /** * Sets the marker for the specified gutter on the specified line number * @param {string} lineNumber The line number for the inserted gutter marker * @param {string} gutterName The name of the gutter * @param {object} marker The dom element representing the marker to the inserted in the gutter */ Editor.prototype.setGutterMarker = function (lineNumber, gutterName, marker) { var gutterNameRegistered = registeredGutters.some(function (gutter) { return gutter.name === gutterName; }); if (!gutterNameRegistered) { console.warn("Gutter name must be registered before calling editor.setGutterMarker"); return; } this._codeMirror.setGutterMarker(lineNumber, gutterName, marker); }; /** * Returns the list of gutters current registered on all editors. * @return {!Array.<{name: string, priority: number}>} */ Editor.getRegisteredGutters = function () { return registeredGutters; }; /** * Registers the gutter with the specified name at the given priority. * @param {string} name The name of the gutter. * @param {number} priority A number denoting the priority of the gutter. Priorities higher than LINE_NUMBER_GUTTER_PRIORITY appear after the line numbers. Priority less than LINE_NUMBER_GUTTER_PRIORITY appear before. * @param {?Array<string>} languageIds A list of language ids that this gutter is valid for. If no language ids are passed, then the gutter is valid in all languages. */ Editor.registerGutter = function (name, priority, languageIds) { if (isNaN(priority)) { console.warn("A non-numeric priority value was passed to registerGutter. The value will default to 0."); priority = 0; } if (!name || typeof name !== "string") { console.error("The name of the registered gutter must be a string."); return; } var gutter = {name: name, priority: priority, languages: languageIds}, gutterExists = registeredGutters.some(function (gutter) { return gutter.name === name; }); if (!gutterExists) { registeredGutters.push(gutter); } Editor.forEveryEditor(function (editor) { editor._renderGutters(); }); }; /** * Unregisters the gutter with the specified name and removes it from the UI. * @param {string} name The name of the gutter to be unregistered. */ Editor.unregisterGutter = function (name) { var i, gutter; registeredGutters = registeredGutters.filter(function (gutter) { return gutter.name !== name; }); Editor.forEveryEditor(function (editor) { editor._renderGutters(); }); }; // Global settings that affect Editor instances that share the same preference locations /** * Sets whether to use tab characters (vs. spaces) when inserting new text. * Affects any editors that share the same preference location. * @param {boolean} value * @param {string=} fullPath Path to file to get preference for * @return {boolean} true if value was valid */ Editor.setUseTabChar = function (value, fullPath) { var options = fullPath && {context: fullPath}; return PreferencesManager.set(USE_TAB_CHAR, value, options); }; /** * Gets whether the specified or current file uses tab characters (vs. spaces) when inserting new text * @param {string=} fullPath Path to file to get preference for * @return {boolean} */ Editor.getUseTabChar = function (fullPath) { return PreferencesManager.get(USE_TAB_CHAR, _buildPreferencesContext(fullPath)); }; /** * Sets tab character width. * Affects any editors that share the same preference location. * @param {number} value * @param {string=} fullPath Path to file to get preference for * @return {boolean} true if value was valid */ Editor.setTabSize = function (value, fullPath) { var options = fullPath && {context: fullPath}; return PreferencesManager.set(TAB_SIZE, value, options); }; /** * Get indent unit * @param {string=} fullPath Path to file to get preference for * @return {number} */ Editor.getTabSize = function (fullPath) { return PreferencesManager.get(TAB_SIZE, _buildPreferencesContext(fullPath)); }; /** * Sets indentation width. * Affects any editors that share the same preference location. * @param {number} value * @param {string=} fullPath Path to file to get preference for * @return {boolean} true if value was valid */ Editor.setSpaceUnits = function (value, fullPath) { var options = fullPath && {context: fullPath}; return PreferencesManager.set(SPACE_UNITS, value, options); }; /** * Get indentation width * @param {string=} fullPath Path to file to get preference for * @return {number} */ Editor.getSpaceUnits = function (fullPath) { return PreferencesManager.get(SPACE_UNITS, _buildPreferencesContext(fullPath)); }; /** * Sets the auto close brackets. * Affects any editors that share the same preference location. * @param {boolean} value * @param {string=} fullPath Path to file to get preference for * @return {boolean} true if value was valid */ Editor.setCloseBrackets = function (value, fullPath) { var options = fullPath && {context: fullPath}; return PreferencesManager.set(CLOSE_BRACKETS, value, options); }; /** * Gets whether the specified or current file uses auto close brackets * @param {string=} fullPath Path to file to get preference for * @return {boolean} */ Editor.getCloseBrackets = function (fullPath) { return PreferencesManager.get(CLOSE_BRACKETS, _buildPreferencesContext(fullPath)); }; /** * Sets show line numbers option. * Affects any editors that share the same preference location. * @param {boolean} value * @param {string=} fullPath Path to file to get preference for * @return {boolean} true if value was valid */ Editor.setShowLineNumbers = function (value, fullPath) { var options = fullPath && {context: fullPath}; return PreferencesManager.set(SHOW_LINE_NUMBERS, value, options); }; /** * Returns true if show line numbers is enabled for the specified or current file * @param {string=} fullPath Path to file to get preference for * @return {boolean} */ Editor.getShowLineNumbers = function (fullPath) { return PreferencesManager.get(SHOW_LINE_NUMBERS, _buildPreferencesContext(fullPath)); }; /** * Sets show active line option. * Affects any editors that share the same preference location. * @param {boolean} value * @param {string=} fullPath Path to file to get preference for * @return {boolean} true if value was valid */ Editor.setShowActiveLine = function (value, fullPath) { return PreferencesManager.set(STYLE_ACTIVE_LINE, value); }; /** * Returns true if show active line is enabled for the specified or current file * @param {string=} fullPath Path to file to get preference for * @return {boolean} */ Editor.getShowActiveLine = function (fullPath) { return PreferencesManager.get(STYLE_ACTIVE_LINE, _buildPreferencesContext(fullPath)); }; /** * Sets word wrap option. * Affects any editors that share the same preference location. * @param {boolean} value * @param {string=} fullPath Path to file to get preference for * @return {boolean} true if value was valid */ Editor.setWordWrap = function (value, fullPath) { var options = fullPath && {context: fullPath}; return PreferencesManager.set(WORD_WRAP, value, options); }; /** * Returns true if word wrap is enabled for the specified or current file * @param {string=} fullPath Path to file to get preference for * @return {boolean} */ Editor.getWordWrap = function (fullPath) { return PreferencesManager.get(WORD_WRAP, _buildPreferencesContext(fullPath)); }; /** * Sets indentLineComment option. * Affects any editors that share the same preference location. * @param {boolean} value * @param {string=} fullPath Path to file to get preference for * @return {boolean} true if value was valid */ Editor.setIndentLineComment = function (value, fullPath) { var options = fullPath && {context: fullPath}; return PreferencesManager.set(INDENT_LINE_COMMENT, value, options); }; /** * Returns true if indentLineComment is enabled for the specified or current file * @param {string=} fullPath Path to file to get preference for * @return {boolean} */ Editor.getIndentLineComment = function (fullPath) { return PreferencesManager.get(INDENT_LINE_COMMENT, _buildPreferencesContext(fullPath)); }; /** * Runs callback for every Editor instance that currently exists * @param {!function(!Editor)} callback */ Editor.forEveryEditor = function (callback) { _instances.forEach(callback); }; /** * @private * Toggles the left padding of all code editors. Used to provide more * space between the code text and the left edge of the editor when * line numbers are hidden. * @param {boolean} showLinePadding */ Editor._toggleLinePadding = function (showLinePadding) { // apply class to all pane DOM nodes var $holders = []; _instances.forEach(function (editor) { var $editorHolder = editor.$el.parent(); if ($holders.indexOf($editorHolder) === -1) { $holders.push($editorHolder); } }); _.each($holders, function ($holder) { $holder.toggleClass("show-line-padding", Boolean(showLinePadding)); }); }; Editor.LINE_NUMBER_GUTTER_PRIORITY = LINE_NUMBER_GUTTER_PRIORITY; Editor.CODE_FOLDING_GUTTER_PRIORITY = CODE_FOLDING_GUTTER_PRIORITY; // Set up listeners for preference changes editorOptions.forEach(function (prefName) { PreferencesManager.on("change", prefName, function () { _instances.forEach(function (editor) { editor._updateOption(prefName); }); }); }); // Define public API exports.Editor = Editor; exports.BOUNDARY_CHECK_NORMAL = BOUNDARY_CHECK_NORMAL; exports.BOUNDARY_IGNORE_TOP = BOUNDARY_IGNORE_TOP; });