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:
(BracketsEvent, Editor, KeyboardEvent)
. The 3nd arg is the
raw DOM event. Note: most listeners will only want to listen for "keypress".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);
Constant: ignore upper boundary when centering text
var BOUNDARY_CHECK_NORMAL = 0,
BOUNDARY_IGNORE_TOP = 1;
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";
Constants
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);
Guard flag to prevent focus() reentrancy (via blur handlers), even across Editors
var _duringFocus = false;
Helper function to build preferences context based on the full path of the file.
function _buildPreferencesContext(fullPath) {
return PreferencesManager._buildContext(fullPath,
fullPath ? LanguageManager.getLanguageForPath(fullPath).getId() : undefined);
}
Helper functions to check options.
function _checkTopBoundary(options) {
return (options !== BOUNDARY_IGNORE_TOP);
}
function _checkBottomBoundary(options) {
return true;
}
function _copyPos(pos) {
return new CodeMirror.Pos(pos.line, pos.ch);
}
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};
}
}
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.
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);
}
};
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).
Editor.prototype._duringSync = false;
Editor.prototype._inlineWidgets = null;
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);
};
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();
});
};
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);
}
};
Editor.prototype._getInlineWidgetLineNumber = function (inlineWidget) {
return this._codeMirror.getLineNumber(inlineWidget.info.line);
};
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.
Editor.prototype._getLineSpaceElement = function () {
return $(".CodeMirror-lines", this.getScrollerElement()).children().get(0);
};
Determine the mode to use from the document's language Uses "text/plain" if the language does not define a mode
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";
};
Editor.prototype._getOption = function (prefName) {
return PreferencesManager.get(prefName, PreferencesManager._buildContext(this.document.file.fullPath, this.document.getLanguage().getId()));
};
Editor.prototype._handleCursorActivity = function (event) {
this._updateStyleActiveLine();
};
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:
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());
};
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:
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);
};
Editor.prototype._handleKeypressEvents = function (event) {
var keyStr = String.fromCharCode(event.which || event.keyCode);
if (/[\]\{\}\)]/.test(keyStr)) {
this._handleWhitespaceForElectricChars();
}
};
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");
};
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;
}
};
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);
};
Hides the specified line number in the editor
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;
};
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";
});
};
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.
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.
Editor.prototype._removeInlineWidgetInternal = function (inlineWidget) {
if (!inlineWidget.isClosed) {
this._removeInlineWidgetFromList(inlineWidget);
inlineWidget.onClosed();
inlineWidget.isClosed = true;
}
};
Renders all registered gutters
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 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.
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);
};
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;
});
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();
};
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);
}
};
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]);
}
};
Adds an inline widget below the given line. If any inline widget was already open for that line, it is closed without warning.
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();
};
Restores and adjusts the current scroll position of the editor.
Editor.prototype.adjustScrollPos = function (scrollPos, heightDelta) {
this._codeMirror.scrollTo(scrollPos.x, scrollPos.y + heightDelta);
};
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.
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);
}
};
Clears all marks from the gutter with the specified name.
Editor.prototype.clearGutter = function (name) {
this._codeMirror.clearGutter(name);
};
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.
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;
};
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);
});
};
Display temporary popover message at current cursor position. Display message above cursor if space allows, otherwise below.
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);
}
});
};
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;
}
};
Runs callback for every Editor instance that currently exists
Editor.forEveryEditor = function (callback) {
_instances.forEach(callback);
};
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.
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;
};
Gets whether the specified or current file uses auto close brackets
Editor.getCloseBrackets = function (fullPath) {
return PreferencesManager.get(CLOSE_BRACKETS, _buildPreferencesContext(fullPath));
};
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.
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;
};
Gets the current cursor position within the editor.
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;
};
Gets the file associated with this editor This is a required Pane-View interface method
Editor.prototype.getFile = function () {
return this.document.file;
};
Gets the number of the first visible line in the editor.
Editor.prototype.getFirstVisibleLine = function () {
return (this._visibleRange ? this._visibleRange.startLine : 0);
};
Returns the currently focused inline widget, if any.
Editor.prototype.getFocusedInlineWidget = function () {
var result = null;
this.getInlineWidgets().forEach(function (widget) {
if (widget.hasFocus()) {
result = widget;
}
});
return result;
};
Returns true if indentLineComment is enabled for the specified or current file
Editor.getIndentLineComment = function (fullPath) {
return PreferencesManager.get(INDENT_LINE_COMMENT, _buildPreferencesContext(fullPath));
};
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().
Editor.prototype.getInlineWidgets = function () {
return this._inlineWidgets;
};
gets the language for the selection. (Javascript selected from an HTML document or CSS selected from an HTML document, etc...)
Editor.prototype.getLanguageForSelection = function () {
return this.document.getLanguage().getLanguageForMode(this.getModeForSelection());
};
Gets the number of the last visible line in the editor.
Editor.prototype.getLastVisibleLine = function () {
return (this._visibleRange ? this._visibleRange.endLine : this.lineCount() - 1);
};
Gets the syntax-highlighting mode for the document.
Editor.prototype.getModeForDocument = function () {
return this._codeMirror.getOption("mode");
};
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).
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.
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");
}
};
Returns the list of gutters current registered on all editors.
Editor.getRegisteredGutters = function () {
return registeredGutters;
};
Gets the root DOM node of the editor.
Editor.prototype.getRootElement = function () {
return this._codeMirror.getWrapperElement();
};
Returns the current scroll position of the editor.
Editor.prototype.getScrollPos = function () {
var scrollInfo = this._codeMirror.getScrollInfo();
return { x: scrollInfo.left, y: scrollInfo.top };
};
Gets the scroller element from the editor.
Editor.prototype.getScrollerElement = function () {
return this._codeMirror.getScrollerElement();
};
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.
Editor.prototype.getSelectedText = function (allSelections) {
if (allSelections) {
return this._codeMirror.getSelection();
} else {
var sel = this.getSelection();
return this.document.getRange(sel.start, sel.end);
}
};
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.
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.
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;
});
};
Returns true if show active line is enabled for the specified or current file
Editor.getShowActiveLine = function (fullPath) {
return PreferencesManager.get(STYLE_ACTIVE_LINE, _buildPreferencesContext(fullPath));
};
Returns true if show line numbers is enabled for the specified or current file
Editor.getShowLineNumbers = function (fullPath) {
return PreferencesManager.get(SHOW_LINE_NUMBERS, _buildPreferencesContext(fullPath));
};
Get indentation width
Editor.getSpaceUnits = function (fullPath) {
return PreferencesManager.get(SPACE_UNITS, _buildPreferencesContext(fullPath));
};
Get indent unit
Editor.getTabSize = function (fullPath) {
return PreferencesManager.get(TAB_SIZE, _buildPreferencesContext(fullPath));
};
Returns the current text height of the editor.
Editor.prototype.getTextHeight = function () {
return this._codeMirror.defaultTextHeight();
};
Gets whether the specified or current file uses tab characters (vs. spaces) when inserting new text
Editor.getUseTabChar = function (fullPath) {
return PreferencesManager.get(USE_TAB_CHAR, _buildPreferencesContext(fullPath));
};
returns the view state for the editor
Editor.prototype.getViewState = function () {
return {
selections: this.getSelections(),
scrollPos: this.getScrollPos()
};
};
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).
Editor.prototype.getVirtualScrollAreaTop = function () {
var topPadding = this._getLineSpaceElement().offsetTop, // padding within mover
scroller = this.getScrollerElement();
return $(scroller).offset().top - scroller.scrollTop + topPadding;
};
Returns true if word wrap is enabled for the specified or current file
Editor.getWordWrap = function (fullPath) {
return PreferencesManager.get(WORD_WRAP, _buildPreferencesContext(fullPath));
};
Returns true if the editor has focus
Editor.prototype.hasFocus = function () {
return this._focused;
};
Editor.prototype.hasSelection = function () {
return this._codeMirror.somethingSelected();
};
Given a position, returns its index within the text (assuming \n newlines)
Editor.prototype.indexFromPos = function (coords) {
return this._codeMirror.indexFromPos(coords);
};
Editor.prototype.posFromIndex = function (index) {
return this._codeMirror.posFromIndex(index);
};
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");
};
Deterines if line is fully visible.
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);
};
Editor.prototype.isTextSubset = function () {
return Boolean(this._visibleRange);
};
Gets the total number of lines in the document (includes lines not visible in the viewport)
Editor.prototype.lineCount = function () {
return this._codeMirror.lineCount();
};
View API Visibility Change Notification handler. This is also called by the native "setVisible" API which refresh can be optimized
Editor.prototype.notifyVisibilityChange = function (show, refresh) {
if (show && (refresh || refresh === undefined)) {
this.refresh();
}
if (show) {
this._inlineWidgets.forEach(function (inlineWidget) {
inlineWidget.onParentShown();
});
}
};
Returns true if pos is between start and end (INclusive at start; EXclusive at end by default, but overridable via the endInclusive flag).
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;
};
Re-renders the editor UI
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.
Editor.prototype.refreshAll = function (handleResize) {
this.refresh(handleResize);
this.getInlineWidgets().forEach(function (inlineWidget) {
inlineWidget.refresh();
});
};
Registers the gutter with the specified name at the given priority.
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();
});
};
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 all inline widgets for a given line
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();
}
};
Removes the given inline widget.
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;
};
Restores the view state
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);
}
};
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");
});
};
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.
Editor.prototype.selectWordAt = function (pos) {
var word = this._codeMirror.findWordAt(pos);
this.setSelection(word.anchor, word.head);
};
Sets the auto close brackets. Affects any editors that share the same preference location.
Editor.setCloseBrackets = function (value, fullPath) {
var options = fullPath && {context: fullPath};
return PreferencesManager.set(CLOSE_BRACKETS, value, options);
};
Sets the cursor position within the editor. Removes any selection.
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();
}
};
Sets the marker for the specified gutter on the specified line number
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);
};
Sets indentLineComment option. Affects any editors that share the same preference location.
Editor.setIndentLineComment = function (value, fullPath) {
var options = fullPath && {context: fullPath};
return PreferencesManager.set(INDENT_LINE_COMMENT, value, options);
};
Sets the height of an inline widget in this editor.
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
});
}
};
Sets the current scroll position of the editor.
Editor.prototype.setScrollPos = function (x, y) {
this._codeMirror.scrollTo(x, y);
};
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
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.
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 show active line option. Affects any editors that share the same preference location.
Editor.setShowActiveLine = function (value, fullPath) {
return PreferencesManager.set(STYLE_ACTIVE_LINE, value);
};
Sets show line numbers option. Affects any editors that share the same preference location.
Editor.setShowLineNumbers = function (value, fullPath) {
var options = fullPath && {context: fullPath};
return PreferencesManager.set(SHOW_LINE_NUMBERS, value, options);
};
Set the editor size in pixels or percentage
Editor.prototype.setSize = function (width, height) {
this._codeMirror.setSize(width, height);
};
Sets indentation width. Affects any editors that share the same preference location.
Editor.setSpaceUnits = function (value, fullPath) {
var options = fullPath && {context: fullPath};
return PreferencesManager.set(SPACE_UNITS, value, options);
};
Sets tab character width. Affects any editors that share the same preference location.
Editor.setTabSize = function (value, fullPath) {
var options = fullPath && {context: fullPath};
return PreferencesManager.set(TAB_SIZE, value, options);
};
Sets whether to use tab characters (vs. spaces) when inserting new text. Affects any editors that share the same preference location.
Editor.setUseTabChar = function (value, fullPath) {
var options = fullPath && {context: fullPath};
return PreferencesManager.set(USE_TAB_CHAR, value, options);
};
Shows or hides the editor within its parent. Does not force its ancestors to become visible.
Editor.prototype.setVisible = function (show, refresh) {
this.$el.css("display", (show ? "" : "none"));
this.notifyVisibilityChange(show, refresh);
};
Sets word wrap option. Affects any editors that share the same preference location.
Editor.setWordWrap = function (value, fullPath) {
var options = fullPath && {context: fullPath};
return PreferencesManager.set(WORD_WRAP, value, options);
};
Sets the editors overwrite mode state. If null is passed, the state is toggled.
Editor.prototype.toggleOverwrite = function (state) {
this._codeMirror.toggleOverwrite(state);
};
Gets the total height of the document in pixels (not the viewport)
Editor.prototype.totalHeight = function () {
return this.getScrollerElement().scrollHeight;
};
Unregisters the gutter with the specified name and removes it from the UI.
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
resizes the editor to fill its parent container should not be used on inline editors
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);
}
};