lib/gollum/public/gollum/livepreview/js/ace/lib/ace/editor.js in gollum-3.1.2 vs lib/gollum/public/gollum/livepreview/js/ace/lib/ace/editor.js in gollum-3.1.3

- old
+ new

@@ -1,22 +1,22 @@ /* ***** BEGIN LICENSE BLOCK ***** * Distributed under the BSD license: * * Copyright (c) 2010, Ajax.org B.V. * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Ajax.org B.V. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES @@ -32,10 +32,11 @@ "use strict"; require("./lib/fixoldbrowsers"); var oop = require("./lib/oop"); +var dom = require("./lib/dom"); var lang = require("./lib/lang"); var useragent = require("./lib/useragent"); var TextInput = require("./keyboard/textinput").TextInput; var MouseHandler = require("./mouse/mouse_handler").MouseHandler; var FoldHandler = require("./mouse/fold_handler").FoldHandler; @@ -44,18 +45,18 @@ var Search = require("./search").Search; var Range = require("./range").Range; var EventEmitter = require("./lib/event_emitter").EventEmitter; var CommandManager = require("./commands/command_manager").CommandManager; var defaultCommands = require("./commands/default_commands").commands; +var config = require("./config"); +var TokenIterator = require("./token_iterator").TokenIterator; /** + * The main entry point into the Ace functionality. * + * The `Editor` manages the [[EditSession]] (which manages [[Document]]s), as well as the [[VirtualRenderer]], which draws everything to the screen. * - * The main entry point into the Ace functionality. - * - * The `Editor` manages the [[EditSession]] (which manages [[Document]]s), as well as the [[VirtualRenderer]], which draws everything to the screen. - * * Event sessions dealing with the mouse and keyboard are bubbled up from `Document` to the `Editor`, which decides what to do with them. * @class Editor **/ /** @@ -84,32 +85,178 @@ this.$blockScrolling = 0; this.$search = new Search().set({ wrap: true }); + this.$historyTracker = this.$historyTracker.bind(this); + this.commands.on("exec", this.$historyTracker); + + this.$initOperationListeners(); + + this._$emitInputEvent = lang.delayedCall(function() { + this._signal("input", {}); + if (this.session && this.session.bgTokenizer) + this.session.bgTokenizer.scheduleStart(); + }.bind(this)); + + this.on("change", function(_, _self) { + _self._$emitInputEvent.schedule(31); + }); + this.setSession(session || new EditSession("")); + config.resetOptions(this); + config._signal("editor", this); }; (function(){ oop.implement(this, EventEmitter); + this.$initOperationListeners = function() { + function last(a) {return a[a.length - 1]} + + this.selections = []; + this.commands.on("exec", this.startOperation.bind(this), true); + this.commands.on("afterExec", this.endOperation.bind(this), true); + + this.$opResetTimer = lang.delayedCall(this.endOperation.bind(this)); + + this.on("change", function() { + this.curOp || this.startOperation(); + this.curOp.docChanged = true; + }.bind(this), true); + + this.on("changeSelection", function() { + this.curOp || this.startOperation(); + this.curOp.selectionChanged = true; + }.bind(this), true); + }; + + this.curOp = null; + this.prevOp = {}; + this.startOperation = function(commadEvent) { + if (this.curOp) { + if (!commadEvent || this.curOp.command) + return; + this.prevOp = this.curOp; + } + if (!commadEvent) { + this.previousCommand = null; + commadEvent = {}; + } + + this.$opResetTimer.schedule(); + this.curOp = { + command: commadEvent.command || {}, + args: commadEvent.args, + scrollTop: this.renderer.scrollTop + }; + if (this.curOp.command.name) + this.$blockScrolling++; + // this.selections.push(this.selection.toJSON()); + }; + + this.endOperation = function(e) { + if (this.curOp) { + if (e && e.returnValue === false) + return this.curOp = null; + this._signal("beforeEndOperation"); + var command = this.curOp.command; + if (command.name && this.$blockScrolling) + this.$blockScrolling--; + if (command && command.scrollIntoView) { + switch (command.scrollIntoView) { + case "center": + this.renderer.scrollCursorIntoView(null, 0.5); + break; + case "animate": + case "cursor": + this.renderer.scrollCursorIntoView(); + break; + case "selectionPart": + var range = this.selection.getRange(); + var config = this.renderer.layerConfig; + if (range.start.row >= config.lastRow || range.end.row <= config.firstRow) { + this.renderer.scrollSelectionIntoView(this.selection.anchor, this.selection.lead); + } + break; + default: + break; + } + if (command.scrollIntoView == "animate") + this.renderer.animateScrolling(this.curOp.scrollTop); + } + + this.prevOp = this.curOp; + this.curOp = null; + } + }; + + // TODO use property on commands instead of this + this.$mergeableCommands = ["backspace", "del", "insertstring"]; + this.$historyTracker = function(e) { + if (!this.$mergeUndoDeltas) + return; + + var prev = this.prevOp; + var mergeableCommands = this.$mergeableCommands; + // previous command was the same + var shouldMerge = prev.command && (e.command.name == prev.command.name); + if (e.command.name == "insertstring") { + var text = e.args; + if (this.mergeNextCommand === undefined) + this.mergeNextCommand = true; + + shouldMerge = shouldMerge + && this.mergeNextCommand // previous command allows to coalesce with + && (!/\s/.test(text) || /\s/.test(prev.args)); // previous insertion was of same type + + this.mergeNextCommand = true; + } else { + shouldMerge = shouldMerge + && mergeableCommands.indexOf(e.command.name) !== -1; // the command is mergeable + } + + if ( + this.$mergeUndoDeltas != "always" + && Date.now() - this.sequenceStartTime > 2000 + ) { + shouldMerge = false; // the sequence is too long + } + + if (shouldMerge) + this.session.mergeUndoDeltas = true; + else if (mergeableCommands.indexOf(e.command.name) !== -1) + this.sequenceStartTime = Date.now(); + }; + /** * Sets a new key handler, such as "vim" or "windows". * @param {String} keyboardHandler The new key handler * - * **/ - this.setKeyboardHandler = function(keyboardHandler) { - this.keyBinding.setKeyboardHandler(keyboardHandler); + this.setKeyboardHandler = function(keyboardHandler, cb) { + if (keyboardHandler && typeof keyboardHandler === "string") { + this.$keybindingId = keyboardHandler; + var _self = this; + config.loadModule(["keybinding", keyboardHandler], function(module) { + if (_self.$keybindingId == keyboardHandler) + _self.keyBinding.setKeyboardHandler(module && module.handler); + cb && cb(); + }); + } else { + this.$keybindingId = null; + this.keyBinding.setKeyboardHandler(keyboardHandler); + cb && cb(); + } }; - /** + /** * Returns the keyboard handler, such as "vim" or "windows". * * @returns {String} - * + * **/ this.getKeyboardHandler = function() { return this.keyBinding.getKeyboardHandler(); }; @@ -117,24 +264,22 @@ /** * Emitted whenever the [[EditSession]] changes. * @event changeSession * @param {Object} e An object with two properties, `oldSession` and `session`, that represent the old and new [[EditSession]]s. * - * **/ /** * Sets a new editsession to use. This method also emits the `'changeSession'` event. * @param {EditSession} session The new session to use * - * **/ this.setSession = function(session) { if (this.session == session) return; - if (this.session) { - var oldSession = this.session; + var oldSession = this.session; + if (oldSession) { this.session.removeEventListener("change", this.$onDocumentChange); this.session.removeEventListener("changeMode", this.$onChangeMode); this.session.removeEventListener("tokenizerUpdate", this.$onTokenizerUpdate); this.session.removeEventListener("changeTabSize", this.$onChangeTabSize); this.session.removeEventListener("changeWrapLimit", this.$onChangeWrapLimit); @@ -144,105 +289,110 @@ this.session.removeEventListener("changeBackMarker", this.$onChangeBackMarker); this.session.removeEventListener("changeBreakpoint", this.$onChangeBreakpoint); this.session.removeEventListener("changeAnnotation", this.$onChangeAnnotation); this.session.removeEventListener("changeOverwrite", this.$onCursorChange); this.session.removeEventListener("changeScrollTop", this.$onScrollTopChange); - this.session.removeEventListener("changeLeftTop", this.$onScrollLeftChange); + this.session.removeEventListener("changeScrollLeft", this.$onScrollLeftChange); var selection = this.session.getSelection(); selection.removeEventListener("changeCursor", this.$onCursorChange); selection.removeEventListener("changeSelection", this.$onSelectionChange); } this.session = session; + if (session) { + this.$onDocumentChange = this.onDocumentChange.bind(this); + session.addEventListener("change", this.$onDocumentChange); + this.renderer.setSession(session); + + this.$onChangeMode = this.onChangeMode.bind(this); + session.addEventListener("changeMode", this.$onChangeMode); + + this.$onTokenizerUpdate = this.onTokenizerUpdate.bind(this); + session.addEventListener("tokenizerUpdate", this.$onTokenizerUpdate); + + this.$onChangeTabSize = this.renderer.onChangeTabSize.bind(this.renderer); + session.addEventListener("changeTabSize", this.$onChangeTabSize); + + this.$onChangeWrapLimit = this.onChangeWrapLimit.bind(this); + session.addEventListener("changeWrapLimit", this.$onChangeWrapLimit); + + this.$onChangeWrapMode = this.onChangeWrapMode.bind(this); + session.addEventListener("changeWrapMode", this.$onChangeWrapMode); + + this.$onChangeFold = this.onChangeFold.bind(this); + session.addEventListener("changeFold", this.$onChangeFold); + + this.$onChangeFrontMarker = this.onChangeFrontMarker.bind(this); + this.session.addEventListener("changeFrontMarker", this.$onChangeFrontMarker); + + this.$onChangeBackMarker = this.onChangeBackMarker.bind(this); + this.session.addEventListener("changeBackMarker", this.$onChangeBackMarker); + + this.$onChangeBreakpoint = this.onChangeBreakpoint.bind(this); + this.session.addEventListener("changeBreakpoint", this.$onChangeBreakpoint); + + this.$onChangeAnnotation = this.onChangeAnnotation.bind(this); + this.session.addEventListener("changeAnnotation", this.$onChangeAnnotation); + + this.$onCursorChange = this.onCursorChange.bind(this); + this.session.addEventListener("changeOverwrite", this.$onCursorChange); + + this.$onScrollTopChange = this.onScrollTopChange.bind(this); + this.session.addEventListener("changeScrollTop", this.$onScrollTopChange); + + this.$onScrollLeftChange = this.onScrollLeftChange.bind(this); + this.session.addEventListener("changeScrollLeft", this.$onScrollLeftChange); + + this.selection = session.getSelection(); + this.selection.addEventListener("changeCursor", this.$onCursorChange); + + this.$onSelectionChange = this.onSelectionChange.bind(this); + this.selection.addEventListener("changeSelection", this.$onSelectionChange); + + this.onChangeMode(); + + this.$blockScrolling += 1; + this.onCursorChange(); + this.$blockScrolling -= 1; + + this.onScrollTopChange(); + this.onScrollLeftChange(); + this.onSelectionChange(); + this.onChangeFrontMarker(); + this.onChangeBackMarker(); + this.onChangeBreakpoint(); + this.onChangeAnnotation(); + this.session.getUseWrapMode() && this.renderer.adjustWrapLimit(); + this.renderer.updateFull(); + } else { + this.selection = null; + this.renderer.setSession(session); + } - this.$onDocumentChange = this.onDocumentChange.bind(this); - session.addEventListener("change", this.$onDocumentChange); - this.renderer.setSession(session); - - this.$onChangeMode = this.onChangeMode.bind(this); - session.addEventListener("changeMode", this.$onChangeMode); - - this.$onTokenizerUpdate = this.onTokenizerUpdate.bind(this); - session.addEventListener("tokenizerUpdate", this.$onTokenizerUpdate); - - this.$onChangeTabSize = this.renderer.onChangeTabSize.bind(this.renderer); - session.addEventListener("changeTabSize", this.$onChangeTabSize); - - this.$onChangeWrapLimit = this.onChangeWrapLimit.bind(this); - session.addEventListener("changeWrapLimit", this.$onChangeWrapLimit); - - this.$onChangeWrapMode = this.onChangeWrapMode.bind(this); - session.addEventListener("changeWrapMode", this.$onChangeWrapMode); - - this.$onChangeFold = this.onChangeFold.bind(this); - session.addEventListener("changeFold", this.$onChangeFold); - - this.$onChangeFrontMarker = this.onChangeFrontMarker.bind(this); - this.session.addEventListener("changeFrontMarker", this.$onChangeFrontMarker); - - this.$onChangeBackMarker = this.onChangeBackMarker.bind(this); - this.session.addEventListener("changeBackMarker", this.$onChangeBackMarker); - - this.$onChangeBreakpoint = this.onChangeBreakpoint.bind(this); - this.session.addEventListener("changeBreakpoint", this.$onChangeBreakpoint); - - this.$onChangeAnnotation = this.onChangeAnnotation.bind(this); - this.session.addEventListener("changeAnnotation", this.$onChangeAnnotation); - - this.$onCursorChange = this.onCursorChange.bind(this); - this.session.addEventListener("changeOverwrite", this.$onCursorChange); - - this.$onScrollTopChange = this.onScrollTopChange.bind(this); - this.session.addEventListener("changeScrollTop", this.$onScrollTopChange); - - this.$onScrollLeftChange = this.onScrollLeftChange.bind(this); - this.session.addEventListener("changeScrollLeft", this.$onScrollLeftChange); - - this.selection = session.getSelection(); - this.selection.addEventListener("changeCursor", this.$onCursorChange); - - this.$onSelectionChange = this.onSelectionChange.bind(this); - this.selection.addEventListener("changeSelection", this.$onSelectionChange); - - this.onChangeMode(); - - this.$blockScrolling += 1; - this.onCursorChange(); - this.$blockScrolling -= 1; - - this.onScrollTopChange(); - this.onScrollLeftChange(); - this.onSelectionChange(); - this.onChangeFrontMarker(); - this.onChangeBackMarker(); - this.onChangeBreakpoint(); - this.onChangeAnnotation(); - this.session.getUseWrapMode() && this.renderer.adjustWrapLimit(); - this.renderer.updateFull(); - - this._emit("changeSession", { + this._signal("changeSession", { session: session, oldSession: oldSession }); + + oldSession && oldSession._signal("changeEditor", {oldEditor: this}); + session && session._signal("changeEditor", {editor: this}); }; /** * Returns the current session being used. * @returns {EditSession} **/ this.getSession = function() { return this.session; }; - /** + /** * Sets the current document to `val`. * @param {String} val The new value to set for the document * @param {Number} cursorPos Where to set the new value. `undefined` or 0 is selectAll, -1 is at the document start, and 1 is at the end * - * - * * @returns {String} The current document value * @related Document.setValue **/ this.setValue = function(val, cursorPos) { this.session.doc.setValue(val); @@ -255,53 +405,52 @@ this.navigateFileStart(); return val; }; - /** + /** * Returns the current session's content. * * @returns {String} * @related EditSession.getValue **/ this.getValue = function() { return this.session.getValue(); }; /** - * + * * Returns the currently highlighted selection. * @returns {String} The highlighted selection **/ this.getSelection = function() { return this.selection; }; - /** + /** * {:VirtualRenderer.onResize} * @param {Boolean} force If `true`, recomputes the size, even if the height and width haven't changed * - * + * * @related VirtualRenderer.onResize **/ this.resize = function(force) { this.renderer.onResize(force); }; /** * {:VirtualRenderer.setTheme} * @param {String} theme The path to a theme - * - * + * @param {Function} cb optional callback called when theme is loaded **/ - this.setTheme = function(theme) { - this.renderer.setTheme(theme); + this.setTheme = function(theme, cb) { + this.renderer.setTheme(theme, cb); }; - /** + /** * {:VirtualRenderer.getTheme} - * + * * @returns {String} The set theme * @related VirtualRenderer.getTheme **/ this.getTheme = function() { return this.renderer.getTheme(); @@ -309,34 +458,41 @@ /** * {:VirtualRenderer.setStyle} * @param {String} style A class name * - * + * * @related VirtualRenderer.setStyle **/ this.setStyle = function(style) { this.renderer.setStyle(style); }; - /** + /** * {:VirtualRenderer.unsetStyle} * @related VirtualRenderer.unsetStyle **/ this.unsetStyle = function(style) { this.renderer.unsetStyle(style); }; /** + * Gets the current font size of the editor text. + */ + this.getFontSize = function () { + return this.getOption("fontSize") || + dom.computedStyle(this.container, "fontSize"); + }; + + /** * Set a new font size (in pixels) for the editor text. - * @param {Number} size A font size - * - * + * @param {String} size A font size ( _e.g._ "12px") + * + * **/ this.setFontSize = function(size) { - this.container.style.fontSize = size; - this.renderer.updateFontSize(); + this.setOption("fontSize", size); }; this.$highlightBrackets = function() { if (this.session.$bracketHighlight) { this.session.removeMarker(this.session.$bracketHighlight); @@ -350,21 +506,114 @@ // perform highlight async to not block the browser during navigation var self = this; this.$highlightPending = true; setTimeout(function() { self.$highlightPending = false; - - var pos = self.session.findMatchingBracket(self.getCursorPosition()); + var session = self.session; + if (!session || !session.bgTokenizer) return; + var pos = session.findMatchingBracket(self.getCursorPosition()); if (pos) { - var range = new Range(pos.row, pos.column, pos.row, pos.column+1); - self.session.$bracketHighlight = self.session.addMarker(range, "ace_bracket", "text"); + var range = new Range(pos.row, pos.column, pos.row, pos.column + 1); + } else if (session.$mode.getMatching) { + var range = session.$mode.getMatching(self.session); } + if (range) + session.$bracketHighlight = session.addMarker(range, "ace_bracket", "text"); }, 50); }; + // todo: move to mode.getMatching + this.$highlightTags = function() { + if (this.$highlightTagPending) + return; + + // perform highlight async to not block the browser during navigation + var self = this; + this.$highlightTagPending = true; + setTimeout(function() { + self.$highlightTagPending = false; + + var session = self.session; + if (!session || !session.bgTokenizer) return; + + var pos = self.getCursorPosition(); + var iterator = new TokenIterator(self.session, pos.row, pos.column); + var token = iterator.getCurrentToken(); + + if (!token || !/\b(?:tag-open|tag-name)/.test(token.type)) { + session.removeMarker(session.$tagHighlight); + session.$tagHighlight = null; + return; + } + + if (token.type.indexOf("tag-open") != -1) { + token = iterator.stepForward(); + if (!token) + return; + } + + var tag = token.value; + var depth = 0; + var prevToken = iterator.stepBackward(); + + if (prevToken.value == '<'){ + //find closing tag + do { + prevToken = token; + token = iterator.stepForward(); + + if (token && token.value === tag && token.type.indexOf('tag-name') !== -1) { + if (prevToken.value === '<'){ + depth++; + } else if (prevToken.value === '</'){ + depth--; + } + } + + } while (token && depth >= 0); + } else { + //find opening tag + do { + token = prevToken; + prevToken = iterator.stepBackward(); + + if (token && token.value === tag && token.type.indexOf('tag-name') !== -1) { + if (prevToken.value === '<') { + depth++; + } else if (prevToken.value === '</') { + depth--; + } + } + } while (prevToken && depth <= 0); + + //select tag again + iterator.stepForward(); + } + + if (!token) { + session.removeMarker(session.$tagHighlight); + session.$tagHighlight = null; + return; + } + + var row = iterator.getCurrentTokenRow(); + var column = iterator.getCurrentTokenColumn(); + var range = new Range(row, column, row, column+token.value.length); + + //remove range if different + if (session.$tagHighlight && range.compareRange(session.$backMarkers[session.$tagHighlight].range)!==0) { + session.removeMarker(session.$tagHighlight); + session.$tagHighlight = null; + } + + if (range && !session.$tagHighlight) + session.$tagHighlight = session.addMarker(range, "ace_bracket", "text"); + }, 50); + }; + /** - * + * * Brings the current `textInput` into focus. **/ this.focus = function() { // Safari needs the timeout // iOS and Firefox need it called immediately @@ -376,81 +625,82 @@ this.textInput.focus(); }; /** * Returns `true` if the current `textInput` is in focus. - * @return Boolean + * @return {Boolean} **/ this.isFocused = function() { return this.textInput.isFocused(); }; /** - * + * * Blurs the current `textInput`. **/ this.blur = function() { this.textInput.blur(); }; /** * Emitted once the editor comes into focus. - * @event focus - * - * + * @event focus + * + * **/ - this.onFocus = function() { + this.onFocus = function(e) { if (this.$isFocused) return; this.$isFocused = true; this.renderer.showCursor(); this.renderer.visualizeFocus(); - this._emit("focus"); + this._emit("focus", e); }; /** * Emitted once the editor has been blurred. * @event blur - * - * + * + * **/ - this.onBlur = function() { + this.onBlur = function(e) { if (!this.$isFocused) return; this.$isFocused = false; this.renderer.hideCursor(); this.renderer.visualizeBlur(); - this._emit("blur"); + this._emit("blur", e); }; this.$cursorChange = function() { this.renderer.updateCursor(); }; /** - * Emitted whenever the document is changed. + * Emitted whenever the document is changed. * @event change * @param {Object} e Contains a single property, `data`, which has the delta of changes * * - * + * **/ this.onDocumentChange = function(e) { var delta = e.data; var range = delta.range; var lastRow; if (range.start.row == range.end.row && delta.action != "insertLines" && delta.action != "removeLines") lastRow = range.end.row; else lastRow = Infinity; - this.renderer.updateLines(range.start.row, lastRow); + this.renderer.updateLines(range.start.row, lastRow, this.session.$useWrapMode); - this._emit("change", e); + this._signal("change", e); // update cursor because tab characters can influence the cursor position this.$cursorChange(); + this.$updateHighlightActiveLine(); }; this.onTokenizerUpdate = function(e) { var rows = e.data; this.renderer.updateLines(rows.first, rows.last); @@ -458,49 +708,59 @@ this.onScrollTopChange = function() { this.renderer.scrollToY(this.session.getScrollTop()); }; - + this.onScrollLeftChange = function() { this.renderer.scrollToX(this.session.getScrollLeft()); }; /** * Emitted when the selection changes. - * + * **/ this.onCursorChange = function() { this.$cursorChange(); if (!this.$blockScrolling) { + config.warn("Automatically scrolling cursor into view after selection change", + "this will be disabled in the next version", + "set editor.$blockScrolling = Infinity to disable this message" + ); this.renderer.scrollCursorIntoView(); } this.$highlightBrackets(); + this.$highlightTags(); this.$updateHighlightActiveLine(); - this._emit("changeSelection"); + this._signal("changeSelection"); }; this.$updateHighlightActiveLine = function() { var session = this.getSession(); var highlight; if (this.$highlightActiveLine) { if ((this.$selectionStyle != "line" || !this.selection.isMultiLine())) highlight = this.getCursorPosition(); + if (this.renderer.$maxLines && this.session.getLength() === 1 && !(this.renderer.$minLines > 1)) + highlight = false; } if (session.$highlightLineMarker && !highlight) { session.removeMarker(session.$highlightLineMarker.id); - session.$highlightLineMarker = null; + session.$highlightLineMarker = null; } else if (!session.$highlightLineMarker && highlight) { - session.$highlightLineMarker = session.highlightLines(highlight.row, highlight.row, "ace_active-line"); + var range = new Range(highlight.row, highlight.column, highlight.row, Infinity); + range.id = session.addMarker(range, "ace_active-line", "screenLine"); + session.$highlightLineMarker = range; } else if (highlight) { session.$highlightLineMarker.start.row = highlight.row; session.$highlightLineMarker.end.row = highlight.row; - session._emit("changeBackMarker"); + session.$highlightLineMarker.start.column = highlight.column; + session._signal("changeBackMarker"); } }; this.onSelectionChange = function(e) { var session = this.session; @@ -516,14 +776,14 @@ session.$selectionMarker = session.addMarker(range, "ace_selection", style); } else { this.$updateHighlightActiveLine(); } - var re = this.$highlightSelectedWord && this.$getSelectionHighLightRegexp() + var re = this.$highlightSelectedWord && this.$getSelectionHighLightRegexp(); this.session.highlight(re); - - this._emit("changeSelection"); + + this._signal("changeSelection"); }; this.$getSelectionHighLightRegexp = function() { var session = this.session; @@ -573,12 +833,13 @@ this.onChangeAnnotation = function() { this.renderer.setAnnotations(this.session.getAnnotations()); }; - this.onChangeMode = function() { + this.onChangeMode = function(e) { this.renderer.updateText(); + this._emit("changeMode", e); }; this.onChangeWrapLimit = function() { this.renderer.updateFull(); @@ -595,29 +856,33 @@ this.$updateHighlightActiveLine(); // TODO: This might be too much updating. Okay for now. this.renderer.updateFull(); }; + /** + * Returns the string of text currently highlighted. + * @returns {String} + **/ + this.getSelectedText = function() { + return this.session.getTextRange(this.getSelectionRange()); + }; + + /** * Emitted when text is copied. - * @event copy + * @event copy * @param {String} text The copied text * - * **/ /** - * * Returns the string of text currently highlighted. * @returns {String} + * @deprecated Use getSelectedText instead. **/ - this.getCopyText = function() { - var text = ""; - if (!this.selection.isEmpty()) - text = this.session.getTextRange(this.getSelectionRange()); - - this._emit("copy", text); + var text = this.getSelectedText(); + this._signal("copy", text); return text; }; /** * Called whenever a text "copy" happens. @@ -634,11 +899,11 @@ }; /** * Emitted when text is pasted. * @event paste - * @param {String} text The pasted text + * @param {Object} an object which contains one property, `text`, that represents the text to be pasted. Editing this property will alter the text that is pasted. * * **/ /** * Called whenever a text "paste" happens. @@ -646,52 +911,85 @@ * * **/ this.onPaste = function(text) { // todo this should change when paste becomes a command - if (this.$readOnly) + if (this.$readOnly) return; - this._emit("paste", text); - this.insert(text); + + var e = {text: text}; + this._signal("paste", e); + text = e.text; + if (!this.inMultiSelectMode || this.inVirtualSelectionMode) { + this.insert(text); + } else { + var lines = text.split(/\r\n|\r|\n/); + var ranges = this.selection.rangeList.ranges; + + if (lines.length > ranges.length || lines.length < 2 || !lines[1]) + return this.commands.exec("insertstring", this, text); + + for (var i = ranges.length; i--;) { + var range = ranges[i]; + if (!range.isEmpty()) + this.session.remove(range); + + this.session.insert(range.start, lines[i]); + } + } + this.renderer.scrollCursorIntoView(); }; - this.execCommand = function(command, args) { - this.commands.exec(command, this, args); + return this.commands.exec(command, this, args); }; /** * Inserts `text` into wherever the cursor is pointing. * @param {String} text The new text to add - * - * + * **/ - this.insert = function(text) { + this.insert = function(text, pasted) { var session = this.session; var mode = session.getMode(); var cursor = this.getCursorPosition(); - if (this.getBehavioursEnabled()) { + if (this.getBehavioursEnabled() && !pasted) { // Get a transform if the current mode wants one. var transform = mode.transformAction(session.getState(cursor.row), 'insertion', this, session, text); - if (transform) + if (transform) { + if (text !== transform.text) { + this.session.mergeUndoDeltas = false; + this.$mergeNextCommand = false; + } text = transform.text; + + } } + + if (text == "\t") + text = this.session.getTabString(); - text = text.replace("\t", this.session.getTabString()); - // remove selected text if (!this.selection.isEmpty()) { - cursor = this.session.remove(this.getSelectionRange()); + var range = this.getSelectionRange(); + cursor = this.session.remove(range); this.clearSelection(); } else if (this.session.getOverwrite()) { var range = new Range.fromPoints(cursor, cursor); range.end.column += text.length; this.session.remove(range); } + if (text == "\n" || text == "\r\n") { + var line = session.getLine(cursor.row); + if (cursor.column > line.search(/\S|$/)) { + var d = line.substr(cursor.column).search(/\S|$/); + session.doc.removeInLine(cursor.row, cursor.column, cursor.column + d); + } + } this.clearSelection(); var start = cursor.column; var lineState = session.getState(cursor.row); var line = session.getLine(cursor.row); @@ -710,48 +1008,14 @@ cursor.row + transform.selection[2], transform.selection[3])); } } - // TODO disabled multiline auto indent - // possibly doing the indent before inserting the text - // if (cursor.row !== end.row) { if (session.getDocument().isNewLine(text)) { var lineIndent = mode.getNextLineIndent(lineState, line.slice(0, cursor.column), session.getTabString()); - this.moveCursorTo(cursor.row+1, 0); - - var size = session.getTabSize(); - var minIndent = Number.MAX_VALUE; - - for (var row = cursor.row + 1; row <= end.row; ++row) { - var indent = 0; - - line = session.getLine(row); - for (var i = 0; i < line.length; ++i) - if (line.charAt(i) == '\t') - indent += size; - else if (line.charAt(i) == ' ') - indent += 1; - else - break; - if (/[^\s]/.test(line)) - minIndent = Math.min(indent, minIndent); - } - - for (var row = cursor.row + 1; row <= end.row; ++row) { - var outdent = minIndent; - - line = session.getLine(row); - for (var i = 0; i < line.length && outdent > 0; ++i) - if (line.charAt(i) == '\t') - outdent -= size; - else if (line.charAt(i) == ' ') - outdent -= 1; - session.remove(new Range(row, 0, row, i)); - } - session.indentRows(cursor.row + 1, end.row, lineIndent); + session.insert({row: cursor.row+1, column: 0}, lineIndent); } if (shouldOutdent) mode.autoOutdent(lineState, session, cursor.row); }; @@ -761,162 +1025,121 @@ this.onCommandKey = function(e, hashId, keyCode) { this.keyBinding.onCommandKey(e, hashId, keyCode); }; - /** + /** * Pass in `true` to enable overwrites in your session, or `false` to disable. If overwrites is enabled, any text you enter will type over any text after it. If the value of `overwrite` changes, this function also emites the `changeOverwrite` event. * @param {Boolean} overwrite Defines wheter or not to set overwrites - * * + * * @related EditSession.setOverwrite **/ this.setOverwrite = function(overwrite) { this.session.setOverwrite(overwrite); }; - /** + /** * Returns `true` if overwrites are enabled; `false` otherwise. * @returns {Boolean} * @related EditSession.getOverwrite **/ this.getOverwrite = function() { return this.session.getOverwrite(); }; - /** + /** * Sets the value of overwrite to the opposite of whatever it currently is. * @related EditSession.toggleOverwrite **/ this.toggleOverwrite = function() { this.session.toggleOverwrite(); }; /** * Sets how fast the mouse scrolling should do. - * @param {Number} speed A value indicating the new speed - * - * - * + * @param {Number} speed A value indicating the new speed (in milliseconds) **/ this.setScrollSpeed = function(speed) { - this.$mouseHandler.setScrollSpeed(speed); + this.setOption("scrollSpeed", speed); }; /** - * Returns the value indicating how fast the mouse scroll speed is. + * Returns the value indicating how fast the mouse scroll speed is (in milliseconds). * @returns {Number} **/ this.getScrollSpeed = function() { - return this.$mouseHandler.getScrollSpeed(); + return this.getOption("scrollSpeed"); }; /** * Sets the delay (in milliseconds) of the mouse drag. * @param {Number} dragDelay A value indicating the new delay - * - * - * **/ this.setDragDelay = function(dragDelay) { - this.$mouseHandler.setDragDelay(dragDelay); + this.setOption("dragDelay", dragDelay); }; /** * Returns the current mouse drag delay. * @returns {Number} **/ this.getDragDelay = function() { - return this.$mouseHandler.getDragDelay(); + return this.getOption("dragDelay"); }; - this.$selectionStyle = "line"; - /** * Emitted when the selection style changes, via [[Editor.setSelectionStyle]]. * @event changeSelectionStyle * @param {Object} data Contains one property, `data`, which indicates the new selection style - * - * - * **/ /** - * Indicates how selections should occur. - * - * By default, selections are set to "line". This function also emits the `'changeSelectionStyle'` event. + * Draw selection markers spanning whole line, or only over selected text. Default value is "line" + * @param {String} style The new selection style "line"|"text" * - * @param {String} style The new selection style - * - * **/ - this.setSelectionStyle = function(style) { - if (this.$selectionStyle == style) return; - - this.$selectionStyle = style; - this.onSelectionChange(); - this._emit("changeSelectionStyle", {data: style}); + this.setSelectionStyle = function(val) { + this.setOption("selectionStyle", val); }; /** * Returns the current selection style. * @returns {String} **/ this.getSelectionStyle = function() { - return this.$selectionStyle; + return this.getOption("selectionStyle"); }; - this.$highlightActiveLine = true; - /** * Determines whether or not the current line should be highlighted. * @param {Boolean} shouldHighlight Set to `true` to highlight the current line - * - * - * **/ this.setHighlightActiveLine = function(shouldHighlight) { - if (this.$highlightActiveLine == shouldHighlight) - return; - - this.$highlightActiveLine = shouldHighlight; - this.$updateHighlightActiveLine(); + this.setOption("highlightActiveLine", shouldHighlight); }; - /** * Returns `true` if current lines are always highlighted. - * @return Boolean + * @return {Boolean} **/ this.getHighlightActiveLine = function() { - return this.$highlightActiveLine; + return this.getOption("highlightActiveLine"); }; - - this.$highlightGutterLine = true; this.setHighlightGutterLine = function(shouldHighlight) { - if (this.$highlightGutterLine == shouldHighlight) - return; - - this.renderer.setHighlightGutterLine(shouldHighlight); - this.$highlightGutterLine = shouldHighlight; + this.setOption("highlightGutterLine", shouldHighlight); }; this.getHighlightGutterLine = function() { - return this.$highlightGutterLine; + return this.getOption("highlightGutterLine"); }; - this.$highlightSelectedWord = true; /** * Determines if the currently selected word should be highlighted. * @param {Boolean} shouldHighlight Set to `true` to highlight the currently selected word * - * **/ this.setHighlightSelectedWord = function(shouldHighlight) { - if (this.$highlightSelectedWord == shouldHighlight) - return; - - this.$highlightSelectedWord = shouldHighlight; - this.$onSelectionChange(); + this.setOption("highlightSelectedWord", shouldHighlight); }; /** * Returns `true` if currently highlighted words are to be highlighted. * @returns {Boolean} @@ -932,14 +1155,13 @@ this.getAnimatedScroll = function(){ return this.renderer.getAnimatedScroll(); }; /** - * If `showInvisibiles` is set to `true`, invisible characters&mdash;like spaces or new lines&mdash;are show in the editor. + * If `showInvisibles` is set to `true`, invisible characters&mdash;like spaces or new lines&mdash;are show in the editor. * @param {Boolean} showInvisibles Specifies whether or not to show invisible characters - * - * + * **/ this.setShowInvisibles = function(showInvisibles) { this.renderer.setShowInvisibles(showInvisibles); }; @@ -960,12 +1182,11 @@ }; /** * If `showPrintMargin` is set to `true`, the print margin is shown in the editor. * @param {Boolean} showPrintMargin Specifies whether or not to show the print margin - * - * + * **/ this.setShowPrintMargin = function(showPrintMargin) { this.renderer.setShowPrintMargin(showPrintMargin); }; @@ -979,12 +1200,10 @@ /** * Sets the column defining where the print margin should be. * @param {Number} showPrintMargin Specifies the new print margin * - * - * **/ this.setPrintMarginColumn = function(showPrintMargin) { this.renderer.setPrintMarginColumn(showPrintMargin); }; @@ -994,107 +1213,89 @@ **/ this.getPrintMarginColumn = function() { return this.renderer.getPrintMarginColumn(); }; - this.$readOnly = false; /** * If `readOnly` is true, then the editor is set to read-only mode, and none of the content can change. * @param {Boolean} readOnly Specifies whether the editor can be modified or not - * - * + * **/ this.setReadOnly = function(readOnly) { - this.$readOnly = readOnly; + this.setOption("readOnly", readOnly); }; /** * Returns `true` if the editor is set to read-only mode. * @returns {Boolean} **/ this.getReadOnly = function() { - return this.$readOnly; + return this.getOption("readOnly"); }; - this.$modeBehaviours = true; - /** * Specifies whether to use behaviors or not. ["Behaviors" in this case is the auto-pairing of special characters, like quotation marks, parenthesis, or brackets.]{: #BehaviorsDef} * @param {Boolean} enabled Enables or disables behaviors - * - * + * **/ this.setBehavioursEnabled = function (enabled) { - this.$modeBehaviours = enabled; + this.setOption("behavioursEnabled", enabled); }; /** * Returns `true` if the behaviors are currently enabled. {:BehaviorsDef} - * + * * @returns {Boolean} **/ this.getBehavioursEnabled = function () { - return this.$modeBehaviours; + return this.getOption("behavioursEnabled"); }; - this.$modeWrapBehaviours = true; - /** - * Editor.setWrapBehavioursEnabled(enabled) - * - enabled (Boolean): Enables or disables wrapping behaviors - * * Specifies whether to use wrapping behaviors or not, i.e. automatically wrapping the selection with characters such as brackets * when such a character is typed in. + * @param {Boolean} enabled Enables or disables wrapping behaviors + * **/ this.setWrapBehavioursEnabled = function (enabled) { - this.$modeWrapBehaviours = enabled; + this.setOption("wrapBehavioursEnabled", enabled); }; /** * Returns `true` if the wrapping behaviors are currently enabled. **/ this.getWrapBehavioursEnabled = function () { - return this.$modeWrapBehaviours; + return this.getOption("wrapBehavioursEnabled"); }; /** - * Indicates whether the fold widgets are shown or not. + * Indicates whether the fold widgets should be shown or not. * @param {Boolean} show Specifies whether the fold widgets are shown - * - * **/ this.setShowFoldWidgets = function(show) { - var gutter = this.renderer.$gutterLayer; - if (gutter.getShowFoldWidgets() == show) - return; + this.setOption("showFoldWidgets", show); - this.renderer.$gutterLayer.setShowFoldWidgets(show); - this.$showFoldWidgets = show; - this.renderer.updateFull(); }; - /** * Returns `true` if the fold widgets are shown. - * @return Boolean + * @return {Boolean} **/ this.getShowFoldWidgets = function() { - return this.renderer.$gutterLayer.getShowFoldWidgets(); + return this.getOption("showFoldWidgets"); }; - this.setFadeFoldWidgets = function(show) { - this.renderer.setFadeFoldWidgets(show); + this.setFadeFoldWidgets = function(fade) { + this.setOption("fadeFoldWidgets", fade); }; this.getFadeFoldWidgets = function() { - return this.renderer.getFadeFoldWidgets(); + return this.getOption("fadeFoldWidgets"); }; /** - * Removes words of text from the editor. A "word" is defined as a string of characters bookended by whitespace. + * Removes the current selection or one character. * @param {String} dir The direction of the deletion to occur, either "left" or "right" - * - * * **/ this.remove = function(dir) { if (this.selection.isEmpty()){ if (dir == "left") @@ -1106,10 +1307,20 @@ var range = this.getSelectionRange(); if (this.getBehavioursEnabled()) { var session = this.session; var state = session.getState(range.start.row); var new_range = session.getMode().transformAction(state, 'deletion', this, session, range); + + if (range.end.column === 0) { + var text = session.getTextRange(range); + if (text[text.length - 1] == "\n") { + var line = session.getLine(range.end.row); + if (/^\s+$/.test(line)) { + range.end.column = line.length; + } + } + } if (new_range) range = new_range; } this.session.remove(range); @@ -1234,39 +1445,62 @@ var text = this.session.getTextRange(range); this.session.replace(range, text.toUpperCase()); this.selection.setSelectionRange(originalRange); }; - /** - * Indents the current line. - * + /** + * Inserts an indentation into the current cursor position or indents the selected lines. + * * @related EditSession.indentRows **/ this.indent = function() { var session = this.session; var range = this.getSelectionRange(); - if (range.start.row < range.end.row || range.start.column < range.end.column) { + if (range.start.row < range.end.row) { var rows = this.$getSelectedRows(); session.indentRows(rows.first, rows.last, "\t"); - } else { - var indentString; + return; + } else if (range.start.column < range.end.column) { + var text = session.getTextRange(range); + if (!/^\s+$/.test(text)) { + var rows = this.$getSelectedRows(); + session.indentRows(rows.first, rows.last, "\t"); + return; + } + } + + var line = session.getLine(range.start.row); + var position = range.start; + var size = session.getTabSize(); + var column = session.documentToScreenColumn(position.row, position.column); - if (this.session.getUseSoftTabs()) { - var size = session.getTabSize(), - position = this.getCursorPosition(), - column = session.documentToScreenColumn(position.row, position.column), - count = (size - column % size); - - indentString = lang.stringRepeat(" ", count); - } else - indentString = "\t"; - return this.insert(indentString); + if (this.session.getUseSoftTabs()) { + var count = (size - column % size); + var indentString = lang.stringRepeat(" ", count); + } else { + var count = column % size; + while (line[range.start.column] == " " && count) { + range.start.column--; + count--; + } + this.selection.setSelectionRange(range); + indentString = "\t"; } + return this.insert(indentString); }; - /** + /** + * Indents the current line. + * @related EditSession.indentRows + **/ + this.blockIndent = function() { + var rows = this.$getSelectedRows(); + this.session.indentRows(rows.first, rows.last, "\t"); + }; + + /** * Outdents the current line. * @related EditSession.outdentRows **/ this.blockOutdent = function() { var selection = this.session.getSelection(); @@ -1297,48 +1531,52 @@ session.replace(deleteRange, lines[i-rows.first]); } }; /** - * * Given the currently selected range, this function either comments all the lines, or uncomments all of them. **/ this.toggleCommentLines = function() { var state = this.session.getState(this.getCursorPosition().row); var rows = this.$getSelectedRows(); this.session.getMode().toggleCommentLines(state, this.session, rows.first, rows.last); }; + this.toggleBlockComment = function() { + var cursor = this.getCursorPosition(); + var state = this.session.getState(cursor.row); + var range = this.getSelectionRange(); + this.session.getMode().toggleBlockComment(state, this.session, range, cursor); + }; + /** * Works like [[EditSession.getTokenAt]], except it returns a number. * @returns {Number} **/ - this.getNumberAt = function( row, column ) { - var _numberRx = /[\-]?[0-9]+(?:\.[0-9]+)?/g - _numberRx.lastIndex = 0 + this.getNumberAt = function(row, column) { + var _numberRx = /[\-]?[0-9]+(?:\.[0-9]+)?/g; + _numberRx.lastIndex = 0; - var s = this.session.getLine(row) - while(_numberRx.lastIndex < column - 1 ){ - var m = _numberRx.exec(s) + var s = this.session.getLine(row); + while (_numberRx.lastIndex < column) { + var m = _numberRx.exec(s); if(m.index <= column && m.index+m[0].length >= column){ var number = { value: m[0], start: m.index, end: m.index+m[0].length - - } - return number + }; + return number; } } return null; }; - + /** - * Editor.modifyNumber(amount) + * If the character before the cursor is a number, this functions changes its value by `amount`. * @param {Number} amount The value to change the numeral by (can be negative to decrease value) * - * If the character before the cursor is a number, this functions changes its value by `amount`. **/ this.modifyNumber = function(amount) { var row = this.selection.getCursor().row; var column = this.selection.getCursor().column; @@ -1355,18 +1593,18 @@ var fp = nr.value.indexOf(".") >= 0 ? nr.start + nr.value.indexOf(".") + 1 : nr.end; var decimals = nr.start + nr.value.length - fp; var t = parseFloat(nr.value); t *= Math.pow(10, decimals); - + if(fp !== nr.end && column < fp){ amount *= Math.pow(10, nr.end - column - 1); } else { amount *= Math.pow(10, nr.end - column); } - + t += amount; t /= Math.pow(10, decimals); var nnr = t.toFixed(decimals); //update number @@ -1377,12 +1615,12 @@ this.moveCursorTo(row, Math.max(nr.start +1, column + nnr.length - nr.value.length)); } } }; - - /** + + /** * Removes all the lines in the current selection * @related EditSession.remove **/ this.removeLines = function() { var rows = this.$getSelectedRows(); @@ -1400,132 +1638,147 @@ this.duplicateSelection = function() { var sel = this.selection; var doc = this.session; var range = sel.getRange(); + var reverse = sel.isBackwards(); if (range.isEmpty()) { var row = range.start.row; doc.duplicateLines(row, row); } else { - var reverse = sel.isBackwards() - var point = sel.isBackwards() ? range.start : range.end; + var point = reverse ? range.start : range.end; var endPoint = doc.insert(point, doc.getTextRange(range), false); range.start = point; range.end = endPoint; - - sel.setSelectionRange(range, reverse) + + sel.setSelectionRange(range, reverse); } }; - - /** + + /** * Shifts all the selected lines down one row. * * @returns {Number} On success, it returns -1. * @related EditSession.moveLinesUp **/ this.moveLinesDown = function() { - this.$moveLines(function(firstRow, lastRow) { - return this.session.moveLinesDown(firstRow, lastRow); - }); + this.$moveLines(1, false); }; - /** + /** * Shifts all the selected lines up one row. * @returns {Number} On success, it returns -1. * @related EditSession.moveLinesDown **/ this.moveLinesUp = function() { - this.$moveLines(function(firstRow, lastRow) { - return this.session.moveLinesUp(firstRow, lastRow); - }); + this.$moveLines(-1, false); }; - /** + /** * Moves a range of text from the given range to the given position. `toPosition` is an object that looks like this: * ```json * { row: newRowLocation, column: newColumnLocation } * ``` * @param {Range} fromRange The range of text you want moved within the document * @param {Object} toPosition The location (row and column) where you want to move the text to - * + * * @returns {Range} The new range where the text was moved to. * @related EditSession.moveText **/ - this.moveText = function(range, toPosition) { - if (this.$readOnly) - return null; - - return this.session.moveText(range, toPosition); + this.moveText = function(range, toPosition, copy) { + return this.session.moveText(range, toPosition, copy); }; - /** + /** * Copies all the selected lines up one row. * @returns {Number} On success, returns 0. - * + * **/ this.copyLinesUp = function() { - this.$moveLines(function(firstRow, lastRow) { - this.session.duplicateLines(firstRow, lastRow); - return 0; - }); + this.$moveLines(-1, true); }; - /** + /** * Copies all the selected lines down one row. * @returns {Number} On success, returns the number of new rows added; in other words, `lastRow - firstRow + 1`. * @related EditSession.duplicateLines * **/ this.copyLinesDown = function() { - this.$moveLines(function(firstRow, lastRow) { - return this.session.duplicateLines(firstRow, lastRow); - }); + this.$moveLines(1, true); }; - /** - * Executes a specific function, which can be anything that manipulates selected lines, such as copying them, duplicating them, or shifting them. - * @param {Function} mover A method to call on each selected row - * + * for internal use + * @ignore * **/ - this.$moveLines = function(mover) { - var rows = this.$getSelectedRows(); + this.$moveLines = function(dir, copy) { + var rows, moved; var selection = this.selection; - if (!selection.isMultiLine()) { - var range = selection.getRange(); - var reverse = selection.isBackwards(); + if (!selection.inMultiSelectMode || this.inVirtualSelectionMode) { + var range = selection.toOrientedRange(); + rows = this.$getSelectedRows(range); + moved = this.session.$moveLines(rows.first, rows.last, copy ? 0 : dir); + if (copy && dir == -1) moved = 0; + range.moveBy(moved, 0); + selection.fromOrientedRange(range); + } else { + var ranges = selection.rangeList.ranges; + selection.rangeList.detach(this.session); + this.inVirtualSelectionMode = true; + + var diff = 0; + var totalDiff = 0; + var l = ranges.length; + for (var i = 0; i < l; i++) { + var rangeIndex = i; + ranges[i].moveBy(diff, 0); + rows = this.$getSelectedRows(ranges[i]); + var first = rows.first; + var last = rows.last; + while (++i < l) { + if (totalDiff) ranges[i].moveBy(totalDiff, 0); + var subRows = this.$getSelectedRows(ranges[i]); + if (copy && subRows.first != last) + break; + else if (!copy && subRows.first > last + 1) + break; + last = subRows.last; + } + i--; + diff = this.session.$moveLines(first, last, copy ? 0 : dir); + if (copy && dir == -1) rangeIndex = i + 1; + while (rangeIndex <= i) { + ranges[rangeIndex].moveBy(diff, 0); + rangeIndex++; + } + if (!copy) diff = 0; + totalDiff += diff; + } + + selection.fromOrientedRange(selection.ranges[0]); + selection.rangeList.attach(this.session); + this.inVirtualSelectionMode = false; } - - var linesMoved = mover.call(this, rows.first, rows.last); - - if (range) { - range.start.row += linesMoved; - range.end.row += linesMoved; - selection.setSelectionRange(range, reverse); - } - else { - selection.setSelectionAnchor(rows.last+linesMoved+1, 0); - selection.$moveSelection(function() { - selection.moveCursorTo(rows.first+linesMoved, 0); - }); - } }; /** * Returns an object indicating the currently selected rows. The object looks like this: + * * ```json * { first: range.start.row, last: range.end.row } * ``` + * * @returns {Object} **/ - this.$getSelectedRows = function() { - var range = this.getSelectionRange().collapseRows(); + this.$getSelectedRows = function(range) { + range = (range || this.getSelectionRange()).collapseRows(); return { - first: range.start.row, - last: range.end.row + first: this.session.getRowFoldStart(range.start.row), + last: this.session.getRowFoldEnd(range.end.row) }; }; this.onCompositionStart = function(text) { this.renderer.showComposition(this.getCursorPosition()); @@ -1537,43 +1790,45 @@ this.onCompositionEnd = function() { this.renderer.hideComposition(); }; - /** + /** * {:VirtualRenderer.getFirstVisibleRow} + * * @returns {Number} * @related VirtualRenderer.getFirstVisibleRow **/ this.getFirstVisibleRow = function() { return this.renderer.getFirstVisibleRow(); }; - /** + /** * {:VirtualRenderer.getLastVisibleRow} + * * @returns {Number} * @related VirtualRenderer.getLastVisibleRow **/ this.getLastVisibleRow = function() { return this.renderer.getLastVisibleRow(); }; /** * Indicates if the row is currently visible on the screen. * @param {Number} row The row to check - * + * * @returns {Boolean} **/ this.isRowVisible = function(row) { return (row >= this.getFirstVisibleRow() && row <= this.getLastVisibleRow()); }; /** * Indicates if the entire row is currently visible on the screen. * @param {Number} row The row to check - * - * + * + * * @returns {Boolean} **/ this.isRowFullyVisible = function(row) { return (row >= this.renderer.getFirstFullyVisibleRow() && row <= this.renderer.getLastFullyVisibleRow()); }; @@ -1590,15 +1845,15 @@ var renderer = this.renderer; var config = this.renderer.layerConfig; var rows = dir * Math.floor(config.height / config.lineHeight); this.$blockScrolling++; - if (select == true) { + if (select === true) { this.selection.$moveSelection(function(){ this.moveCursorBy(rows, 0); }); - } else if (select == false) { + } else if (select === false) { this.selection.moveCursorBy(rows, 0); this.selection.clearSelection(); } this.$blockScrolling--; @@ -1651,26 +1906,26 @@ **/ this.scrollPageUp = function() { this.$moveByPage(-1); }; - /** + /** * Moves the editor to the specified row. * @related VirtualRenderer.scrollToRow **/ this.scrollToRow = function(row) { this.renderer.scrollToRow(row); }; - /** + /** * Scrolls to a line. If `center` is `true`, it puts the line in middle of screen (or attempts to). * @param {Number} line The line to scroll to - * @param {Boolean} center If `true` + * @param {Boolean} center If `true` * @param {Boolean} animate If `true` animates scrolling * @param {Function} callback Function to be called when the animation has finished * - * + * * @related VirtualRenderer.scrollToLine **/ this.scrollToLine = function(line, center, animate, callback) { this.renderer.scrollToLine(line, center, animate, callback); }; @@ -1681,65 +1936,66 @@ this.centerSelection = function() { var range = this.getSelectionRange(); var pos = { row: Math.floor(range.start.row + (range.end.row - range.start.row) / 2), column: Math.floor(range.start.column + (range.end.column - range.start.column) / 2) - } + }; this.renderer.alignCursor(pos, 0.5); }; - /** + /** * Gets the current position of the cursor. - * @returns {Object} An object that looks something like this: + * @returns {Object} An object that looks something like this: + * * ```json * { row: currRow, column: currCol } * ``` * * @related Selection.getCursor **/ this.getCursorPosition = function() { return this.selection.getCursor(); }; - /** + /** * Returns the screen position of the cursor. * @returns {Number} * @related EditSession.documentToScreenPosition **/ this.getCursorPositionScreen = function() { return this.session.documentToScreenPosition(this.getCursorPosition()); }; - /** + /** * {:Selection.getRange} * @returns {Range} * @related Selection.getRange **/ this.getSelectionRange = function() { return this.selection.getRange(); }; - /** + /** * Selects all the text in editor. * @related Selection.selectAll **/ this.selectAll = function() { this.$blockScrolling += 1; this.selection.selectAll(); this.$blockScrolling -= 1; }; - /** + /** * {:Selection.clearSelection} * @related Selection.clearSelection **/ this.clearSelection = function() { this.selection.clearSelection(); }; - /** + /** * Moves the cursor to the specified row and column. Note that this does not de-select the current selection. * @param {Number} row The new row number * @param {Number} column The new column number * * @@ -1747,117 +2003,256 @@ **/ this.moveCursorTo = function(row, column) { this.selection.moveCursorTo(row, column); }; - /** + /** * Moves the cursor to the position indicated by `pos.row` and `pos.column`. * @param {Object} pos An object with two properties, row and column - * * + * * @related Selection.moveCursorToPosition **/ this.moveCursorToPosition = function(pos) { this.selection.moveCursorToPosition(pos); }; - /** - * Moves the cursor's row and column to the next matching bracket. + /** + * Moves the cursor's row and column to the next matching bracket or HTML tag. * **/ - this.jumpToMatching = function(select) { + this.jumpToMatching = function(select, expand) { var cursor = this.getCursorPosition(); + var iterator = new TokenIterator(this.session, cursor.row, cursor.column); + var prevToken = iterator.getCurrentToken(); + var token = prevToken || iterator.stepForward(); - var range = this.session.getBracketRange(cursor); - if (!range) { - range = this.find({ - needle: /[{}()\[\]]/g, - preventScroll:true, - start: {row: cursor.row, column: cursor.column - 1} - }); - if (!range) + if (!token) return; + + //get next closing tag or bracket + var matchType; + var found = false; + var depth = {}; + var i = cursor.column - token.start; + var bracketType; + var brackets = { + ")": "(", + "(": "(", + "]": "[", + "[": "[", + "{": "{", + "}": "{" + }; + + do { + if (token.value.match(/[{}()\[\]]/g)) { + for (; i < token.value.length && !found; i++) { + if (!brackets[token.value[i]]) { + continue; + } + + bracketType = brackets[token.value[i]] + '.' + token.type.replace("rparen", "lparen"); + + if (isNaN(depth[bracketType])) { + depth[bracketType] = 0; + } + + switch (token.value[i]) { + case '(': + case '[': + case '{': + depth[bracketType]++; + break; + case ')': + case ']': + case '}': + depth[bracketType]--; + + if (depth[bracketType] === -1) { + matchType = 'bracket'; + found = true; + } + break; + } + } + } + else if (token && token.type.indexOf('tag-name') !== -1) { + if (isNaN(depth[token.value])) { + depth[token.value] = 0; + } + + if (prevToken.value === '<') { + depth[token.value]++; + } + else if (prevToken.value === '</') { + depth[token.value]--; + } + + if (depth[token.value] === -1) { + matchType = 'tag'; + found = true; + } + } + + if (!found) { + prevToken = token; + token = iterator.stepForward(); + i = 0; + } + } while (token && !found); + + //no match found + if (!matchType) + return; + + var range, pos; + if (matchType === 'bracket') { + range = this.session.getBracketRange(cursor); + if (!range) { + range = new Range( + iterator.getCurrentTokenRow(), + iterator.getCurrentTokenColumn() + i - 1, + iterator.getCurrentTokenRow(), + iterator.getCurrentTokenColumn() + i - 1 + ); + pos = range.start; + if (expand || pos.row === cursor.row && Math.abs(pos.column - cursor.column) < 2) + range = this.session.getBracketRange(pos); + } + } + else if (matchType === 'tag') { + if (token && token.type.indexOf('tag-name') !== -1) + var tag = token.value; + else return; - var pos = range.start; - if (pos.row == cursor.row && Math.abs(pos.column - cursor.column) < 2) - range = this.session.getBracketRange(pos); + + range = new Range( + iterator.getCurrentTokenRow(), + iterator.getCurrentTokenColumn() - 2, + iterator.getCurrentTokenRow(), + iterator.getCurrentTokenColumn() - 2 + ); + + //find matching tag + if (range.compare(cursor.row, cursor.column) === 0) { + found = false; + do { + token = prevToken; + prevToken = iterator.stepBackward(); + + if (prevToken) { + if (prevToken.type.indexOf('tag-close') !== -1) { + range.setEnd(iterator.getCurrentTokenRow(), iterator.getCurrentTokenColumn() + 1); + } + + if (token.value === tag && token.type.indexOf('tag-name') !== -1) { + if (prevToken.value === '<') { + depth[tag]++; + } + else if (prevToken.value === '</') { + depth[tag]--; + } + + if (depth[tag] === 0) + found = true; + } + } + } while (prevToken && !found); + } + + //we found it + if (token && token.type.indexOf('tag-name')) { + pos = range.start; + if (pos.row == cursor.row && Math.abs(pos.column - cursor.column) < 2) + pos = range.end; + } } - + pos = range && range.cursor || pos; if (pos) { if (select) { - if (range && range.isEqual(this.getSelectionRange())) + if (range && expand) { + this.selection.setRange(range); + } else if (range && range.isEqual(this.getSelectionRange())) { this.clearSelection(); - else + } else { this.selection.selectTo(pos.row, pos.column); + } } else { - this.clearSelection(); - this.moveCursorTo(pos.row, pos.column); + this.selection.moveTo(pos.row, pos.column); } } }; /** * Moves the cursor to the specified line number, and also into the indiciated column. * @param {Number} lineNumber The line number to go to * @param {Number} column A column number to go to * @param {Boolean} animate If `true` animates scolling - * + * **/ this.gotoLine = function(lineNumber, column, animate) { this.selection.clearSelection(); this.session.unfold({row: lineNumber - 1, column: column || 0}); this.$blockScrolling += 1; + // todo: find a way to automatically exit multiselect mode + this.exitMultiSelectMode && this.exitMultiSelectMode(); this.moveCursorTo(lineNumber - 1, column || 0); this.$blockScrolling -= 1; if (!this.isRowFullyVisible(lineNumber - 1)) this.scrollToLine(lineNumber - 1, true, animate); }; - /** + /** * Moves the cursor to the specified row and column. Note that this does de-select the current selection. * @param {Number} row The new row number * @param {Number} column The new column number * * * @related Editor.moveCursorTo **/ this.navigateTo = function(row, column) { - this.clearSelection(); - this.moveCursorTo(row, column); + this.selection.moveTo(row, column); }; /** * Moves the cursor up in the document the specified number of times. Note that this does de-select the current selection. * @param {Number} times The number of times to change navigation - * - * + * + * **/ this.navigateUp = function(times) { + if (this.selection.isMultiLine() && !this.selection.isBackwards()) { + var selectionStart = this.selection.anchor.getPosition(); + return this.moveCursorToPosition(selectionStart); + } this.selection.clearSelection(); - times = times || 1; - this.selection.moveCursorBy(-times, 0); + this.selection.moveCursorBy(-times || -1, 0); }; /** * Moves the cursor down in the document the specified number of times. Note that this does de-select the current selection. * @param {Number} times The number of times to change navigation - * - * + * + * **/ this.navigateDown = function(times) { + if (this.selection.isMultiLine() && this.selection.isBackwards()) { + var selectionEnd = this.selection.anchor.getPosition(); + return this.moveCursorToPosition(selectionEnd); + } this.selection.clearSelection(); - times = times || 1; - this.selection.moveCursorBy(times, 0); + this.selection.moveCursorBy(times || 1, 0); }; /** * Moves the cursor left in the document the specified number of times. Note that this does de-select the current selection. * @param {Number} times The number of times to change navigation - * - * + * + * **/ this.navigateLeft = function(times) { if (!this.selection.isEmpty()) { var selectionStart = this.getSelectionRange().start; this.moveCursorToPosition(selectionStart); @@ -1872,12 +2267,12 @@ }; /** * Moves the cursor right in the document the specified number of times. Note that this does de-select the current selection. * @param {Number} times The number of times to change navigation - * - * + * + * **/ this.navigateRight = function(times) { if (!this.selection.isEmpty()) { var selectionEnd = this.getSelectionRange().end; this.moveCursorToPosition(selectionEnd); @@ -1890,60 +2285,56 @@ } this.clearSelection(); }; /** - * + * * Moves the cursor to the start of the current line. Note that this does de-select the current selection. **/ this.navigateLineStart = function() { this.selection.moveCursorLineStart(); this.clearSelection(); }; /** - * + * * Moves the cursor to the end of the current line. Note that this does de-select the current selection. **/ this.navigateLineEnd = function() { this.selection.moveCursorLineEnd(); this.clearSelection(); }; /** - * + * * Moves the cursor to the end of the current file. Note that this does de-select the current selection. **/ this.navigateFileEnd = function() { - var scrollTop = this.renderer.scrollTop; this.selection.moveCursorFileEnd(); this.clearSelection(); - this.renderer.animateScrolling(scrollTop); }; /** - * + * * Moves the cursor to the start of the current file. Note that this does de-select the current selection. **/ this.navigateFileStart = function() { - var scrollTop = this.renderer.scrollTop; this.selection.moveCursorFileStart(); this.clearSelection(); - this.renderer.animateScrolling(scrollTop); }; /** - * + * * Moves the cursor to the word immediately to the right of the current position. Note that this does de-select the current selection. **/ this.navigateWordRight = function() { this.selection.moveCursorWordRight(); this.clearSelection(); }; /** - * + * * Moves the cursor to the word immediately to the left of the current position. Note that this does de-select the current selection. **/ this.navigateWordLeft = function() { this.selection.moveCursorWordLeft(); this.clearSelection(); @@ -1994,12 +2385,11 @@ return replaced; this.$blockScrolling += 1; var selection = this.getSelectionRange(); - this.clearSelection(); - this.selection.moveCursorTo(0, 0); + this.selection.moveTo(0, 0); for (var i = ranges.length - 1; i >= 0; --i) { if(this.$tryReplace(ranges[i], replacement)) { replaced++; } @@ -2020,20 +2410,20 @@ } else { return null; } }; - /** + /** * {:Search.getOptions} For more information on `options`, see [[Search `Search`]]. * @related Search.getOptions * @returns {Object} **/ this.getLastSearchOptions = function() { return this.$search.getOptions(); }; - /** + /** * Attempts to find `needle` within the document. For more information on `options`, see [[Search `Search`]]. * @param {String} needle The text to search for (optional) * @param {Object} options An object defining various search properties * @param {Boolean} animate If `true` animate scrolling * @@ -2077,11 +2467,11 @@ else range.end = range.start; this.selection.setRange(range); }; - /** + /** * Performs another search for `needle` in the document. For more information on `options`, see [[Search `Search`]]. * @param {Object} options search options * @param {Boolean} animate If `true` animate scrolling * * @@ -2089,11 +2479,11 @@ **/ this.findNext = function(options, animate) { this.find({skipCurrent: true, backwards: false}, options, animate); }; - /** + /** * Performs a search for `needle` backwards. For more information on `options`, see [[Search `Search`]]. * @param {Object} options search options * @param {Boolean} animate If `true` animate scrolling * * @@ -2109,44 +2499,195 @@ this.selection.setSelectionRange(range); this.$blockScrolling -= 1; var scrollTop = this.renderer.scrollTop; this.renderer.scrollSelectionIntoView(range.start, range.end, 0.5); - if (animate != false) + if (animate !== false) this.renderer.animateScrolling(scrollTop); }; - /** + /** * {:UndoManager.undo} * @related UndoManager.undo **/ this.undo = function() { this.$blockScrolling++; this.session.getUndoManager().undo(); this.$blockScrolling--; this.renderer.scrollCursorIntoView(null, 0.5); }; - /** + /** * {:UndoManager.redo} * @related UndoManager.redo **/ this.redo = function() { this.$blockScrolling++; this.session.getUndoManager().redo(); this.$blockScrolling--; this.renderer.scrollCursorIntoView(null, 0.5); }; - /** - * + /** + * * Cleans up the entire editor. **/ this.destroy = function() { this.renderer.destroy(); + this._signal("destroy", this); + if (this.session) { + this.session.destroy(); + } }; + /** + * Enables automatic scrolling of the cursor into view when editor itself is inside scrollable element + * @param {Boolean} enable default true + **/ + this.setAutoScrollEditorIntoView = function(enable) { + if (!enable) + return; + var rect; + var self = this; + var shouldScroll = false; + if (!this.$scrollAnchor) + this.$scrollAnchor = document.createElement("div"); + var scrollAnchor = this.$scrollAnchor; + scrollAnchor.style.cssText = "position:absolute"; + this.container.insertBefore(scrollAnchor, this.container.firstChild); + var onChangeSelection = this.on("changeSelection", function() { + shouldScroll = true; + }); + // needed to not trigger sync reflow + var onBeforeRender = this.renderer.on("beforeRender", function() { + if (shouldScroll) + rect = self.renderer.container.getBoundingClientRect(); + }); + var onAfterRender = this.renderer.on("afterRender", function() { + if (shouldScroll && rect && (self.isFocused() + || self.searchBox && self.searchBox.isFocused()) + ) { + var renderer = self.renderer; + var pos = renderer.$cursorLayer.$pixelPos; + var config = renderer.layerConfig; + var top = pos.top - config.offset; + if (pos.top >= 0 && top + rect.top < 0) { + shouldScroll = true; + } else if (pos.top < config.height && + pos.top + rect.top + config.lineHeight > window.innerHeight) { + shouldScroll = false; + } else { + shouldScroll = null; + } + if (shouldScroll != null) { + scrollAnchor.style.top = top + "px"; + scrollAnchor.style.left = pos.left + "px"; + scrollAnchor.style.height = config.lineHeight + "px"; + scrollAnchor.scrollIntoView(shouldScroll); + } + shouldScroll = rect = null; + } + }); + this.setAutoScrollEditorIntoView = function(enable) { + if (enable) + return; + delete this.setAutoScrollEditorIntoView; + this.removeEventListener("changeSelection", onChangeSelection); + this.renderer.removeEventListener("afterRender", onAfterRender); + this.renderer.removeEventListener("beforeRender", onBeforeRender); + }; + }; + + + this.$resetCursorStyle = function() { + var style = this.$cursorStyle || "ace"; + var cursorLayer = this.renderer.$cursorLayer; + if (!cursorLayer) + return; + cursorLayer.setSmoothBlinking(/smooth/.test(style)); + cursorLayer.isBlinking = !this.$readOnly && style != "wide"; + dom.setCssClass(cursorLayer.element, "ace_slim-cursors", /slim/.test(style)); + }; + }).call(Editor.prototype); + +config.defineOptions(Editor.prototype, "editor", { + selectionStyle: { + set: function(style) { + this.onSelectionChange(); + this._signal("changeSelectionStyle", {data: style}); + }, + initialValue: "line" + }, + highlightActiveLine: { + set: function() {this.$updateHighlightActiveLine();}, + initialValue: true + }, + highlightSelectedWord: { + set: function(shouldHighlight) {this.$onSelectionChange();}, + initialValue: true + }, + readOnly: { + set: function(readOnly) { + // disabled to not break vim mode! + // this.textInput.setReadOnly(readOnly); + this.$resetCursorStyle(); + }, + initialValue: false + }, + cursorStyle: { + set: function(val) { this.$resetCursorStyle(); }, + values: ["ace", "slim", "smooth", "wide"], + initialValue: "ace" + }, + mergeUndoDeltas: { + values: [false, true, "always"], + initialValue: true + }, + behavioursEnabled: {initialValue: true}, + wrapBehavioursEnabled: {initialValue: true}, + autoScrollEditorIntoView: { + set: function(val) {this.setAutoScrollEditorIntoView(val)} + }, + + hScrollBarAlwaysVisible: "renderer", + vScrollBarAlwaysVisible: "renderer", + highlightGutterLine: "renderer", + animatedScroll: "renderer", + showInvisibles: "renderer", + showPrintMargin: "renderer", + printMarginColumn: "renderer", + printMargin: "renderer", + fadeFoldWidgets: "renderer", + showFoldWidgets: "renderer", + showLineNumbers: "renderer", + showGutter: "renderer", + displayIndentGuides: "renderer", + fontSize: "renderer", + fontFamily: "renderer", + maxLines: "renderer", + minLines: "renderer", + scrollPastEnd: "renderer", + fixedWidthGutter: "renderer", + theme: "renderer", + + scrollSpeed: "$mouseHandler", + dragDelay: "$mouseHandler", + dragEnabled: "$mouseHandler", + focusTimout: "$mouseHandler", + tooltipFollowsMouse: "$mouseHandler", + + firstLineNumber: "session", + overwrite: "session", + newLineMode: "session", + useWorker: "session", + useSoftTabs: "session", + tabSize: "session", + wrap: "session", + foldStyle: "session", + mode: "session" +}); + exports.Editor = Editor; -}); \ No newline at end of file +});