/* vim:set ts=2 sw=2 sts=2 et tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const Cu = Components.utils; const Ci = Components.interfaces; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource:///modules/devtools/sourceeditor/source-editor-ui.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); const ORION_SCRIPT = "chrome://browser/content/devtools/orion.js"; const ORION_IFRAME = "data:text/html;charset=utf8," + "" + "
" + "" + "" + ""; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; /** * Maximum allowed vertical offset for the line index when you call * SourceEditor.setCaretPosition(). * * @type number */ const VERTICAL_OFFSET = 3; /** * The primary selection update delay. On Linux, the X11 primary selection is * updated to hold the currently selected text. * * @type number */ const PRIMARY_SELECTION_DELAY = 100; /** * Predefined themes for syntax highlighting. This objects maps * SourceEditor.THEMES to Orion CSS files. */ const ORION_THEMES = { mozilla: ["chrome://browser/skin/devtools/orion.css"], }; /** * Known Orion editor events you can listen for. This object maps several of the * SourceEditor.EVENTS to Orion events. */ const ORION_EVENTS = { ContextMenu: "ContextMenu", TextChanged: "ModelChanged", Selection: "Selection", Focus: "Focus", Blur: "Blur", MouseOver: "MouseOver", MouseOut: "MouseOut", MouseMove: "MouseMove", }; /** * Known Orion annotation types. */ const ORION_ANNOTATION_TYPES = { currentBracket: "orion.annotation.currentBracket", matchingBracket: "orion.annotation.matchingBracket", breakpoint: "orion.annotation.breakpoint", task: "orion.annotation.task", currentLine: "orion.annotation.currentLine", debugLocation: "mozilla.annotation.debugLocation", }; /** * Default key bindings in the Orion editor. */ const DEFAULT_KEYBINDINGS = [ { action: "enter", code: Ci.nsIDOMKeyEvent.DOM_VK_ENTER, }, { action: "undo", code: Ci.nsIDOMKeyEvent.DOM_VK_Z, accel: true, }, { action: "redo", code: Ci.nsIDOMKeyEvent.DOM_VK_Z, accel: true, shift: true, }, { action: "Unindent Lines", code: Ci.nsIDOMKeyEvent.DOM_VK_TAB, shift: true, }, { action: "Move Lines Up", code: Ci.nsIDOMKeyEvent.DOM_VK_UP, ctrl: Services.appinfo.OS == "Darwin", alt: true, }, { action: "Move Lines Down", code: Ci.nsIDOMKeyEvent.DOM_VK_DOWN, ctrl: Services.appinfo.OS == "Darwin", alt: true, }, { action: "Comment/Uncomment", code: Ci.nsIDOMKeyEvent.DOM_VK_SLASH, accel: true, }, { action: "Move to Bracket Opening", code: Ci.nsIDOMKeyEvent.DOM_VK_OPEN_BRACKET, accel: true, alt: true, }, { action: "Move to Bracket Closing", code: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET, accel: true, alt: true, }, ]; if (Services.appinfo.OS == "WINNT" || Services.appinfo.OS == "Linux") { DEFAULT_KEYBINDINGS.push({ action: "redo", code: Ci.nsIDOMKeyEvent.DOM_VK_Y, accel: true, }); } this.EXPORTED_SYMBOLS = ["SourceEditor"]; /** * The SourceEditor object constructor. The SourceEditor component allows you to * provide users with an editor tailored to the specific needs of editing source * code, aimed primarily at web developers. * * The editor used here is Eclipse Orion (see http://www.eclipse.org/orion). * * @constructor */ this.SourceEditor = function SourceEditor() { // Update the SourceEditor defaults from user preferences. SourceEditor.DEFAULTS.tabSize = Services.prefs.getIntPref(SourceEditor.PREFS.TAB_SIZE); SourceEditor.DEFAULTS.expandTab = Services.prefs.getBoolPref(SourceEditor.PREFS.EXPAND_TAB); this._onOrionSelection = this._onOrionSelection.bind(this); this._onTextChanged = this._onTextChanged.bind(this); this._onOrionContextMenu = this._onOrionContextMenu.bind(this); this._eventTarget = {}; this._eventListenersQueue = []; this.ui = new SourceEditorUI(this); } SourceEditor.prototype = { _view: null, _iframe: null, _model: null, _undoStack: null, _linesRuler: null, _annotationRuler: null, _overviewRuler: null, _styler: null, _annotationStyler: null, _annotationModel: null, _dragAndDrop: null, _currentLineAnnotation: null, _primarySelectionTimeout: null, _mode: null, _expandTab: null, _tabSize: null, _iframeWindow: null, _eventTarget: null, _eventListenersQueue: null, _contextMenu: null, _dirty: false, /** * The Source Editor user interface manager. * @type object * An instance of the SourceEditorUI. */ ui: null, /** * The editor container element. * @type nsIDOMElement */ parentElement: null, /** * Initialize the editor. * * @param nsIDOMElement aElement * The DOM element where you want the editor to show. * @param object aConfig * Editor configuration object. See SourceEditor.DEFAULTS for the * available configuration options. * @param function [aCallback] * Function you want to execute once the editor is loaded and * initialized. * @see SourceEditor.DEFAULTS */ init: function SE_init(aElement, aConfig, aCallback) { if (this._iframe) { throw new Error("SourceEditor is already initialized!"); } let doc = aElement.ownerDocument; this._iframe = doc.createElementNS(XUL_NS, "iframe"); this._iframe.flex = 1; let onIframeLoad = (function() { this._iframe.removeEventListener("load", onIframeLoad, true); this._onIframeLoad(); }).bind(this); this._iframe.addEventListener("load", onIframeLoad, true); this._iframe.setAttribute("src", ORION_IFRAME); aElement.appendChild(this._iframe); this.parentElement = aElement; this._config = {}; for (let key in SourceEditor.DEFAULTS) { this._config[key] = key in aConfig ? aConfig[key] : SourceEditor.DEFAULTS[key]; } // TODO: Bug 725677 - Remove the deprecated placeholderText option from the // Source Editor initialization. if (aConfig.placeholderText) { this._config.initialText = aConfig.placeholderText; Services.console.logStringMessage("SourceEditor.init() was called with the placeholderText option which is deprecated, please use initialText."); } this._onReadyCallback = aCallback; this.ui.init(); }, /** * The editor iframe load event handler. * @private */ _onIframeLoad: function SE__onIframeLoad() { this._iframeWindow = this._iframe.contentWindow.wrappedJSObject; let window = this._iframeWindow; let config = this._config; Services.scriptloader.loadSubScript(ORION_SCRIPT, window, "utf8"); let TextModel = window.require("orion/textview/textModel").TextModel; let TextView = window.require("orion/textview/textView").TextView; this._expandTab = config.expandTab; this._tabSize = config.tabSize; let theme = config.theme; let stylesheet = theme in ORION_THEMES ? ORION_THEMES[theme] : theme; this._model = new TextModel(config.initialText); this._view = new TextView({ model: this._model, parent: "editor", stylesheet: stylesheet, tabSize: this._tabSize, expandTab: this._expandTab, readonly: config.readOnly, themeClass: "mozilla" + (config.readOnly ? " readonly" : ""), }); let onOrionLoad = function() { this._view.removeEventListener("Load", onOrionLoad); this._onOrionLoad(); }.bind(this); this._view.addEventListener("Load", onOrionLoad); if (config.highlightCurrentLine || Services.appinfo.OS == "Linux") { this.addEventListener(SourceEditor.EVENTS.SELECTION, this._onOrionSelection); } this.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, this._onTextChanged); if (typeof config.contextMenu == "string") { let chromeDocument = this.parentElement.ownerDocument; this._contextMenu = chromeDocument.getElementById(config.contextMenu); } else if (typeof config.contextMenu == "object" ) { this._contextMenu = config._contextMenu; } if (this._contextMenu) { this.addEventListener(SourceEditor.EVENTS.CONTEXT_MENU, this._onOrionContextMenu); } let KeyBinding = window.require("orion/textview/keyBinding").KeyBinding; let TextDND = window.require("orion/textview/textDND").TextDND; let Rulers = window.require("orion/textview/rulers"); let LineNumberRuler = Rulers.LineNumberRuler; let AnnotationRuler = Rulers.AnnotationRuler; let OverviewRuler = Rulers.OverviewRuler; let UndoStack = window.require("orion/textview/undoStack").UndoStack; let AnnotationModel = window.require("orion/textview/annotations").AnnotationModel; this._annotationModel = new AnnotationModel(this._model); if (config.showAnnotationRuler) { this._annotationRuler = new AnnotationRuler(this._annotationModel, "left", {styleClass: "ruler annotations"}); this._annotationRuler.onClick = this._annotationRulerClick.bind(this); this._annotationRuler.addAnnotationType(ORION_ANNOTATION_TYPES.breakpoint); this._annotationRuler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation); this._view.addRuler(this._annotationRuler); } if (config.showLineNumbers) { let rulerClass = this._annotationRuler ? "ruler lines linesWithAnnotations" : "ruler lines"; this._linesRuler = new LineNumberRuler(this._annotationModel, "left", {styleClass: rulerClass}, {styleClass: "rulerLines odd"}, {styleClass: "rulerLines even"}); this._linesRuler.onClick = this._linesRulerClick.bind(this); this._linesRuler.onDblClick = this._linesRulerDblClick.bind(this); this._view.addRuler(this._linesRuler); } if (config.showOverviewRuler) { this._overviewRuler = new OverviewRuler(this._annotationModel, "right", {styleClass: "ruler overview"}); this._overviewRuler.onClick = this._overviewRulerClick.bind(this); this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.matchingBracket); this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.currentBracket); this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.breakpoint); this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation); this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.task); this._view.addRuler(this._overviewRuler); } this.setMode(config.mode); this._undoStack = new UndoStack(this._view, config.undoLimit); this._dragAndDrop = new TextDND(this._view, this._undoStack); let actions = { "undo": [this.undo, this], "redo": [this.redo, this], "tab": [this._doTab, this], "Unindent Lines": [this._doUnindentLines, this], "enter": [this._doEnter, this], "Find...": [this.ui.find, this.ui], "Find Next Occurrence": [this.ui.findNext, this.ui], "Find Previous Occurrence": [this.ui.findPrevious, this.ui], "Goto Line...": [this.ui.gotoLine, this.ui], "Move Lines Down": [this._moveLines, this], "Comment/Uncomment": [this._doCommentUncomment, this], "Move to Bracket Opening": [this._moveToBracketOpening, this], "Move to Bracket Closing": [this._moveToBracketClosing, this], }; for (let name in actions) { let action = actions[name]; this._view.setAction(name, action[0].bind(action[1])); } this._view.setAction("Move Lines Up", this._moveLines.bind(this, true)); let keys = (config.keys || []).concat(DEFAULT_KEYBINDINGS); keys.forEach(function(aKey) { // In Orion mod1 refers to Cmd on Macs and Ctrl on Windows and Linux. // So, if ctrl is in aKey we use it on Windows and Linux, otherwise // we use aKey.accel for mod1. let mod1 = Services.appinfo.OS != "Darwin" && "ctrl" in aKey ? aKey.ctrl : aKey.accel; let binding = new KeyBinding(aKey.code, mod1, aKey.shift, aKey.alt, aKey.ctrl); this._view.setKeyBinding(binding, aKey.action); if (aKey.callback) { this._view.setAction(aKey.action, aKey.callback); } }, this); this._initEventTarget(); }, /** * Initialize the private Orion EventTarget object. This is used for tracking * our own event listeners for events outside of Orion's scope. * @private */ _initEventTarget: function SE__initEventTarget() { let EventTarget = this._iframeWindow.require("orion/textview/eventTarget").EventTarget; EventTarget.addMixin(this._eventTarget); this._eventListenersQueue.forEach(function(aRequest) { if (aRequest[0] == "add") { this.addEventListener(aRequest[1], aRequest[2]); } else { this.removeEventListener(aRequest[1], aRequest[2]); } }, this); this._eventListenersQueue = []; }, /** * Dispatch an event to the SourceEditor event listeners. This covers only the * SourceEditor-specific events. * * @private * @param object aEvent * The event object to dispatch to all listeners. */ _dispatchEvent: function SE__dispatchEvent(aEvent) { this._eventTarget.dispatchEvent(aEvent); }, /** * The Orion "Load" event handler. This is called when the Orion editor * completes the initialization. * @private */ _onOrionLoad: function SE__onOrionLoad() { this.ui.onReady(); if (this._onReadyCallback) { this._onReadyCallback(this); this._onReadyCallback = null; } }, /** * The "tab" editor action implementation. This adds support for expanded tabs * to spaces, and support for the indentation of multiple lines at once. * @private */ _doTab: function SE__doTab() { if (this.readOnly) { return false; } let indent = "\t"; let selection = this.getSelection(); let model = this._model; let firstLine = model.getLineAtOffset(selection.start); let firstLineStart = this.getLineStart(firstLine); let lastLineOffset = selection.end > selection.start ? selection.end - 1 : selection.end; let lastLine = model.getLineAtOffset(lastLineOffset); if (this._expandTab) { let offsetFromLineStart = firstLine == lastLine ? selection.start - firstLineStart : 0; let spaces = this._tabSize - (offsetFromLineStart % this._tabSize); indent = (new Array(spaces + 1)).join(" "); } // Do selection indentation. if (firstLine != lastLine) { let lines = [""]; let lastLineEnd = this.getLineEnd(lastLine, true); let selectedLines = lastLine - firstLine + 1; for (let i = firstLine; i <= lastLine; i++) { lines.push(model.getLine(i, true)); } this.startCompoundChange(); this.setText(lines.join(indent), firstLineStart, lastLineEnd); let newSelectionStart = firstLineStart == selection.start ? selection.start : selection.start + indent.length; let newSelectionEnd = selection.end + (selectedLines * indent.length); this._view.setSelection(newSelectionStart, newSelectionEnd); this.endCompoundChange(); return true; } return false; }, /** * The "Unindent lines" editor action implementation. This method is invoked * when the user presses Shift-Tab. * @private */ _doUnindentLines: function SE__doUnindentLines() { if (this.readOnly) { return true; } let indent = "\t"; let selection = this.getSelection(); let model = this._model; let firstLine = model.getLineAtOffset(selection.start); let lastLineOffset = selection.end > selection.start ? selection.end - 1 : selection.end; let lastLine = model.getLineAtOffset(lastLineOffset); if (this._expandTab) { indent = (new Array(this._tabSize + 1)).join(" "); } let lines = []; for (let line, i = firstLine; i <= lastLine; i++) { line = model.getLine(i, true); if (line.indexOf(indent) != 0) { return true; } lines.push(line.substring(indent.length)); } let firstLineStart = this.getLineStart(firstLine); let lastLineStart = this.getLineStart(lastLine); let lastLineEnd = this.getLineEnd(lastLine, true); this.startCompoundChange(); this.setText(lines.join(""), firstLineStart, lastLineEnd); let selectedLines = lastLine - firstLine + 1; let newSelectionStart = firstLineStart == selection.start ? selection.start : Math.max(firstLineStart, selection.start - indent.length); let newSelectionEnd = selection.end - (selectedLines * indent.length) + (selection.end == lastLineStart + 1 ? 1 : 0); if (firstLine == lastLine) { newSelectionEnd = Math.max(lastLineStart, newSelectionEnd); } this._view.setSelection(newSelectionStart, newSelectionEnd); this.endCompoundChange(); return true; }, /** * The editor Enter action implementation, which adds simple automatic * indentation based on the previous line when the user presses the Enter key. * @private */ _doEnter: function SE__doEnter() { if (this.readOnly) { return false; } let selection = this.getSelection(); if (selection.start != selection.end) { return false; } let model = this._model; let lineIndex = model.getLineAtOffset(selection.start); let lineText = model.getLine(lineIndex, true); let lineStart = this.getLineStart(lineIndex); let index = 0; let lineOffset = selection.start - lineStart; while (index < lineOffset && /[ \t]/.test(lineText.charAt(index))) { index++; } if (!index) { return false; } let prefix = lineText.substring(0, index); index = lineOffset; while (index < lineText.length && /[ \t]/.test(lineText.charAt(index++))) { selection.end++; } this.setText(this.getLineDelimiter() + prefix, selection.start, selection.end); return true; }, /** * Move lines upwards or downwards, relative to the current caret location. * * @private * @param boolean aLineAbove * True if moving lines up, false to move lines down. */ _moveLines: function SE__moveLines(aLineAbove) { if (this.readOnly) { return false; } let model = this._model; let selection = this.getSelection(); let firstLine = model.getLineAtOffset(selection.start); if (firstLine == 0 && aLineAbove) { return true; } let lastLine = model.getLineAtOffset(selection.end); let firstLineStart = this.getLineStart(firstLine); let lastLineStart = this.getLineStart(lastLine); if (selection.start != selection.end && lastLineStart == selection.end) { lastLine--; } if (!aLineAbove && (lastLine + 1) == this.getLineCount()) { return true; } let lastLineEnd = this.getLineEnd(lastLine, true); let text = this.getText(firstLineStart, lastLineEnd); if (aLineAbove) { let aboveLine = firstLine - 1; let aboveLineStart = this.getLineStart(aboveLine); this.startCompoundChange(); if (lastLine == (this.getLineCount() - 1)) { let delimiterStart = this.getLineEnd(aboveLine); let delimiterEnd = this.getLineEnd(aboveLine, true); let lineDelimiter = this.getText(delimiterStart, delimiterEnd); text += lineDelimiter; this.setText("", firstLineStart - lineDelimiter.length, lastLineEnd); } else { this.setText("", firstLineStart, lastLineEnd); } this.setText(text, aboveLineStart, aboveLineStart); this.endCompoundChange(); this.setSelection(aboveLineStart, aboveLineStart + text.length); } else { let belowLine = lastLine + 1; let belowLineEnd = this.getLineEnd(belowLine, true); let insertAt = belowLineEnd - lastLineEnd + firstLineStart; let lineDelimiter = ""; if (belowLine == this.getLineCount() - 1) { let delimiterStart = this.getLineEnd(lastLine); lineDelimiter = this.getText(delimiterStart, lastLineEnd); text = lineDelimiter + text.substr(0, text.length - lineDelimiter.length); } this.startCompoundChange(); this.setText("", firstLineStart, lastLineEnd); this.setText(text, insertAt, insertAt); this.endCompoundChange(); this.setSelection(insertAt + lineDelimiter.length, insertAt + text.length); } return true; }, /** * The Orion Selection event handler. The current caret line is * highlighted and for Linux users the selected text is copied into the X11 * PRIMARY buffer. * * @private * @param object aEvent * The Orion Selection event object. */ _onOrionSelection: function SE__onOrionSelection(aEvent) { if (this._config.highlightCurrentLine) { this._highlightCurrentLine(aEvent); } if (Services.appinfo.OS == "Linux") { let window = this.parentElement.ownerDocument.defaultView; if (this._primarySelectionTimeout) { window.clearTimeout(this._primarySelectionTimeout); } this._primarySelectionTimeout = window.setTimeout(this._updatePrimarySelection.bind(this), PRIMARY_SELECTION_DELAY); } }, /** * The TextChanged event handler which tracks the dirty state of the editor. * * @see SourceEditor.EVENTS.TEXT_CHANGED * @see SourceEditor.EVENTS.DIRTY_CHANGED * @see SourceEditor.dirty * @private */ _onTextChanged: function SE__onTextChanged() { this._updateDirty(); }, /** * The Orion contextmenu event handler. This method opens the default or * the custom context menu popup at the pointer location. * * @param object aEvent * The contextmenu event object coming from Orion. This object should * hold the screenX and screenY properties. */ _onOrionContextMenu: function SE__onOrionContextMenu(aEvent) { if (this._contextMenu.state == "closed") { this._contextMenu.openPopupAtScreen(aEvent.screenX || 0, aEvent.screenY || 0, true); } }, /** * Update the dirty state of the editor based on the undo stack. * @private */ _updateDirty: function SE__updateDirty() { this.dirty = !this._undoStack.isClean(); }, /** * Update the X11 PRIMARY buffer to hold the current selection. * @private */ _updatePrimarySelection: function SE__updatePrimarySelection() { this._primarySelectionTimeout = null; let text = this.getSelectedText(); if (!text) { return; } clipboardHelper.copyStringToClipboard(text, Ci.nsIClipboard.kSelectionClipboard, this.parentElement.ownerDocument); }, /** * Highlight the current line using the Orion annotation model. * * @private * @param object aEvent * The Selection event object. */ _highlightCurrentLine: function SE__highlightCurrentLine(aEvent) { let annotationModel = this._annotationModel; let model = this._model; let oldAnnotation = this._currentLineAnnotation; let newSelection = aEvent.newValue; let collapsed = newSelection.start == newSelection.end; if (!collapsed) { if (oldAnnotation) { annotationModel.removeAnnotation(oldAnnotation); this._currentLineAnnotation = null; } return; } let line = model.getLineAtOffset(newSelection.start); let lineStart = this.getLineStart(line); let lineEnd = this.getLineEnd(line); let title = oldAnnotation ? oldAnnotation.title : SourceEditorUI.strings.GetStringFromName("annotation.currentLine"); this._currentLineAnnotation = { start: lineStart, end: lineEnd, type: ORION_ANNOTATION_TYPES.currentLine, title: title, html: "", overviewStyle: {styleClass: "annotationOverview currentLine"}, lineStyle: {styleClass: "annotationLine currentLine"}, }; annotationModel.replaceAnnotations(oldAnnotation ? [oldAnnotation] : null, [this._currentLineAnnotation]); }, /** * The click event handler for the lines gutter. This function allows the user * to jump to a line or to perform line selection while holding the Shift key * down. * * @private * @param number aLineIndex * The line index where the click event occurred. * @param object aEvent * The DOM click event object. */ _linesRulerClick: function SE__linesRulerClick(aLineIndex, aEvent) { if (aLineIndex === undefined || aLineIndex == -1) { return; } if (aEvent.shiftKey) { let model = this._model; let selection = this.getSelection(); let selectionLineStart = model.getLineAtOffset(selection.start); let selectionLineEnd = model.getLineAtOffset(selection.end); let newStart = aLineIndex <= selectionLineStart ? this.getLineStart(aLineIndex) : selection.start; let newEnd = aLineIndex <= selectionLineStart ? selection.end : this.getLineEnd(aLineIndex); this.setSelection(newStart, newEnd); } else { if (this._annotationRuler) { this._annotationRulerClick(aLineIndex, aEvent); } else { this.setCaretPosition(aLineIndex); } } }, /** * The dblclick event handler for the lines gutter. This function selects the * whole line where the event occurred. * * @private * @param number aLineIndex * The line index where the double click event occurred. * @param object aEvent * The DOM dblclick event object. */ _linesRulerDblClick: function SE__linesRulerDblClick(aLineIndex) { if (aLineIndex === undefined) { return; } let newStart = this.getLineStart(aLineIndex); let newEnd = this.getLineEnd(aLineIndex); this.setSelection(newStart, newEnd); }, /** * Highlight the Orion annotations. This updates the annotation styler as * needed. * @private */ _highlightAnnotations: function SE__highlightAnnotations() { if (this._annotationStyler) { this._annotationStyler.destroy(); this._annotationStyler = null; } let AnnotationStyler = this._iframeWindow.require("orion/textview/annotations").AnnotationStyler; let styler = new AnnotationStyler(this._view, this._annotationModel); this._annotationStyler = styler; styler.addAnnotationType(ORION_ANNOTATION_TYPES.matchingBracket); styler.addAnnotationType(ORION_ANNOTATION_TYPES.currentBracket); styler.addAnnotationType(ORION_ANNOTATION_TYPES.task); styler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation); if (this._config.highlightCurrentLine) { styler.addAnnotationType(ORION_ANNOTATION_TYPES.currentLine); } }, /** * Retrieve the list of Orion Annotations filtered by type for the given text range. * * @private * @param string aType * The annotation type to filter annotations for. Use one of the keys * in ORION_ANNOTATION_TYPES. * @param number aStart * Offset from where to start finding the annotations. * @param number aEnd * End offset for retrieving the annotations. * @return array * The array of annotations, filtered by type, within the given text * range. */ _getAnnotationsByType: function SE__getAnnotationsByType(aType, aStart, aEnd) { let annotations = this._annotationModel.getAnnotations(aStart, aEnd); let annotation, result = []; while (annotation = annotations.next()) { if (annotation.type == ORION_ANNOTATION_TYPES[aType]) { result.push(annotation); } } return result; }, /** * The click event handler for the annotation ruler. * * @private * @param number aLineIndex * The line index where the click event occurred. * @param object aEvent * The DOM click event object. */ _annotationRulerClick: function SE__annotationRulerClick(aLineIndex, aEvent) { if (aLineIndex === undefined || aLineIndex == -1) { return; } let lineStart = this.getLineStart(aLineIndex); let lineEnd = this.getLineEnd(aLineIndex); let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd); if (annotations.length > 0) { this.removeBreakpoint(aLineIndex); } else { this.addBreakpoint(aLineIndex); } }, /** * The click event handler for the overview ruler. When the user clicks on an * annotation the editor jumps to the associated line. * * @private * @param number aLineIndex * The line index where the click event occurred. * @param object aEvent * The DOM click event object. */ _overviewRulerClick: function SE__overviewRulerClick(aLineIndex, aEvent) { if (aLineIndex === undefined || aLineIndex == -1) { return; } let model = this._model; let lineStart = this.getLineStart(aLineIndex); let lineEnd = this.getLineEnd(aLineIndex); let annotations = this._annotationModel.getAnnotations(lineStart, lineEnd); let annotation = annotations.next(); // Jump to the line where annotation is. If the annotation is specific to // a substring part of the line, then select the substring. if (!annotation || lineStart == annotation.start && lineEnd == annotation.end) { this.setSelection(lineStart, lineStart); } else { this.setSelection(annotation.start, annotation.end); } }, /** * Get the editor element. * * @return nsIDOMElement * In this implementation a xul:iframe holds the editor. */ get editorElement() { return this._iframe; }, /** * Helper function to retrieve the strings used for comments in the current * editor mode. * * @private * @return object * An object that holds the following properties: * - line: the comment string used for the start of a single line * comment. * - blockStart: the comment string used for the start of a comment * block. * - blockEnd: the comment string used for the end of a block comment. * Null is returned for unsupported editor modes. */ _getCommentStrings: function SE__getCommentStrings() { let line = ""; let blockCommentStart = ""; let blockCommentEnd = ""; switch (this.getMode()) { case SourceEditor.MODES.JAVASCRIPT: line = "//"; blockCommentStart = "/*"; blockCommentEnd = "*/"; break; case SourceEditor.MODES.CSS: blockCommentStart = "/*"; blockCommentEnd = "*/"; break; case SourceEditor.MODES.HTML: case SourceEditor.MODES.XML: blockCommentStart = ""; break; default: return null; } return {line: line, blockStart: blockCommentStart, blockEnd: blockCommentEnd}; }, /** * Decide whether to comment the selection/current line or to uncomment it. * * @private */ _doCommentUncomment: function SE__doCommentUncomment() { if (this.readOnly) { return false; } let commentObject = this._getCommentStrings(); if (!commentObject) { return false; } let selection = this.getSelection(); let model = this._model; let firstLine = model.getLineAtOffset(selection.start); let lastLine = model.getLineAtOffset(selection.end); // Checks for block comment. let firstLineText = model.getLine(firstLine); let lastLineText = model.getLine(lastLine); let openIndex = firstLineText.indexOf(commentObject.blockStart); let closeIndex = lastLineText.lastIndexOf(commentObject.blockEnd); if (openIndex != -1 && closeIndex != -1 && (firstLine != lastLine || (closeIndex - openIndex) >= commentObject.blockStart.length)) { return this._doUncomment(); } if (!commentObject.line) { return this._doComment(); } // If the selection is not a block comment, check for the first and the last // lines to be line commented. let firstLastCommented = [firstLineText, lastLineText].every(function(aLineText) { let openIndex = aLineText.indexOf(commentObject.line); if (openIndex != -1) { let textUntilComment = aLineText.slice(0, openIndex); if (!textUntilComment || /^\s+$/.test(textUntilComment)) { return true; } } return false; }); if (firstLastCommented) { return this._doUncomment(); } // If we reach here, then we have to comment the selection/line. return this._doComment(); }, /** * Wrap the selected text in comments. If nothing is selected the current * caret line is commented out. Single line and block comments depend on the * current editor mode. * * @private */ _doComment: function SE__doComment() { if (this.readOnly) { return false; } let commentObject = this._getCommentStrings(); if (!commentObject) { return false; } let selection = this.getSelection(); if (selection.start == selection.end) { let selectionLine = this._model.getLineAtOffset(selection.start); let lineStartOffset = this.getLineStart(selectionLine); if (commentObject.line) { this.setText(commentObject.line, lineStartOffset, lineStartOffset); } else { let lineEndOffset = this.getLineEnd(selectionLine); this.startCompoundChange(); this.setText(commentObject.blockStart, lineStartOffset, lineStartOffset); this.setText(commentObject.blockEnd, lineEndOffset + commentObject.blockStart.length, lineEndOffset + commentObject.blockStart.length); this.endCompoundChange(); } } else { this.startCompoundChange(); this.setText(commentObject.blockStart, selection.start, selection.start); this.setText(commentObject.blockEnd, selection.end + commentObject.blockStart.length, selection.end + commentObject.blockStart.length); this.endCompoundChange(); } return true; }, /** * Uncomment the selected text. If nothing is selected the current caret line * is umcommented. Single line and block comments depend on the current editor * mode. * * @private */ _doUncomment: function SE__doUncomment() { if (this.readOnly) { return false; } let commentObject = this._getCommentStrings(); if (!commentObject) { return false; } let selection = this.getSelection(); let firstLine = this._model.getLineAtOffset(selection.start); let lastLine = this._model.getLineAtOffset(selection.end); // Uncomment a block of text. let firstLineText = this._model.getLine(firstLine); let lastLineText = this._model.getLine(lastLine); let openIndex = firstLineText.indexOf(commentObject.blockStart); let closeIndex = lastLineText.lastIndexOf(commentObject.blockEnd); if (openIndex != -1 && closeIndex != -1 && (firstLine != lastLine || (closeIndex - openIndex) >= commentObject.blockStart.length)) { let firstLineStartOffset = this.getLineStart(firstLine); let lastLineStartOffset = this.getLineStart(lastLine); let openOffset = firstLineStartOffset + openIndex; let closeOffset = lastLineStartOffset + closeIndex; this.startCompoundChange(); this.setText("", closeOffset, closeOffset + commentObject.blockEnd.length); this.setText("", openOffset, openOffset + commentObject.blockStart.length); this.endCompoundChange(); return true; } if (!commentObject.line) { return true; } // If the selected text is not a block of comment, then uncomment each line. this.startCompoundChange(); let lineCaret = firstLine; while (lineCaret <= lastLine) { let currentLine = this._model.getLine(lineCaret); let lineStart = this.getLineStart(lineCaret); let openIndex = currentLine.indexOf(commentObject.line); let openOffset = lineStart + openIndex; let textUntilComment = this.getText(lineStart, openOffset); if (openIndex != -1 && (!textUntilComment || /^\s+$/.test(textUntilComment))) { this.setText("", openOffset, openOffset + commentObject.line.length); } lineCaret++; } this.endCompoundChange(); return true; }, /** * Helper function for _moveToBracket{Opening/Closing} to find the offset of * matching bracket. * * @param number aOffset * The offset of the bracket for which you want to find the bracket. * @private */ _getMatchingBracketIndex: function SE__getMatchingBracketIndex(aOffset) { return this._styler._findMatchingBracket(this._model, aOffset); }, /** * Move the cursor to the matching opening bracket if at corresponding closing * bracket, otherwise move to the opening bracket for the current block of code. * * @private */ _moveToBracketOpening: function SE__moveToBracketOpening() { let mode = this.getMode(); // Returning early if not in JavaScipt or CSS mode. if (mode != SourceEditor.MODES.JAVASCRIPT && mode != SourceEditor.MODES.CSS) { return false; } let caretOffset = this.getCaretOffset() - 1; let matchingIndex = this._getMatchingBracketIndex(caretOffset); // If the caret is not at the closing bracket "}", find the index of the // opening bracket "{" for the current code block. if (matchingIndex == -1 || matchingIndex > caretOffset) { matchingIndex = -1; let text = this.getText(); let closingOffset = text.indexOf("}", caretOffset); while (closingOffset > -1) { let closingMatchingIndex = this._getMatchingBracketIndex(closingOffset); if (closingMatchingIndex < caretOffset && closingMatchingIndex != -1) { matchingIndex = closingMatchingIndex; break; } closingOffset = text.indexOf("}", closingOffset + 1); } // Moving to the previous code block starting bracket if caret not inside // any code block. if (matchingIndex == -1) { let lastClosingOffset = text.lastIndexOf("}", caretOffset); while (lastClosingOffset > -1) { let closingMatchingIndex = this._getMatchingBracketIndex(lastClosingOffset); if (closingMatchingIndex < caretOffset && closingMatchingIndex != -1) { matchingIndex = closingMatchingIndex; break; } lastClosingOffset = text.lastIndexOf("}", lastClosingOffset - 1); } } } if (matchingIndex > -1) { this.setCaretOffset(matchingIndex + 1); } return true; }, /** * Moves the cursor to the matching closing bracket if at corresponding * opening bracket, otherwise move to the closing bracket for the current * block of code. * * @private */ _moveToBracketClosing: function SE__moveToBracketClosing() { let mode = this.getMode(); // Returning early if not in JavaScipt or CSS mode. if (mode != SourceEditor.MODES.JAVASCRIPT && mode != SourceEditor.MODES.CSS) { return false; } let caretOffset = this.getCaretOffset(); let matchingIndex = this._getMatchingBracketIndex(caretOffset - 1); // If the caret is not at the opening bracket "{", find the index of the // closing bracket "}" for the current code block. if (matchingIndex == -1 || matchingIndex < caretOffset) { matchingIndex = -1; let text = this.getText(); let openingOffset = text.lastIndexOf("{", caretOffset); while (openingOffset > -1) { let openingMatchingIndex = this._getMatchingBracketIndex(openingOffset); if (openingMatchingIndex > caretOffset) { matchingIndex = openingMatchingIndex; break; } openingOffset = text.lastIndexOf("{", openingOffset - 1); } // Moving to the next code block ending bracket if caret not inside // any code block. if (matchingIndex == -1) { let nextOpeningIndex = text.indexOf("{", caretOffset + 1); while (nextOpeningIndex > -1) { let openingMatchingIndex = this._getMatchingBracketIndex(nextOpeningIndex); if (openingMatchingIndex > caretOffset) { matchingIndex = openingMatchingIndex; break; } nextOpeningIndex = text.indexOf("{", nextOpeningIndex + 1); } } } if (matchingIndex > -1) { this.setCaretOffset(matchingIndex); } return true; }, /** * Add an event listener to the editor. You can use one of the known events. * * @see SourceEditor.EVENTS * * @param string aEventType * The event type you want to listen for. * @param function aCallback * The function you want executed when the event is triggered. */ addEventListener: function SE_addEventListener(aEventType, aCallback) { if (this._view && aEventType in ORION_EVENTS) { this._view.addEventListener(ORION_EVENTS[aEventType], aCallback); } else if (this._eventTarget.addEventListener) { this._eventTarget.addEventListener(aEventType, aCallback); } else { this._eventListenersQueue.push(["add", aEventType, aCallback]); } }, /** * Remove an event listener from the editor. You can use one of the known * events. * * @see SourceEditor.EVENTS * * @param string aEventType * The event type you have a listener for. * @param function aCallback * The function you have as the event handler. */ removeEventListener: function SE_removeEventListener(aEventType, aCallback) { if (this._view && aEventType in ORION_EVENTS) { this._view.removeEventListener(ORION_EVENTS[aEventType], aCallback); } else if (this._eventTarget.removeEventListener) { this._eventTarget.removeEventListener(aEventType, aCallback); } else { this._eventListenersQueue.push(["remove", aEventType, aCallback]); } }, /** * Undo a change in the editor. * * @return boolean * True if there was a change undone, false otherwise. */ undo: function SE_undo() { let result = this._undoStack.undo(); this.ui._onUndoRedo(); return result; }, /** * Redo a change in the editor. * * @return boolean * True if there was a change redone, false otherwise. */ redo: function SE_redo() { let result = this._undoStack.redo(); this.ui._onUndoRedo(); return result; }, /** * Check if there are changes that can be undone. * * @return boolean * True if there are changes that can be undone, false otherwise. */ canUndo: function SE_canUndo() { return this._undoStack.canUndo(); }, /** * Check if there are changes that can be repeated. * * @return boolean * True if there are changes that can be repeated, false otherwise. */ canRedo: function SE_canRedo() { return this._undoStack.canRedo(); }, /** * Reset the Undo stack. */ resetUndo: function SE_resetUndo() { this._undoStack.reset(); this._updateDirty(); this.ui._onUndoRedo(); }, /** * Set the "dirty" state of the editor. Set this to false when you save the * text being edited. The dirty state will become true once the user makes * changes to the text. * * @param boolean aNewValue * The new dirty state: true if the text is not saved, false if you * just saved the text. */ set dirty(aNewValue) { if (aNewValue == this._dirty) { return; } let event = { type: SourceEditor.EVENTS.DIRTY_CHANGED, oldValue: this._dirty, newValue: aNewValue, }; this._dirty = aNewValue; if (!this._dirty && !this._undoStack.isClean()) { this._undoStack.markClean(); } this._dispatchEvent(event); }, /** * Get the editor "dirty" state. This tells if the text is considered saved or * not. * * @see SourceEditor.EVENTS.DIRTY_CHANGED * @return boolean * True if there are changes which are not saved, false otherwise. */ get dirty() { return this._dirty; }, /** * Start a compound change in the editor. Compound changes are grouped into * only one change that you can undo later, after you invoke * endCompoundChange(). */ startCompoundChange: function SE_startCompoundChange() { this._undoStack.startCompoundChange(); }, /** * End a compound change in the editor. */ endCompoundChange: function SE_endCompoundChange() { this._undoStack.endCompoundChange(); }, /** * Focus the editor. */ focus: function SE_focus() { this._view.focus(); }, /** * Get the first visible line number. * * @return number * The line number, counting from 0. */ getTopIndex: function SE_getTopIndex() { return this._view.getTopIndex(); }, /** * Set the first visible line number. * * @param number aTopIndex * The line number, counting from 0. */ setTopIndex: function SE_setTopIndex(aTopIndex) { this._view.setTopIndex(aTopIndex); }, /** * Check if the editor has focus. * * @return boolean * True if the editor is focused, false otherwise. */ hasFocus: function SE_hasFocus() { return this._view.hasFocus(); }, /** * Get the editor content, in the given range. If no range is given you get * the entire editor content. * * @param number [aStart=0] * Optional, start from the given offset. * @param number [aEnd=content char count] * Optional, end offset for the text you want. If this parameter is not * given, then the text returned goes until the end of the editor * content. * @return string * The text in the given range. */ getText: function SE_getText(aStart, aEnd) { return this._view.getText(aStart, aEnd); }, /** * Get the start character offset of the line with index aLineIndex. * * @param number aLineIndex * Zero based index of the line. * @return number * Line start offset or -1 if out of range. */ getLineStart: function SE_getLineStart(aLineIndex) { return this._model.getLineStart(aLineIndex); }, /** * Get the end character offset of the line with index aLineIndex, * excluding the end offset. When the line delimiter is present, * the offset is the start offset of the next line or the char count. * Otherwise, it is the offset of the line delimiter. * * @param number aLineIndex * Zero based index of the line. * @param boolean [aIncludeDelimiter = false] * Optional, whether or not to include the line delimiter. * @return number * Line end offset or -1 if out of range. */ getLineEnd: function SE_getLineEnd(aLineIndex, aIncludeDelimiter) { return this._model.getLineEnd(aLineIndex, aIncludeDelimiter); }, /** * Get the number of characters in the editor content. * * @return number * The number of editor content characters. */ getCharCount: function SE_getCharCount() { return this._model.getCharCount(); }, /** * Get the selected text. * * @return string * The currently selected text. */ getSelectedText: function SE_getSelectedText() { let selection = this.getSelection(); return this.getText(selection.start, selection.end); }, /** * Replace text in the source editor with the given text, in the given range. * * @param string aText * The text you want to put into the editor. * @param number [aStart=0] * Optional, the start offset, zero based, from where you want to start * replacing text in the editor. * @param number [aEnd=char count] * Optional, the end offset, zero based, where you want to stop * replacing text in the editor. */ setText: function SE_setText(aText, aStart, aEnd) { this._view.setText(aText, aStart, aEnd); }, /** * Drop the current selection / deselect. */ dropSelection: function SE_dropSelection() { this.setCaretOffset(this.getCaretOffset()); }, /** * Select a specific range in the editor. * * @param number aStart * Selection range start. * @param number aEnd * Selection range end. */ setSelection: function SE_setSelection(aStart, aEnd) { this._view.setSelection(aStart, aEnd, true); }, /** * Get the current selection range. * * @return object * An object with two properties, start and end, that give the * selection range (zero based offsets). */ getSelection: function SE_getSelection() { return this._view.getSelection(); }, /** * Get the current caret offset. * * @return number * The current caret offset. */ getCaretOffset: function SE_getCaretOffset() { return this._view.getCaretOffset(); }, /** * Set the caret offset. * * @param number aOffset * The new caret offset you want to set. */ setCaretOffset: function SE_setCaretOffset(aOffset) { this._view.setCaretOffset(aOffset, true); }, /** * Get the caret position. * * @return object * An object that holds two properties: * - line: the line number, counting from 0. * - col: the column number, counting from 0. */ getCaretPosition: function SE_getCaretPosition() { let offset = this.getCaretOffset(); let line = this._model.getLineAtOffset(offset); let lineStart = this.getLineStart(line); let column = offset - lineStart; return {line: line, col: column}; }, /** * Set the caret position: line and column. * * @param number aLine * The new caret line location. Line numbers start from 0. * @param number [aColumn=0] * Optional. The new caret column location. Columns start from 0. * @param number [aAlign=0] * Optional. Position of the line with respect to viewport. * Allowed values are: * SourceEditor.VERTICAL_ALIGN.TOP target line at top of view. * SourceEditor.VERTICAL_ALIGN.CENTER target line at center of view. * SourceEditor.VERTICAL_ALIGN.BOTTOM target line at bottom of view. */ setCaretPosition: function SE_setCaretPosition(aLine, aColumn, aAlign) { let editorHeight = this._view.getClientArea().height; let lineHeight = this._view.getLineHeight(); let linesVisible = Math.floor(editorHeight/lineHeight); let halfVisible = Math.round(linesVisible/2); let firstVisible = this.getTopIndex(); let lastVisible = this._view.getBottomIndex(); let caretOffset = this.getLineStart(aLine) + (aColumn || 0); this._view.setSelection(caretOffset, caretOffset, false); // If the target line is in view, skip the vertical alignment part. if (aLine <= lastVisible && aLine >= firstVisible) { this._view.showSelection(); return; } // Setting the offset so that the line always falls in the upper half // of visible lines (lower half for BOTTOM aligned). // VERTICAL_OFFSET is the maximum allowed value. let offset = Math.min(halfVisible, VERTICAL_OFFSET); let topIndex; switch (aAlign) { case this.VERTICAL_ALIGN.CENTER: topIndex = Math.max(aLine - halfVisible, 0); break; case this.VERTICAL_ALIGN.BOTTOM: topIndex = Math.max(aLine - linesVisible + offset, 0); break; default: // this.VERTICAL_ALIGN.TOP. topIndex = Math.max(aLine - offset, 0); break; } // Bringing down the topIndex to total lines in the editor if exceeding. topIndex = Math.min(topIndex, this.getLineCount()); this.setTopIndex(topIndex); let location = this._view.getLocationAtOffset(caretOffset); this._view.setHorizontalPixel(location.x); }, /** * Get the line count. * * @return number * The number of lines in the document being edited. */ getLineCount: function SE_getLineCount() { return this._model.getLineCount(); }, /** * Get the line delimiter used in the document being edited. * * @return string * The line delimiter. */ getLineDelimiter: function SE_getLineDelimiter() { return this._model.getLineDelimiter(); }, /** * Get the indentation string used in the document being edited. * * @return string * The indentation string. */ getIndentationString: function SE_getIndentationString() { if (this._expandTab) { return (new Array(this._tabSize + 1)).join(" "); } return "\t"; }, /** * Set the source editor mode to the file type you are editing. * * @param string aMode * One of the predefined SourceEditor.MODES. */ setMode: function SE_setMode(aMode) { if (this._styler) { this._styler.destroy(); this._styler = null; } let window = this._iframeWindow; switch (aMode) { case SourceEditor.MODES.JAVASCRIPT: case SourceEditor.MODES.CSS: let TextStyler = window.require("examples/textview/textStyler").TextStyler; this._styler = new TextStyler(this._view, aMode, this._annotationModel); this._styler.setFoldingEnabled(false); break; case SourceEditor.MODES.HTML: case SourceEditor.MODES.XML: let TextMateStyler = window.require("orion/editor/textMateStyler").TextMateStyler; let HtmlGrammar = window.require("orion/editor/htmlGrammar").HtmlGrammar; this._styler = new TextMateStyler(this._view, new HtmlGrammar()); break; } this._highlightAnnotations(); this._mode = aMode; }, /** * Get the current source editor mode. * * @return string * Returns one of the predefined SourceEditor.MODES. */ getMode: function SE_getMode() { return this._mode; }, /** * Setter for the read-only state of the editor. * @param boolean aValue * Tells if you want the editor to read-only or not. */ set readOnly(aValue) { this._view.setOptions({ readonly: aValue, themeClass: "mozilla" + (aValue ? " readonly" : ""), }); }, /** * Getter for the read-only state of the editor. * @type boolean */ get readOnly() { return this._view.getOptions("readonly"); }, /** * Set the current debugger location at the given line index. This is useful in * a debugger or in any other context where the user needs to track the * current state, where a debugger-like environment is at. * * @param number aLineIndex * Line index of the current debugger location, starting from 0. * Use any negative number to clear the current location. */ setDebugLocation: function SE_setDebugLocation(aLineIndex) { let annotations = this._getAnnotationsByType("debugLocation", 0, this.getCharCount()); if (annotations.length > 0) { annotations.forEach(this._annotationModel.removeAnnotation, this._annotationModel); } if (aLineIndex < 0) { return; } let lineStart = this._model.getLineStart(aLineIndex); let lineEnd = this._model.getLineEnd(aLineIndex); let lineText = this._model.getLine(aLineIndex); let title = SourceEditorUI.strings. formatStringFromName("annotation.debugLocation.title", [lineText], 1); let annotation = { type: ORION_ANNOTATION_TYPES.debugLocation, start: lineStart, end: lineEnd, title: title, style: {styleClass: "annotation debugLocation"}, html: "", overviewStyle: {styleClass: "annotationOverview debugLocation"}, rangeStyle: {styleClass: "annotationRange debugLocation"}, lineStyle: {styleClass: "annotationLine debugLocation"}, }; this._annotationModel.addAnnotation(annotation); }, /** * Retrieve the current debugger line index configured for this editor. * * @return number * The line index starting from 0 where the current debugger is * paused. If no debugger location has been set -1 is returned. */ getDebugLocation: function SE_getDebugLocation() { let annotations = this._getAnnotationsByType("debugLocation", 0, this.getCharCount()); if (annotations.length > 0) { return this._model.getLineAtOffset(annotations[0].start); } return -1; }, /** * Add a breakpoint at the given line index. * * @param number aLineIndex * Line index where to add the breakpoint (starts from 0). * @param string [aCondition] * Optional breakpoint condition. */ addBreakpoint: function SE_addBreakpoint(aLineIndex, aCondition) { let lineStart = this.getLineStart(aLineIndex); let lineEnd = this.getLineEnd(aLineIndex); let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd); if (annotations.length > 0) { return; } let lineText = this._model.getLine(aLineIndex); let title = SourceEditorUI.strings. formatStringFromName("annotation.breakpoint.title", [lineText], 1); let annotation = { type: ORION_ANNOTATION_TYPES.breakpoint, start: lineStart, end: lineEnd, breakpointCondition: aCondition, title: title, style: {styleClass: "annotation breakpoint"}, html: "", overviewStyle: {styleClass: "annotationOverview breakpoint"}, rangeStyle: {styleClass: "annotationRange breakpoint"} }; this._annotationModel.addAnnotation(annotation); let event = { type: SourceEditor.EVENTS.BREAKPOINT_CHANGE, added: [{line: aLineIndex, condition: aCondition}], removed: [], }; this._dispatchEvent(event); }, /** * Remove the current breakpoint from the given line index. * * @param number aLineIndex * Line index from where to remove the breakpoint (starts from 0). * @return boolean * True if a breakpoint was removed, false otherwise. */ removeBreakpoint: function SE_removeBreakpoint(aLineIndex) { let lineStart = this.getLineStart(aLineIndex); let lineEnd = this.getLineEnd(aLineIndex); let event = { type: SourceEditor.EVENTS.BREAKPOINT_CHANGE, added: [], removed: [], }; let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd); annotations.forEach(function(annotation) { this._annotationModel.removeAnnotation(annotation); event.removed.push({line: aLineIndex, condition: annotation.breakpointCondition}); }, this); if (event.removed.length > 0) { this._dispatchEvent(event); } return event.removed.length > 0; }, /** * Get the list of breakpoints in the Source Editor instance. * * @return array * The array of breakpoints. Each item is an object with two * properties: line and condition. */ getBreakpoints: function SE_getBreakpoints() { let annotations = this._getAnnotationsByType("breakpoint", 0, this.getCharCount()); let breakpoints = []; annotations.forEach(function(annotation) { breakpoints.push({line: this._model.getLineAtOffset(annotation.start), condition: annotation.breakpointCondition}); }, this); return breakpoints; }, /** * Convert the given rectangle from one coordinate reference to another. * * Known coordinate references: * - "document" - gives the coordinates relative to the entire document. * - "view" - gives the coordinates relative to the editor viewport. * * @param object aRect * The rectangle to convert. Object properties: x, y, width and height. * @param string aFrom * The source coordinate reference. * @param string aTo * The destination coordinate reference. * @return object aRect * Returns the rectangle with changed coordinates. */ convertCoordinates: function SE_convertCoordinates(aRect, aFrom, aTo) { return this._view.convert(aRect, aFrom, aTo); }, /** * Get the character offset nearest to the given pixel location. * * @param number aX * @param number aY * @return number * Returns the character offset at the given location. */ getOffsetAtLocation: function SE_getOffsetAtLocation(aX, aY) { return this._view.getOffsetAtLocation(aX, aY); }, /** * Get the pixel location, relative to the document, at the given character * offset. * * @param number aOffset * @return object * The pixel location relative to the document being edited. Two * properties are included: x and y. */ getLocationAtOffset: function SE_getLocationAtOffset(aOffset) { return this._view.getLocationAtOffset(aOffset); }, /** * Get the line location for a given character offset. * * @param number aOffset * @return number * The line location relative to the give character offset. */ getLineAtOffset: function SE_getLineAtOffset(aOffset) { return this._model.getLineAtOffset(aOffset); }, /** * Destroy/uninitialize the editor. */ destroy: function SE_destroy() { if (this._config.highlightCurrentLine || Services.appinfo.OS == "Linux") { this.removeEventListener(SourceEditor.EVENTS.SELECTION, this._onOrionSelection); } this._onOrionSelection = null; this.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, this._onTextChanged); this._onTextChanged = null; if (this._contextMenu) { this.removeEventListener(SourceEditor.EVENTS.CONTEXT_MENU, this._onOrionContextMenu); this._contextMenu = null; } this._onOrionContextMenu = null; if (this._primarySelectionTimeout) { let window = this.parentElement.ownerDocument.defaultView; window.clearTimeout(this._primarySelectionTimeout); this._primarySelectionTimeout = null; } this._view.destroy(); this.ui.destroy(); this.ui = null; this.parentElement.removeChild(this._iframe); this.parentElement = null; this._iframeWindow = null; this._iframe = null; this._undoStack = null; this._styler = null; this._linesRuler = null; this._annotationRuler = null; this._overviewRuler = null; this._dragAndDrop = null; this._annotationModel = null; this._annotationStyler = null; this._currentLineAnnotation = null; this._eventTarget = null; this._eventListenersQueue = null; this._view = null; this._model = null; this._config = null; this._lastFind = null; }, };