lib/gollum/public/gollum/livepreview/js/ace/lib/ace/edit_session.js in gollum-3.1.2 vs lib/gollum/public/gollum/livepreview/js/ace/lib/ace/edit_session.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 @@ -29,32 +29,29 @@ * ***** END LICENSE BLOCK ***** */ define(function(require, exports, module) { "use strict"; -var config = require("./config"); var oop = require("./lib/oop"); var lang = require("./lib/lang"); -var net = require("./lib/net"); +var config = require("./config"); var EventEmitter = require("./lib/event_emitter").EventEmitter; var Selection = require("./selection").Selection; var TextMode = require("./mode/text").Mode; var Range = require("./range").Range; var Document = require("./document").Document; var BackgroundTokenizer = require("./background_tokenizer").BackgroundTokenizer; var SearchHighlight = require("./search_highlight").SearchHighlight; /** + * Stores all the data about [[Editor `Editor`]] state providing easy way to change editors state. * - * - * Stores all the data about [[Editor `Editor`]] state providing easy way to change editors state. - * * `EditSession` can be attached to only one [[Document `Document`]]. Same `Document` can be attached to several `EditSession`s. * @class EditSession **/ -// events +//{ events /** * * Emitted when the document changes. * @event change * @param {Object} e An object containing a `delta` of information about the change. @@ -94,29 +91,23 @@ * @event tokenizerUpdate * * @param {Object} e An object containing one property, `"data"`, that contains information about the changing rows * **/ -/** - * Emitted when the mode is loaded. - * - * @event loadMode - * - **/ -/** +/** * Emitted when the current mode changes. * * @event changeMode * **/ -/** +/** * Emitted when the wrap mode changes. * * @event changeWrapMode * **/ -/** +/** * Emitted when the wrapping limit changes. * * @event changeWrapLimit * **/ @@ -136,48 +127,45 @@ * Emitted when the scroll left changes. * @event changeScrollLeft * * @param {Number} scrollLeft The new scroll left value **/ +//} - /** * * Sets up a new `EditSession` and associates it with the given `Document` and `TextMode`. - * @param {Document | String} text If `text` is a `Document`, it associates the `EditSession` with it. Otherwise, a new `Document` is created, with the initial text + * @param {Document | String} text [If `text` is a `Document`, it associates the `EditSession` with it. Otherwise, a new `Document` is created, with the initial text]{: #textParam} + * @param {TextMode} mode [The inital language mode to use for the document]{: #modeParam} * - * @param {TextMode} mode The inital language mode to use for the document - * * @constructor **/ var EditSession = function(text, mode) { this.$breakpoints = []; this.$decorations = []; this.$frontMarkers = {}; this.$backMarkers = {}; this.$markerId = 1; this.$undoSelect = true; - + this.$foldData = []; this.$foldData.toString = function() { - var str = ""; - this.forEach(function(foldLine) { - str += "\n" + foldLine.toString(); - }); - return str; + return this.join("\n"); } this.on("changeFold", this.onChangeFold.bind(this)); this.$onChange = this.onChange.bind(this); - + if (typeof text != "object" || !text.getLine) text = new Document(text); this.setDocument(text); - this.selection = new Selection(this); + + config.resetOptions(this); this.setMode(mode); + config._signal("session", this); }; (function() { @@ -202,32 +190,32 @@ this.resetCaches(); }; /** * Returns the `Document` associated with this session. - * @return {Document} + * @return {Document} **/ this.getDocument = function() { return this.doc; }; - /** + /** * @param {Number} row The row to work with * **/ this.$resetRowCache = function(docRow) { if (!docRow) { this.$docRowCache = []; this.$screenRowCache = []; return; } - - var i = this.$getRowCacheIndex(this.$docRowCache, docRow) + 1; var l = this.$docRowCache.length; - this.$docRowCache.splice(i, l); - this.$screenRowCache.splice(i, l); - + var i = this.$getRowCacheIndex(this.$docRowCache, docRow) + 1; + if (l > i) { + this.$docRowCache.splice(i, l); + this.$screenRowCache.splice(i, l); + } }; this.$getRowCacheIndex = function(cacheArray, val) { var low = 0; var hi = cacheArray.length - 1; @@ -242,11 +230,11 @@ hi = mid - 1; else return mid; } - return low && low -1; + return low -1; }; this.resetCaches = function() { this.$modified = true; this.$wrapData = []; @@ -278,44 +266,44 @@ } this.$informUndoManager.schedule(); } - this.bgTokenizer.$updateOnChange(delta); - this._emit("change", e); + this.bgTokenizer && this.bgTokenizer.$updateOnChange(delta); + this._signal("change", e); }; /** * Sets the session text. * @param {String} text The new text to place * - * - * **/ this.setValue = function(text) { this.doc.setValue(text); - this.selection.moveCursorTo(0, 0); - this.selection.clearSelection(); + this.selection.moveTo(0, 0); this.$resetRowCache(0); this.$deltas = []; this.$deltasDoc = []; this.$deltasFold = []; + this.setUndoManager(this.$undoManager); this.getUndoManager().reset(); }; - /** + /** * Returns the current [[Document `Document`]] as a string. * @method toString - * @alias EditSession.getValue + * @returns {String} + * @alias EditSession.getValue * **/ - - /** + + /** * Returns the current [[Document `Document`]] as a string. * @method getValue - * @alias EditSession.toString + * @returns {String} + * @alias EditSession.toString **/ this.getValue = this.toString = function() { return this.doc.getValue(); }; @@ -325,21 +313,21 @@ **/ this.getSelection = function() { return this.selection; }; - /** + /** * {:BackgroundTokenizer.getState} * @param {Number} row The row to start at * * @related BackgroundTokenizer.getState **/ this.getState = function(row) { return this.bgTokenizer.getState(row); }; - /** + /** * Starts tokenizing at the row indicated. Returns a list of objects of the tokenized rows. * @param {Number} row The row to start at * * * @@ -414,21 +402,29 @@ } if (self.$deltas.length > 0) { undoManager.execute({ action: "aceupdate", - args: [self.$deltas, self] + args: [self.$deltas, self], + merge: self.mergeUndoDeltas }); } - + self.mergeUndoDeltas = false; self.$deltas = []; - } - this.$informUndoManager = - lang.deferredCall(this.$syncInformUndoManager); + }; + this.$informUndoManager = lang.delayedCall(this.$syncInformUndoManager); } }; + /** + * starts a new group in undo history + **/ + this.markUndoGroup = function() { + if (this.$syncInformUndoManager) + this.$syncInformUndoManager(); + }; + this.$defaultUndoManager = { undo: function() {}, redo: function() {}, reset: function() {} }; @@ -449,48 +445,33 @@ } else { return "\t"; } }; - this.$useSoftTabs = true; /** + /** * Pass `true` to enable the use of soft tabs. Soft tabs means you're using spaces instead of the tab character (`'\t'`). * @param {Boolean} useSoftTabs Value indicating whether or not to use soft tabs - * - * - * **/ - this.setUseSoftTabs = function(useSoftTabs) { - if (this.$useSoftTabs === useSoftTabs) return; - - this.$useSoftTabs = useSoftTabs; + this.setUseSoftTabs = function(val) { + this.setOption("useSoftTabs", val); }; - /** * Returns `true` if soft tabs are being used, `false` otherwise. * @returns {Boolean} **/ this.getUseSoftTabs = function() { - return this.$useSoftTabs; + // todo might need more general way for changing settings from mode, but this is ok for now + return this.$useSoftTabs && !this.$mode.$indentWithTabs; }; - - this.$tabSize = 4; /** * Set the number of spaces that define a soft tab; for example, passing in `4` transforms the soft tabs to be equivalent to four spaces. This function also emits the `changeTabSize` event. * @param {Number} tabSize The new tab size - * - * **/ this.setTabSize = function(tabSize) { - if (isNaN(tabSize) || this.$tabSize === tabSize) return; - - this.$modified = true; - this.$rowLengthCache = []; - this.$tabSize = tabSize; - this._emit("changeTabSize"); + this.setOption("tabSize", tabSize); }; - /** * Returns the current tab size. **/ this.getTabSize = function() { return this.$tabSize; @@ -501,28 +482,25 @@ * @param {Object} position The position to check * * **/ this.isTabStop = function(position) { - return this.$useSoftTabs && (position.column % this.$tabSize == 0); + return this.$useSoftTabs && (position.column % this.$tabSize === 0); }; this.$overwrite = false; /** - * Pass in `true` to enable overwrites in your session, or `false` to disable. + * 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 * * **/ this.setOverwrite = function(overwrite) { - if (this.$overwrite == overwrite) return; - - this.$overwrite = overwrite; - this._emit("changeOverwrite"); + this.setOption("overwrite", overwrite); }; /** * Returns `true` if overwrites are enabled; `false` otherwise. **/ @@ -546,11 +524,11 @@ **/ this.addGutterDecoration = function(row, className) { if (!this.$decorations[row]) this.$decorations[row] = ""; this.$decorations[row] += " " + className; - this._emit("changeBreakpoint", {}); + this._signal("changeBreakpoint", {}); }; /** * Removes `className` from the `row`. * @param {Number} row The row number @@ -558,42 +536,42 @@ * * **/ this.removeGutterDecoration = function(row, className) { this.$decorations[row] = (this.$decorations[row] || "").replace(" " + className, ""); - this._emit("changeBreakpoint", {}); + this._signal("changeBreakpoint", {}); }; - + /** * Returns an array of numbers, indicating which rows have breakpoints. - * @returns [Number] + * @returns {[Number]} **/ this.getBreakpoints = function() { return this.$breakpoints; }; /** * Sets a breakpoint on every row number given by `rows`. This function also emites the `'changeBreakpoint'` event. - * @param {Number} rows An array of row indicies + * @param {Array} rows An array of row indices * * * **/ this.setBreakpoints = function(rows) { this.$breakpoints = []; for (var i=0; i<rows.length; i++) { this.$breakpoints[rows[i]] = "ace_breakpoint"; } - this._emit("changeBreakpoint", {}); + this._signal("changeBreakpoint", {}); }; /** * Removes all breakpoints on the rows. This function also emites the `'changeBreakpoint'` event. **/ this.clearBreakpoints = function() { this.$breakpoints = []; - this._emit("changeBreakpoint", {}); + this._signal("changeBreakpoint", {}); }; /** * Sets a breakpoint on the row number given by `rows`. This function also emites the `'changeBreakpoint'` event. * @param {Number} row A row index @@ -606,33 +584,33 @@ className = "ace_breakpoint"; if (className) this.$breakpoints[row] = className; else delete this.$breakpoints[row]; - this._emit("changeBreakpoint", {}); + this._signal("changeBreakpoint", {}); }; /** * Removes a breakpoint on the row number given by `rows`. This function also emites the `'changeBreakpoint'` event. * @param {Number} row A row index * * **/ this.clearBreakpoint = function(row) { delete this.$breakpoints[row]; - this._emit("changeBreakpoint", {}); + this._signal("changeBreakpoint", {}); }; /** * Adds a new marker to the given `Range`. If `inFront` is `true`, a front marker is defined, and the `'changeFrontMarker'` event fires; otherwise, the `'changeBackMarker'` event fires. * @param {Range} range Define the range of the marker * @param {String} clazz Set the CSS class for the marker * @param {Function | String} type Identify the type of the marker * @param {Boolean} inFront Set to `true` to establish a front marker * * - * @return Number + * @return {Number} The new marker id **/ this.addMarker = function(range, clazz, type, inFront) { var id = this.$markerId++; var marker = { @@ -640,44 +618,44 @@ type : type || "line", renderer: typeof type == "function" ? type : null, clazz : clazz, inFront: !!inFront, id: id - } + }; if (inFront) { this.$frontMarkers[id] = marker; - this._emit("changeFrontMarker") + this._signal("changeFrontMarker"); } else { this.$backMarkers[id] = marker; - this._emit("changeBackMarker") + this._signal("changeBackMarker"); } return id; }; /** * Adds a dynamic marker to the session. * @param {Object} marker object with update method * @param {Boolean} inFront Set to `true` to establish a front marker * - * - * @return Object The added marker + * + * @return {Object} The added marker **/ this.addDynamicMarker = function(marker, inFront) { if (!marker.update) return; var id = this.$markerId++; marker.id = id; marker.inFront = !!inFront; if (inFront) { this.$frontMarkers[id] = marker; - this._emit("changeFrontMarker") + this._signal("changeFrontMarker"); } else { this.$backMarkers[id] = marker; - this._emit("changeBackMarker") + this._signal("changeBackMarker"); } return marker; }; @@ -694,11 +672,11 @@ return; var markers = marker.inFront ? this.$frontMarkers : this.$backMarkers; if (marker) { delete (markers[markerId]); - this._emit(marker.inFront ? "changeFrontMarker" : "changeBackMarker"); + this._signal(marker.inFront ? "changeFrontMarker" : "changeBackMarker"); } }; /** * Returns an array containing the IDs of all the markers, either front or back. @@ -714,28 +692,26 @@ if (!this.$searchHighlight) { var highlight = new SearchHighlight(null, "ace_selected-word", "text"); this.$searchHighlight = this.addDynamicMarker(highlight); } this.$searchHighlight.setRegexp(re); - } - + }; + // experimental this.highlightLines = function(startRow, endRow, clazz, inFront) { if (typeof endRow != "number") { clazz = endRow; endRow = startRow; } if (!clazz) clazz = "ace_step"; - + var range = new Range(startRow, 0, endRow, Infinity); - - var id = this.addMarker(range, clazz, "fullLine", inFront); - range.id = id; + range.id = this.addMarker(range, clazz, "fullLine", inFront); return range; }; - + /* * Error: * { * row: 12, * column: 2, //can be undefined @@ -748,30 +724,29 @@ * @param {Array} annotations A list of annotations * **/ this.setAnnotations = function(annotations) { this.$annotations = annotations; - this._emit("changeAnnotation", {}); + this._signal("changeAnnotation", {}); }; /** * Returns the annotations for the `EditSession`. - * @returns {Object} + * @returns {Array} **/ this.getAnnotations = function() { return this.$annotations || []; }; /** * Clears all the annotations for this session. This function also triggers the `'changeAnnotation'` event. **/ this.clearAnnotations = function() { - this.$annotations = {}; - this._emit("changeAnnotation", {}); + this.setAnnotations([]); }; - /** + /** * If `text` contains either the newline (`\n`) or carriage-return ('\r') characters, `$autoNewLine` stores that value. * @param {String} text A block of text * * **/ @@ -798,11 +773,11 @@ if (column > 0) inToken = !!line.charAt(column - 1).match(this.tokenRe); if (!inToken) inToken = !!line.charAt(column).match(this.tokenRe); - + if (inToken) var re = this.tokenRe; else if (/^\s+$/.test(line.slice(column-1, column+1))) var re = /\s/; else @@ -828,11 +803,11 @@ /** * Gets the range of a word, including its right whitespace. * @param {Number} row The row number to start from * @param {Number} column The column number to start from * - * @return Range + * @return {Range} **/ this.getAWordRange = function(row, column) { var wordRange = this.getWordRange(row, column); var line = this.getLine(wordRange.end.row); @@ -840,158 +815,111 @@ wordRange.end.column += 1; } return wordRange; }; - /** + /** * {:Document.setNewLineMode.desc} * @param {String} newLineMode {:Document.setNewLineMode.param} * * * @related Document.setNewLineMode **/ this.setNewLineMode = function(newLineMode) { this.doc.setNewLineMode(newLineMode); }; - /** + /** * * Returns the current new line mode. - * @returns String + * @returns {String} * @related Document.getNewLineMode **/ this.getNewLineMode = function() { return this.doc.getNewLineMode(); }; - this.$useWorker = true; - /** * Identifies if you want to use a worker for the `EditSession`. * @param {Boolean} useWorker Set to `true` to use a worker * **/ - this.setUseWorker = function(useWorker) { - if (this.$useWorker == useWorker) - return; + this.setUseWorker = function(useWorker) { this.setOption("useWorker", useWorker); }; - this.$useWorker = useWorker; - - this.$stopWorker(); - if (useWorker) - this.$startWorker(); - }; - /** * Returns `true` if workers are being used. **/ - this.getUseWorker = function() { - return this.$useWorker; - }; + this.getUseWorker = function() { return this.$useWorker; }; /** * Reloads all the tokens on the current session. This function calls [[BackgroundTokenizer.start `BackgroundTokenizer.start ()`]] to all the rows; it also emits the `'tokenizerUpdate'` event. **/ this.onReloadTokenizer = function(e) { var rows = e.data; this.bgTokenizer.start(rows.first); - this._emit("tokenizerUpdate", e); + this._signal("tokenizerUpdate", e); }; this.$modes = {}; - this._loadMode = function(mode, callback) { - if (!this.$modes["null"]) - this.$modes["null"] = this.$modes["ace/mode/text"] = new TextMode(); - if (this.$modes[mode]) - return callback(this.$modes[mode]); - - var _self = this; - var module; - try { - module = require(mode); - } catch (e) {}; - // sometimes require returns empty object (this bug is present in requirejs 2 as well) - if (module && module.Mode) - return done(module); - - // set mode to text until loading is finished - if (!this.$mode) - this.$setModePlaceholder(); - - fetch(mode, function() { - require([mode], done); - }); - - function done(module) { - if (_self.$modes[mode]) - return callback(_self.$modes[mode]); - - _self.$modes[mode] = new module.Mode(); - _self.$modes[mode].$id = mode; - _self._emit("loadmode", { - name: mode, - mode: _self.$modes[mode] - }); - callback(_self.$modes[mode]); - } - - function fetch(name, callback) { - if (!config.get("packaged")) - return callback(); - - net.loadScript(config.moduleUrl(name, "mode"), callback); - } - }; - - this.$setModePlaceholder = function() { - this.$mode = this.$modes["null"]; - var tokenizer = this.$mode.getTokenizer(); - - if (!this.bgTokenizer) { - this.bgTokenizer = new BackgroundTokenizer(tokenizer); - var _self = this; - this.bgTokenizer.addEventListener("update", function(e) { - _self._emit("tokenizerUpdate", e); - }); - } else { - this.bgTokenizer.setTokenizer(tokenizer); - } - this.bgTokenizer.setDocument(this.getDocument()); - - this.tokenRe = this.$mode.tokenRe; - this.nonTokenRe = this.$mode.nonTokenRe; - }; - /** * Sets a new text mode for the `EditSession`. This method also emits the `'changeMode'` event. If a [[BackgroundTokenizer `BackgroundTokenizer`]] is set, the `'tokenizerUpdate'` event is also emitted. * @param {TextMode} mode Set a new text mode + * @param {cb} optional callback * **/ this.$mode = null; this.$modeId = null; - this.setMode = function(mode) { - mode = mode || "null"; - // load on demand - if (typeof mode === "string") { - if (this.$modeId == mode) - return; + this.setMode = function(mode, cb) { + if (mode && typeof mode === "object") { + if (mode.getTokenizer) + return this.$onChangeMode(mode); + var options = mode; + var path = options.path; + } else { + path = mode || "ace/mode/text"; + } - this.$modeId = mode; - var _self = this; - this._loadMode(mode, function(module) { - if (_self.$modeId !== mode) - return; + // this is needed if ace isn't on require path (e.g tests in node) + if (!this.$modes["ace/mode/text"]) + this.$modes["ace/mode/text"] = new TextMode(); - _self.setMode(module); - }); + if (this.$modes[path] && !options) { + this.$onChangeMode(this.$modes[path]); + cb && cb(); return; } + // load on demand + this.$modeId = path; + config.loadModule(["mode", path], function(m) { + if (this.$modeId !== path) + return cb && cb(); + if (this.$modes[path] && !options) { + this.$onChangeMode(this.$modes[path]); + } else if (m && m.Mode) { + m = new m.Mode(options); + if (!options) { + this.$modes[path] = m; + m.$id = path; + } + this.$onChangeMode(m); + } + cb && cb(); + }.bind(this)); - if (this.$mode === mode) return; + // set mode to text until loading is finished + if (!this.$mode) + this.$onChangeMode(this.$modes["ace/mode/text"], true); + }; + + this.$onChangeMode = function(mode, $isPlaceholder) { + if (!$isPlaceholder) + this.$modeId = mode.$id; + if (this.$mode === mode) + return; + this.$mode = mode; - this.$modeId = mode.$id; this.$stopWorker(); if (this.$useWorker) this.$startWorker(); @@ -1005,48 +933,47 @@ if (!this.bgTokenizer) { this.bgTokenizer = new BackgroundTokenizer(tokenizer); var _self = this; this.bgTokenizer.addEventListener("update", function(e) { - _self._emit("tokenizerUpdate", e); + _self._signal("tokenizerUpdate", e); }); } else { this.bgTokenizer.setTokenizer(tokenizer); } this.bgTokenizer.setDocument(this.getDocument()); - this.bgTokenizer.start(0); this.tokenRe = mode.tokenRe; this.nonTokenRe = mode.nonTokenRe; - this.$setFolding(mode.foldingRules); - - this._emit("changeMode"); + + if (!$isPlaceholder) { + // experimental method, used by c9 findiniles + if (mode.attachToSession) + mode.attachToSession(this); + this.$options.wrapMethod.set.call(this, this.$wrapMethod); + this.$setFolding(mode.foldingRules); + this.bgTokenizer.start(0); + this._emit("changeMode"); + } }; - this.$stopWorker = function() { - if (this.$worker) + if (this.$worker) { this.$worker.terminate(); - - this.$worker = null; + this.$worker = null; + } }; - this.$startWorker = function() { - if (typeof Worker !== "undefined" && !require.noWorker) { - try { - this.$worker = this.$mode.createWorker(this); - } catch (e) { - console.log("Could not load worker"); - console.log(e); - this.$worker = null; - } - } - else + try { + this.$worker = this.$mode.createWorker(this); + } catch (e) { + config.warn("Could not load worker", e); this.$worker = null; + } }; /** * Returns the current text mode. * @returns {TextMode} The current text mode @@ -1060,16 +987,16 @@ * This function sets the scroll top value. It also emits the `'changeScrollTop'` event. * @param {Number} scrollTop The new scroll top value * **/ this.setScrollTop = function(scrollTop) { - scrollTop = Math.round(Math.max(0, scrollTop)); - if (this.$scrollTop === scrollTop) + // TODO: should we force integer lineheight instead? scrollTop = Math.round(scrollTop); + if (this.$scrollTop === scrollTop || isNaN(scrollTop)) return; this.$scrollTop = scrollTop; - this._emit("changeScrollTop", scrollTop); + this._signal("changeScrollTop", scrollTop); }; /** * [Returns the value of the distance between the top of the editor and the topmost part of the visible content.]{: #EditSession.getScrollTop} * @returns {Number} @@ -1081,16 +1008,16 @@ this.$scrollLeft = 0; /** * [Sets the value of the distance between the left of the editor and the leftmost part of the visible content.]{: #EditSession.setScrollLeft} **/ this.setScrollLeft = function(scrollLeft) { - scrollLeft = Math.round(Math.max(0, scrollLeft)); - if (this.$scrollLeft === scrollLeft) + // scrollLeft = Math.round(scrollLeft); + if (this.$scrollLeft === scrollLeft || isNaN(scrollLeft)) return; this.$scrollLeft = scrollLeft; - this._emit("changeScrollLeft", scrollLeft); + this._signal("changeScrollLeft", scrollLeft); }; /** * [Returns the value of the distance between the left of the editor and the leftmost part of the visible content.]{: #EditSession.getScrollLeft} * @returns {Number} @@ -1103,12 +1030,24 @@ * Returns the width of the screen. * @returns {Number} **/ this.getScreenWidth = function() { this.$computeWidth(); + if (this.lineWidgets) + return Math.max(this.getLineWidgetMaxWidth(), this.screenWidth); return this.screenWidth; }; + + this.getLineWidgetMaxWidth = function() { + if (this.lineWidgetsWidth != null) return this.lineWidgetsWidth; + var width = 0; + this.lineWidgets.forEach(function(w) { + if (w && w.screenWidth > width) + width = w.screenWidth; + }); + return this.lineWidgetWidth = width; + }; this.$computeWidth = function(force) { if (this.$modified || force) { this.$modified = false; @@ -1140,11 +1079,11 @@ } this.screenWidth = longestScreenLine; } }; - /** + /** * Returns a verbatim copy of the given line as it is in the document * @param {Number} row The row to retrieve from * * * @returns {String} @@ -1152,70 +1091,70 @@ **/ this.getLine = function(row) { return this.doc.getLine(row); }; - /** + /** * Returns an array of strings of the rows between `firstRow` and `lastRow`. This function is inclusive of `lastRow`. * @param {Number} firstRow The first row index to retrieve * @param {Number} lastRow The final row index to retrieve * - * @returns [String] + * @returns {[String]} * **/ this.getLines = function(firstRow, lastRow) { return this.doc.getLines(firstRow, lastRow); }; - /** + /** * Returns the number of rows in the document. * @returns {Number} **/ this.getLength = function() { return this.doc.getLength(); }; - /** + /** * {:Document.getTextRange.desc} * @param {Range} range The range to work with * - * @returns {Range} + * @returns {String} **/ this.getTextRange = function(range) { return this.doc.getTextRange(range || this.selection.getRange()); }; - /** - * Inserts a block of `text` and the indicated `position`. + /** + * Inserts a block of `text` and the indicated `position`. * @param {Object} position The position {row, column} to start inserting at * @param {String} text A chunk of text to insert * @returns {Object} The position of the last line of `text`. If the length of `text` is 0, this function simply returns `position`. - * - * - **/ + * + * + **/ this.insert = function(position, text) { return this.doc.insert(position, text); }; - /** - * Removes the `range` from the document. + /** + * Removes the `range` from the document. * @param {Range} range A specified Range to remove * @returns {Object} The new `start` property of the range, which contains `startRow` and `startColumn`. If `range` is empty, this function returns the unmodified value of `range.start`. - * + * * @related Document.remove - * - **/ + * + **/ this.remove = function(range) { return this.doc.remove(range); }; /** * Reverts previous changes to your document. * @param {Array} deltas An array of previous changes * @param {Boolean} dontSelect [If `true`, doesn't select the range of where the change occured]{: #dontSelect} * - * + * * @returns {Range} **/ this.undoChanges = function(deltas, dontSelect) { if (!deltas.length) return; @@ -1282,19 +1221,19 @@ }; this.$getUndoSelection = function(deltas, isUndo, lastUndoRange) { function isInsert(delta) { var insert = - delta.action == "insertText" || delta.action == "insertLines"; + delta.action === "insertText" || delta.action === "insertLines"; return isUndo ? !insert : insert; } var delta = deltas[0]; var range, point; var lastDeltaIsInsert = false; if (isInsert(delta)) { - range = delta.range.clone(); + range = Range.fromPoints(delta.range.start, delta.range.end); lastDeltaIsInsert = true; } else { range = Range.fromPoints(delta.range.start, delta.range.start); lastDeltaIsInsert = false; } @@ -1322,10 +1261,15 @@ } // Check if this range and the last undo range has something in common. // If true, merge the ranges. if (lastUndoRange != null) { + if (Range.comparePoints(lastUndoRange.start, range.start) === 0) { + lastUndoRange.start.column += range.end.column - range.start.column; + lastUndoRange.end.column += range.end.column - range.start.column; + } + var cmp = lastUndoRange.compareRange(range); if (cmp == 1) { range.setStart(lastUndoRange.start); } else if (cmp == -1) { range.setEnd(lastUndoRange.end); @@ -1333,20 +1277,20 @@ } return range; }; - /** + /** * Replaces a range in the document with the new `text`. * * @param {Range} range A specified Range to replace * @param {String} text The new text to use as a replacement - * @returns {Object} An object containing the final row and column, like this: + * @returns {Object} An object containing the final row and column, like this: * ``` * {row: endRow, column: 0} - * ``` - * If the text and range are empty, this function returns an object containing the current `range.start` value. + * ``` + * If the text and range are empty, this function returns an object containing the current `range.start` value. * If the text is the exact same as what currently exists, this function returns an object containing the current `range.end` value. * * * * @related Document.replace @@ -1367,37 +1311,49 @@ * @returns {Range} The new range where the text was moved to. * * * **/ - this.moveText = function(fromRange, toPosition) { + this.moveText = function(fromRange, toPosition, copy) { var text = this.getTextRange(fromRange); - this.remove(fromRange); + var folds = this.getFoldsInRange(fromRange); - var toRow = toPosition.row; - var toColumn = toPosition.column; + var toRange = Range.fromPoints(toPosition, toPosition); + if (!copy) { + this.remove(fromRange); + var rowDiff = fromRange.start.row - fromRange.end.row; + var collDiff = rowDiff ? -fromRange.end.column : fromRange.start.column - fromRange.end.column; + if (collDiff) { + if (toRange.start.row == fromRange.end.row && toRange.start.column > fromRange.end.column) + toRange.start.column += collDiff; + if (toRange.end.row == fromRange.end.row && toRange.end.column > fromRange.end.column) + toRange.end.column += collDiff; + } + if (rowDiff && toRange.start.row >= fromRange.end.row) { + toRange.start.row += rowDiff; + toRange.end.row += rowDiff; + } + } - // Make sure to update the insert location, when text is removed in - // front of the chosen point of insertion. - if (!fromRange.isMultiLine() && fromRange.start.row == toRow && - fromRange.end.column < toColumn) - toColumn -= text.length; - - if (fromRange.isMultiLine() && fromRange.end.row < toRow) { - var lines = this.doc.$split(text); - toRow -= lines.length - 1; + toRange.end = this.insert(toRange.start, text); + if (folds.length) { + var oldStart = fromRange.start; + var newStart = toRange.start; + var rowDiff = newStart.row - oldStart.row; + var collDiff = newStart.column - oldStart.column; + this.addFolds(folds.map(function(x) { + x = x.clone(); + if (x.start.row == oldStart.row) + x.start.column += collDiff; + if (x.end.row == oldStart.row) + x.end.column += collDiff; + x.start.row += rowDiff; + x.end.row += rowDiff; + return x; + })); } - var endRow = toRow + fromRange.end.row - fromRange.start.row; - var endColumn = fromRange.isMultiLine() ? - fromRange.end.column : - toColumn + fromRange.end.column - fromRange.start.column; - - var toRange = new Range(toRow, toColumn, endRow, endColumn); - - this.insert(toRange.start, text); - return toRange; }; /** * Indents all the rows, from `startRow` to `endRow` (inclusive), by prefixing each row with the token in `indentString`. @@ -1443,43 +1399,65 @@ } this.remove(deleteRange); } }; - /** + this.$moveLines = function(firstRow, lastRow, dir) { + firstRow = this.getRowFoldStart(firstRow); + lastRow = this.getRowFoldEnd(lastRow); + if (dir < 0) { + var row = this.getRowFoldStart(firstRow + dir); + if (row < 0) return 0; + var diff = row-firstRow; + } else if (dir > 0) { + var row = this.getRowFoldEnd(lastRow + dir); + if (row > this.doc.getLength()-1) return 0; + var diff = row-lastRow; + } else { + firstRow = this.$clipRowToDocument(firstRow); + lastRow = this.$clipRowToDocument(lastRow); + var diff = lastRow - firstRow + 1; + } + + var range = new Range(firstRow, 0, lastRow, Number.MAX_VALUE); + var folds = this.getFoldsInRange(range).map(function(x){ + x = x.clone(); + x.start.row += diff; + x.end.row += diff; + return x; + }); + + var lines = dir == 0 + ? this.doc.getLines(firstRow, lastRow) + : this.doc.removeLines(firstRow, lastRow); + this.doc.insertLines(firstRow+diff, lines); + folds.length && this.addFolds(folds); + return diff; + }; + /** * Shifts all the lines in the document up one, starting from `firstRow` and ending at `lastRow`. * @param {Number} firstRow The starting row to move up * @param {Number} lastRow The final row to move up * @returns {Number} If `firstRow` is less-than or equal to 0, this function returns 0. Otherwise, on success, it returns -1. * * @related Document.insertLines * **/ this.moveLinesUp = function(firstRow, lastRow) { - if (firstRow <= 0) return 0; - - var removed = this.doc.removeLines(firstRow, lastRow); - this.doc.insertLines(firstRow - 1, removed); - return -1; + return this.$moveLines(firstRow, lastRow, -1); }; - /** + /** * Shifts all the lines in the document down one, starting from `firstRow` and ending at `lastRow`. * @param {Number} firstRow The starting row to move down * @param {Number} lastRow The final row to move down * @returns {Number} If `firstRow` is less-than or equal to 0, this function returns 0. Otherwise, on success, it returns -1. * - * - * * @related Document.insertLines **/ this.moveLinesDown = function(firstRow, lastRow) { - if (lastRow >= this.doc.getLength()-1) return 0; - - var removed = this.doc.removeLines(firstRow, lastRow); - this.doc.insertLines(firstRow+1, removed); - return 1; + return this.$moveLines(firstRow, lastRow, 1); }; /** * Duplicates all the text between `firstRow` and `lastRow`. * @param {Number} firstRow The starting row to duplicate @@ -1487,18 +1465,11 @@ * @returns {Number} Returns the number of new rows added; in other words, `lastRow - firstRow + 1`. * * **/ this.duplicateLines = function(firstRow, lastRow) { - var firstRow = this.$clipRowToDocument(firstRow); - var lastRow = this.$clipRowToDocument(lastRow); - - var lines = this.getLines(firstRow, lastRow); - this.doc.insertLines(firstRow, lines); - - var addedRows = lastRow - firstRow + 1; - return addedRows; + return this.$moveLines(firstRow, lastRow, 0); }; this.$clipRowToDocument = function(row) { return Math.max(0, Math.min(row, this.doc.getLength()-1)); @@ -1578,18 +1549,15 @@ this.$resetRowCache(0); // If wrapMode is activaed, the wrapData array has to be initialized. if (useWrapMode) { var len = this.getLength(); - this.$wrapData = []; - for (var i = 0; i < len; i++) { - this.$wrapData.push([]); - } + this.$wrapData = Array(len); this.$updateWrapData(0, len - 1); } - this._emit("changeWrapMode"); + this._signal("changeWrapMode"); } }; /** * Returns `true` if wrap mode is being used; `false` otherwise. @@ -1610,62 +1578,74 @@ * * **/ this.setWrapLimitRange = function(min, max) { if (this.$wrapLimitRange.min !== min || this.$wrapLimitRange.max !== max) { - this.$wrapLimitRange.min = min; - this.$wrapLimitRange.max = max; + this.$wrapLimitRange = { + min: min, + max: max + }; this.$modified = true; // This will force a recalculation of the wrap limit - this._emit("changeWrapMode"); + this._signal("changeWrapMode"); } }; - /** + /** * This should generally only be called by the renderer when a resize is detected. * @param {Number} desiredLimit The new wrap limit * @returns {Boolean} * - * * @private **/ - this.adjustWrapLimit = function(desiredLimit) { - var wrapLimit = this.$constrainWrapLimit(desiredLimit); - if (wrapLimit != this.$wrapLimit && wrapLimit > 0) { + this.adjustWrapLimit = function(desiredLimit, $printMargin) { + var limits = this.$wrapLimitRange; + if (limits.max < 0) + limits = {min: $printMargin, max: $printMargin}; + var wrapLimit = this.$constrainWrapLimit(desiredLimit, limits.min, limits.max); + if (wrapLimit != this.$wrapLimit && wrapLimit > 1) { this.$wrapLimit = wrapLimit; this.$modified = true; if (this.$useWrapMode) { this.$updateWrapData(0, this.getLength() - 1); this.$resetRowCache(0); - this._emit("changeWrapLimit"); + this._signal("changeWrapLimit"); } return true; } return false; }; - this.$constrainWrapLimit = function(wrapLimit) { - var min = this.$wrapLimitRange.min; + this.$constrainWrapLimit = function(wrapLimit, min, max) { if (min) wrapLimit = Math.max(min, wrapLimit); - var max = this.$wrapLimitRange.max; if (max) wrapLimit = Math.min(max, wrapLimit); - // What would a limit of 0 even mean? - return Math.max(1, wrapLimit); + return wrapLimit; }; /** * Returns the value of wrap limit. + * @returns {Number} The wrap limit. **/ this.getWrapLimit = function() { return this.$wrapLimit; }; - + /** + * Sets the line length for soft wrap in the editor. Lines will break + * at a minimum of the given length minus 20 chars and at a maximum + * of the given number of chars. + * @param {number} limit The maximum line length in chars, for soft wrapping lines. + */ + this.setWrapLimit = function (limit) { + this.setWrapLimitRange(limit, limit); + }; + + /** * Returns an object that defines the minimum and maximum of the wrap limit; it looks something like this: * * { min: wrapLimitRange_min, max: wrapLimitRange_max } * * @returns {Object} @@ -1697,10 +1677,11 @@ len = e.data.lines ? e.data.lines.length : lastRow - firstRow; } else { len = lastRow - firstRow; } + this.$updating = true; if (len != 0) { if (action.indexOf("remove") != -1) { this[useWrapMode ? "$wrapData" : "$rowLengthCache"].splice(firstRow, len); var foldLines = this.$foldData; @@ -1728,34 +1709,29 @@ } } lastRow = firstRow; } else { - var args; - if (useWrapMode) { - args = [firstRow, 0]; - for (var i = 0; i < len; i++) args.push([]); - this.$wrapData.splice.apply(this.$wrapData, args); - } else { - args = Array(len); - args.unshift(firstRow, 0); - this.$rowLengthCache.splice.apply(this.$rowLengthCache, args); - } + var args = Array(len); + args.unshift(firstRow, 0); + var arr = useWrapMode ? this.$wrapData : this.$rowLengthCache + arr.splice.apply(arr, args); // If some new line is added inside of a foldLine, then split // the fold line up. var foldLines = this.$foldData; var foldLine = this.getFoldLine(firstRow); var idx = 0; if (foldLine) { - var cmp = foldLine.range.compareInside(start.row, start.column) + var cmp = foldLine.range.compareInside(start.row, start.column); // Inside of the foldLine range. Need to split stuff up. if (cmp == 0) { foldLine = foldLine.split(start.row, start.column); - foldLine.shiftRow(len); - foldLine.addRemoveChars( - lastRow, 0, end.column - start.column); + if (foldLine) { + foldLine.shiftRow(len); + foldLine.addRemoveChars(lastRow, 0, end.column - start.column); + } } else // Infront of the foldLine but same row. Need to shift column. if (cmp == -1) { foldLine.addRemoveChars(firstRow, 0, end.column - start.column); foldLine.shiftRow(len); @@ -1789,10 +1765,11 @@ } if (useWrapMode && this.$wrapData.length != this.doc.getLength()) { console.error("doc.getLength() and $wrapData.length have to be the same!"); } + this.$updating = false; if (useWrapMode) this.$updateWrapData(firstRow, lastRow); else this.$updateRowLengthCache(firstRow, lastRow); @@ -1816,11 +1793,11 @@ var row = firstRow; lastRow = Math.min(lastRow, lines.length - 1); while (row <= lastRow) { foldLine = this.getFoldLine(row, foldLine); if (!foldLine) { - tokens = this.$getDisplayTokens(lang.stringTrimRight(lines[row])); + tokens = this.$getDisplayTokens(lines[row]); wrapData[row] = this.$computeWrapSplits(tokens, wrapLimit, tabSize); row ++; } else { tokens = []; foldLine.walk(function(placeholder, row, column, lastColumn) { @@ -1840,16 +1817,12 @@ tokens = tokens.concat(walkTokens); }.bind(this), foldLine.end.row, lines[foldLine.end.row].length + 1 ); - // Remove spaces/tabs from the back of the token array. - while (tokens.length != 0 && tokens[tokens.length - 1] >= SPACE) - tokens.pop(); - wrapData[foldLine.start.row] - = this.$computeWrapSplits(tokens, wrapLimit, tabSize); + wrapData[foldLine.start.row] = this.$computeWrapSplits(tokens, wrapLimit, tabSize); row = foldLine.end.row + 1; } } }; @@ -1871,10 +1844,12 @@ var splits = []; var displayLength = tokens.length; var lastSplit = 0, lastDocSplit = 0; + var isCode = this.$wrapAsCode; + function addSplit(screenPos) { var displayed = tokens.slice(lastSplit, screenPos); // The document size is the current size - the extra width for tabs // and multipleWidth characters. @@ -1898,26 +1873,25 @@ // This is, where the split should be. var split = lastSplit + wrapLimit; // If there is a space or tab at this split position, then making // a split is simple. - if (tokens[split] >= SPACE) { + if (tokens[split - 1] >= SPACE && tokens[split] >= SPACE) { + /* disabled see https://github.com/ajaxorg/ace/issues/1186 // Include all following spaces + tabs in this split as well. while (tokens[split] >= SPACE) { split ++; - } + } */ addSplit(split); continue; } // === ELSE === // Check if split is inside of a placeholder. Placeholder are // not splitable. Therefore, seek the beginning of the placeholder // and try to place the split beofre the placeholder's start. - if (tokens[split] == PLACEHOLDER_START - || tokens[split] == PLACEHOLDER_BODY) - { + if (tokens[split] == PLACEHOLDER_START || tokens[split] == PLACEHOLDER_BODY) { // Seek the start of the placeholder and do the split // before the placeholder. By definition there always // a PLACEHOLDER_START between split and lastSplit. for (split; split != lastSplit - 1; split--) { if (tokens[split] == PLACEHOLDER_START) { @@ -1937,12 +1911,11 @@ // If the PLACEHOLDER_START IS the index of the last // split, then we have to place the split after the // placeholder. So, let's seek for the end of the placeholder. split = lastSplit + wrapLimit; for (split; split < tokens.length; split++) { - if (tokens[split] != PLACEHOLDER_BODY) - { + if (tokens[split] != PLACEHOLDER_BODY) { break; } } // If spilt == tokens.length, then the placeholder is the last @@ -1956,16 +1929,25 @@ continue; } // === ELSE === // Search for the first non space/tab/placeholder/punctuation token backwards. - var minSplit = Math.max(split - 10, lastSplit - 1); + var minSplit = Math.max(split - (isCode ? 10 : wrapLimit-(wrapLimit>>2)), lastSplit - 1); while (split > minSplit && tokens[split] < PLACEHOLDER_START) { split --; } - while (split > minSplit && tokens[split] == PUNCTUATION) { - split --; + if (isCode) { + while (split > minSplit && tokens[split] < PLACEHOLDER_START) { + split --; + } + while (split > minSplit && tokens[split] == PUNCTUATION) { + split --; + } + } else { + while (split > minSplit && tokens[split] < SPACE) { + split --; + } } // If we found one, then add the split. if (split > minSplit) { addSplit(++split); continue; @@ -1973,10 +1955,12 @@ // === ELSE === split = lastSplit + wrapLimit; // The split is inside of a CHAR or CHAR_EXT token and no space // around -> force a split. + if (tokens[split] == CHAR_EXT) + split--; addSplit(split); } return splits; }; @@ -2025,13 +2009,10 @@ * @param {Number} screenColumn * @returns {[Number]} Returns an `int[]` array with two elements:<br/> * The first position indicates the number of columns for `str` on screen.<br/> * The second value contains the position of the document column that this function read until. * - * - * - * **/ this.$getStringScreenWidth = function(str, maxScreenColumn, screenColumn) { if (maxScreenColumn == 0) return [0, 0]; if (maxScreenColumn == null) @@ -2057,60 +2038,72 @@ } return [screenColumn, column]; }; + this.lineWidgets = null; /** * Returns number of screenrows in a wrapped line. * @param {Number} row The row number to check * * @returns {Number} **/ this.getRowLength = function(row) { + if (this.lineWidgets) + var h = this.lineWidgets[row] && this.lineWidgets[row].rowCount || 0; + else + h = 0 if (!this.$useWrapMode || !this.$wrapData[row]) { + return 1 + h; + } else { + return this.$wrapData[row].length + 1 + h; + } + }; + this.getRowLineCount = function(row) { + if (!this.$useWrapMode || !this.$wrapData[row]) { return 1; } else { return this.$wrapData[row].length + 1; } }; - /** + /** * Returns the position (on screen) for the last character in the provided screen row. * @param {Number} screenRow The screen row to check - * - * + * @returns {Number} + * * @related EditSession.documentToScreenColumn **/ this.getScreenLastRowColumn = function(screenRow) { var pos = this.screenToDocumentPosition(screenRow, Number.MAX_VALUE); return this.documentToScreenColumn(pos.row, pos.column); }; - /** + /** * For the given document row and column, this returns the column position of the last screen row. - * @param {Number} docRow + * @param {Number} docRow * - * @param {Number} docColumn + * @param {Number} docColumn **/ this.getDocumentLastRowColumn = function(docRow, docColumn) { var screenRow = this.documentToScreenRow(docRow, docColumn); return this.getScreenLastRowColumn(screenRow); }; - /** + /** * For the given document row and column, this returns the document position of the last row. - * @param {Number} docRow - * @param {Number} docColumn + * @param {Number} docRow + * @param {Number} docColumn * * **/ this.getDocumentLastRowColumnPosition = function(docRow, docColumn) { var screenRow = this.documentToScreenRow(docRow, docColumn); return this.screenToDocumentPosition(screenRow, Number.MAX_VALUE / 10); }; - /** + /** * For the given row, this returns the split data. * @returns {String} **/ this.getRowSplitData = function(row) { if (!this.$useWrapMode) { @@ -2122,11 +2115,11 @@ /** * The distance to the next tab stop at the specified screen column. * @param {Number} screenColumn The screen column to check * - * + * * @returns {Number} **/ this.getScreenTabSize = function(screenColumn) { return this.$tabSize - screenColumn % this.$tabSize; }; @@ -2139,11 +2132,11 @@ this.screenToDocumentColumn = function(screenRow, screenColumn) { return this.screenToDocumentPosition(screenRow, screenColumn).column; }; - /** + /** * Converts characters coordinates on the screen to characters coordinates within the document. [This takes into account code folding, word wrap, tab size, and any other visual modifications.]{: #conversionConsiderations} * @param {Number} screenRow The screen row to check * @param {Number} screenColumn The screen column to check * @returns {Object} The object returned has two properties: `row` and `column`. * @@ -2162,35 +2155,37 @@ var row = 0; var rowLength = 0; var rowCache = this.$screenRowCache; var i = this.$getRowCacheIndex(rowCache, screenRow); - if (0 < i && i < rowCache.length) { + var l = rowCache.length; + if (l && i >= 0) { var row = rowCache[i]; var docRow = this.$docRowCache[i]; - var doCache = screenRow > row || (screenRow == row && i == rowCache.length - 1); + var doCache = screenRow > rowCache[l - 1]; } else { - var doCache = i != 0 || !rowCache.length; + var doCache = !l; } var maxRow = this.getLength() - 1; var foldLine = this.getNextFoldLine(docRow); var foldStart = foldLine ? foldLine.start.row : Infinity; while (row <= screenRow) { rowLength = this.getRowLength(docRow); - if (row + rowLength - 1 >= screenRow || docRow >= maxRow) { + if (row + rowLength > screenRow || docRow >= maxRow) { break; } else { row += rowLength; docRow++; if (docRow > foldStart) { docRow = foldLine.end.row+1; foldLine = this.getNextFoldLine(docRow, foldLine); foldStart = foldLine ? foldLine.start.row : Infinity; } } + if (doCache) { this.$docRowCache.push(docRow); this.$screenRowCache.push(row); } } @@ -2201,22 +2196,23 @@ } else if (row + rowLength <= screenRow || docRow > maxRow) { // clip at the end of the document return { row: maxRow, column: this.getLine(maxRow).length - } + }; } else { line = this.getLine(docRow); foldLine = null; } if (this.$useWrapMode) { var splits = this.$wrapData[docRow]; if (splits) { - column = splits[screenRow - row]; - if(screenRow > row && splits.length) { - docColumn = splits[screenRow - row - 1] || splits[splits.length - 1]; + var splitIndex = Math.floor(screenRow - row); + column = splits[splitIndex]; + if(splitIndex > 0 && splits.length) { + docColumn = splits[splitIndex - 1] || splits[splits.length - 1]; line = line.substring(docColumn); } } } @@ -2231,11 +2227,11 @@ return foldLine.idxToPosition(docColumn); return {row: docRow, column: docColumn}; }; - /** + /** * Converts document coordinates to screen coordinates. {:conversionConsiderations} * @param {Number} docRow The document row to check * @param {Number} docColumn The document column to check * @returns {Object} The object returned by this method has two properties: `row` and `column`. * @@ -2267,16 +2263,17 @@ var rowEnd, row = 0; var rowCache = this.$docRowCache; var i = this.$getRowCacheIndex(rowCache, docRow); - if (0 < i && i < rowCache.length) { + var l = rowCache.length; + if (l && i >= 0) { var row = rowCache[i]; var screenRow = this.$screenRowCache[i]; - var doCache = docRow > row || (docRow == row && i == rowCache.length - 1); + var doCache = docRow > rowCache[l - 1]; } else { - var doCache = i != 0 || !rowCache.length; + var doCache = !l; } var foldLine = this.getNextFoldLine(row); var foldStart = foldLine ?foldLine.start.row :Infinity; @@ -2312,18 +2309,20 @@ foldStartRow = docRow; } // Clamp textLine if in wrapMode. if (this.$useWrapMode) { var wrapRow = this.$wrapData[foldStartRow]; - var screenRowOffset = 0; - while (textLine.length >= wrapRow[screenRowOffset]) { - screenRow ++; - screenRowOffset++; + if (wrapRow) { + var screenRowOffset = 0; + while (textLine.length >= wrapRow[screenRowOffset]) { + screenRow ++; + screenRowOffset++; + } + textLine = textLine.substring( + wrapRow[screenRowOffset - 1] || 0, textLine.length + ); } - textLine = textLine.substring( - wrapRow[screenRowOffset - 1] || 0, textLine.length - ); } return { row: screenRow, column: this.$getStringScreenWidth(textLine)[0] @@ -2332,12 +2331,12 @@ /** * For the given document row and column, returns the screen column. * @param {Number} row * @param {Number} docColumn + * @returns {Number} * - * **/ this.documentToScreenColumn = function(row, docColumn) { return this.documentToScreenPosition(row, docColumn).column; }; @@ -2373,22 +2372,43 @@ var row = 0, i = 0; var fold = this.$foldData[i++]; var foldStart = fold ? fold.start.row :Infinity; while (row < lastRow) { - screenRows += this.$wrapData[row].length + 1; + var splits = this.$wrapData[row]; + screenRows += splits ? splits.length + 1 : 1; row ++; if (row > foldStart) { row = fold.end.row+1; fold = this.$foldData[i++]; foldStart = fold ?fold.start.row :Infinity; } } } + // todo + if (this.lineWidgets) + screenRows += this.$getWidgetScreenLength(); + return screenRows; - } + }; + + /** + * @private + * + */ + this.$setFontMetrics = function(fm) { + // todo + }; + + this.destroy = function() { + if (this.bgTokenizer) { + this.bgTokenizer.setDocument(null); + this.bgTokenizer = null; + } + this.$stopWorker(); + }; // For every keystroke this gets called once per char in the whole doc!! // Wouldn't hurt to make it a bit faster for c >= 0x1100 function isFullWidth(c) { if (c < 0x1100) @@ -2429,8 +2449,103 @@ }).call(EditSession.prototype); require("./edit_session/folding").Folding.call(EditSession.prototype); require("./edit_session/bracket_match").BracketMatch.call(EditSession.prototype); + + +config.defineOptions(EditSession.prototype, "session", { + wrap: { + set: function(value) { + if (!value || value == "off") + value = false; + else if (value == "free") + value = true; + else if (value == "printMargin") + value = -1; + else if (typeof value == "string") + value = parseInt(value, 10) || false; + + if (this.$wrap == value) + return; + if (!value) { + this.setUseWrapMode(false); + } else { + var col = typeof value == "number" ? value : null; + this.setWrapLimitRange(col, col); + this.setUseWrapMode(true); + } + this.$wrap = value; + }, + get: function() { + if (this.getUseWrapMode()) { + if (this.$wrap == -1) + return "printMargin"; + if (!this.getWrapLimitRange().min) + return "free"; + return this.$wrap; + } + return "off"; + }, + handlesSet: true + }, + wrapMethod: { + // code|text|auto + set: function(val) { + val = val == "auto" + ? this.$mode.type != "text" + : val != "text"; + if (val != this.$wrapAsCode) { + this.$wrapAsCode = val; + if (this.$useWrapMode) { + this.$modified = true; + this.$resetRowCache(0); + this.$updateWrapData(0, this.getLength() - 1); + } + } + }, + initialValue: "auto" + }, + firstLineNumber: { + set: function() {this._signal("changeBreakpoint");}, + initialValue: 1 + }, + useWorker: { + set: function(useWorker) { + this.$useWorker = useWorker; + + this.$stopWorker(); + if (useWorker) + this.$startWorker(); + }, + initialValue: true + }, + useSoftTabs: {initialValue: true}, + tabSize: { + set: function(tabSize) { + if (isNaN(tabSize) || this.$tabSize === tabSize) return; + + this.$modified = true; + this.$rowLengthCache = []; + this.$tabSize = tabSize; + this._signal("changeTabSize"); + }, + initialValue: 4, + handlesSet: true + }, + overwrite: { + set: function(val) {this._signal("changeOverwrite");}, + initialValue: false + }, + newLineMode: { + set: function(val) {this.doc.setNewLineMode(val)}, + get: function() {return this.doc.getNewLineMode()}, + handlesSet: true + }, + mode: { + set: function(val) { this.setMode(val) }, + get: function() { return this.$modeId } + } +}); exports.EditSession = EditSession; });