/* vim:ts=4:sts=4:sw=4: * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Ajax.org Code Editor (ACE). * * The Initial Developer of the Original Code is * Ajax.org B.V. * Portions created by the Initial Developer are Copyright (C) 2010 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Fabian Jakobs * Mihai Sucan * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ define(function(require, exports, module) { var oop = require("pilot/oop"); var lang = require("pilot/lang"); var EventEmitter = require("pilot/event_emitter").EventEmitter; var Selection = require("ace/selection").Selection; var TextMode = require("ace/mode/text").Mode; var Range = require("ace/range").Range; var Document = require("ace/document").Document; var EditSession = function(text, mode) { this.$modified = true; this.$breakpoints = []; this.$frontMarkers = {}; this.$backMarkers = {}; this.$markerId = 1; this.$wrapData = []; if (text instanceof Document) { this.setDocument(text); } else { this.setDocument(new Document(text)); } this.selection = new Selection(this); if (mode) this.setMode(mode); }; (function() { oop.implement(this, EventEmitter); this.setDocument = function(doc) { if (this.doc) throw new Error("Document is already set"); this.doc = doc; doc.on("change", this.onChange.bind(this)); }; this.getDocument = function() { return this.doc; }; this.onChange = function(e) { var delta = e.data; this.$modified = true; if (!this.$fromUndo && this.$undoManager && !delta.ignore) { this.$deltas.push(delta); this.$informUndoManager.schedule(); } this.$updateWrapDataOnChange(e); this._dispatchEvent("change", e); }; this.setValue = function(text) { this.doc.setValue(text); this.$deltas = []; this.getUndoManager().reset(); }; this.getValue = this.toString = function() { return this.doc.getValue(); }; this.getSelection = function() { return this.selection; }; this.setUndoManager = function(undoManager) { this.$undoManager = undoManager; this.$deltas = []; if (this.$informUndoManager) { this.$informUndoManager.cancel(); } if (undoManager) { var self = this; this.$informUndoManager = lang.deferredCall(function() { if (self.$deltas.length > 0) undoManager.execute({ action : "aceupdate", args : [self.$deltas, self] }); self.$deltas = []; }); } }; this.$defaultUndoManager = { undo: function() {}, redo: function() {}, reset: function() {} }; this.getUndoManager = function() { return this.$undoManager || this.$defaultUndoManager; }, this.getTabString = function() { if (this.getUseSoftTabs()) { return lang.stringRepeat(" ", this.getTabSize()); } else { return "\t"; } }; this.$useSoftTabs = true; this.setUseSoftTabs = function(useSoftTabs) { if (this.$useSoftTabs === useSoftTabs) return; this.$useSoftTabs = useSoftTabs; }; this.getUseSoftTabs = function() { return this.$useSoftTabs; }; this.$tabSize = 4; this.setTabSize = function(tabSize) { if (isNaN(tabSize) || this.$tabSize === tabSize) return; this.$modified = true; this.$tabSize = tabSize; this._dispatchEvent("changeTabSize"); }; this.getTabSize = function() { return this.$tabSize; }; this.isTabStop = function(position) { return this.$useSoftTabs && (position.column % this.$tabSize == 0); }; this.$overwrite = false; this.setOverwrite = function(overwrite) { if (this.$overwrite == overwrite) return; this.$overwrite = overwrite; this._dispatchEvent("changeOverwrite"); }; this.getOverwrite = function() { return this.$overwrite; }; this.toggleOverwrite = function() { this.setOverwrite(!this.$overwrite); }; this.getBreakpoints = function() { return this.$breakpoints; }; this.setBreakpoints = function(rows) { this.$breakpoints = []; for (var i=0; i 0) { inToken = !!line.charAt(column - 1).match(this.tokenRe); } if (!inToken) { inToken = !!line.charAt(column).match(this.tokenRe); } var re = inToken ? this.tokenRe : this.nonTokenRe; var start = column; if (start > 0) { do { start--; } while (start >= 0 && line.charAt(start).match(re)); start++; } var end = column; while (end < line.length && line.charAt(end).match(re)) { end++; } return new Range(row, start, row, end); }; this.setNewLineMode = function(newLineMode) { this.doc.setNewLineMode(newLineMode); }; this.getNewLineMode = function() { return this.doc.getNewLineMode(); }; this.$useWorker = true; this.setUseWorker = function(useWorker) { if (this.$useWorker == useWorker) return; if (useWorker && !this.$worker && window.Worker) this.$worker = mode.createWorker(this); if (!useWorker && this.$worker) { this.$worker.terminate(); this.$worker = null; } }; this.getUseWorker = function() { return this.$useWorker; }; this.$mode = null; this.setMode = function(mode) { if (this.$mode === mode) return; if (this.$worker) this.$worker.terminate(); if (this.$useWorker && window.Worker && !require.noWorker) this.$worker = mode.createWorker(this); else this.$worker = null; this.$mode = mode; this._dispatchEvent("changeMode"); }; this.getMode = function() { if (!this.$mode) { this.$mode = new TextMode(); } return this.$mode; }; this.$scrollTop = 0; this.setScrollTopRow = function(scrollTopRow) { if (this.$scrollTop === scrollTopRow) return; this.$scrollTop = scrollTopRow; this._dispatchEvent("changeScrollTop"); }; this.getScrollTopRow = function() { return this.$scrollTop; }; this.getWidth = function() { this.$computeWidth(); return this.width; }; this.getScreenWidth = function() { this.$computeWidth(); return this.screenWidth; }; this.$computeWidth = function(force) { if (this.$modified || force) { this.$modified = false; var lines = this.doc.getAllLines(); var longestLine = 0; var longestScreenLine = 0; for ( var i = 0; i < lines.length; i++) { var line = lines[i], len = line.length, screenLen = this.$getStringScreenWidth(line); longestLine = Math.max(longestLine, len); longestScreenLine = Math.max(longestScreenLine, screenLen); } this.width = longestLine; if (this.$useWrapMode) { this.screenWidth = this.$wrapLimit; } else { this.screenWidth = longestScreenLine; } } }; /** * Get a verbatim copy of the given line as it is in the document */ this.getLine = function(row) { return this.doc.getLine(row); }; this.getLines = function(firstRow, lastRow) { return this.doc.getLines(firstRow, lastRow); }; this.getLength = function() { return this.doc.getLength(); }; this.getTextRange = function(range) { return this.doc.getTextRange(range); }; this.findMatchingBracket = function(position) { if (position.column == 0) return null; var charBeforeCursor = this.getLine(position.row).charAt(position.column-1); if (charBeforeCursor == "") return null; var match = charBeforeCursor.match(/([\(\[\{])|([\)\]\}])/); if (!match) { return null; } if (match[1]) { return this.$findClosingBracket(match[1], position); } else { return this.$findOpeningBracket(match[2], position); } }; this.$brackets = { ")": "(", "(": ")", "]": "[", "[": "]", "{": "}", "}": "{" }; this.$findOpeningBracket = function(bracket, position) { var openBracket = this.$brackets[bracket]; var column = position.column - 2; var row = position.row; var depth = 1; var line = this.getLine(row); while (true) { while(column >= 0) { var ch = line.charAt(column); if (ch == openBracket) { depth -= 1; if (depth == 0) { return {row: row, column: column}; } } else if (ch == bracket) { depth +=1; } column -= 1; } row -=1; if (row < 0) break; var line = this.getLine(row); var column = line.length-1; } return null; }; this.$findClosingBracket = function(bracket, position) { var closingBracket = this.$brackets[bracket]; var column = position.column; var row = position.row; var depth = 1; var line = this.getLine(row); var lineCount = this.getLength(); while (true) { while(column < line.length) { var ch = line.charAt(column); if (ch == closingBracket) { depth -= 1; if (depth == 0) { return {row: row, column: column}; } } else if (ch == bracket) { depth +=1; } column += 1; } row +=1; if (row >= lineCount) break; var line = this.getLine(row); var column = 0; } return null; }; this.insert = function(position, text) { return this.doc.insert(position, text); }; this.remove = function(range) { return this.doc.remove(range); }; this.undoChanges = function(deltas) { if (!deltas.length) return; this.$fromUndo = true; this.doc.revertDeltas(deltas); this.$fromUndo = false; this.$setUndoSelection(deltas, true); }, this.redoChanges = function(deltas) { if (!deltas.length) return; this.$fromUndo = true; this.doc.applyDeltas(deltas); this.$fromUndo = false; this.$setUndoSelection(deltas, false); }, this.$setUndoSelection = function(deltas, isUndo) { // invert deltas is they are an undo if (isUndo) deltas = deltas.map(function(delta) { var d = { range: delta.range } if (delta.action == "insertText" || delta.action == "insertLines") d.action = "removeText" else d.action = "insertText" return d; }).reverse(); var actions = [{}]; // collapse insert and remove operations for (var i=0; i= this.doc.getLength()-1) return 0; var removed = this.doc.removeLines(firstRow, lastRow); this.doc.insertLines(firstRow+1, removed); return 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; }; this.$clipRowToDocument = function(row) { return Math.max(0, Math.min(row, this.doc.getLength()-1)); }; // WRAPMODE this.$wrapLimit = 80; this.$useWrapMode = false; this.$wrapLimitRange = { min : null, max : null }; this.setUseWrapMode = function(useWrapMode) { if (useWrapMode != this.$useWrapMode) { this.$useWrapMode = useWrapMode; this.$modified = true; // If wrapMode is activaed, the wrapData array has to be initialized. if (useWrapMode) { var len = this.getLength(); this.$wrapData = []; for (i = 0; i < len; i++) { this.$wrapData.push([]); } this.$updateWrapData(0, len - 1); } this._dispatchEvent("changeWrapMode"); } }; this.getUseWrapMode = function() { return this.$useWrapMode; }; // Allow the wrap limit to move freely between min and max. Either // parameter can be null to allow the wrap limit to be unconstrained // in that direction. Or set both parameters to the same number to pin // the limit to that value. this.setWrapLimitRange = function(min, max) { if (this.$wrapLimitRange.min !== min || this.$wrapLimitRange.max !== max) { this.$wrapLimitRange.min = min; this.$wrapLimitRange.max = max; this.$modified = true; // This will force a recalculation of the wrap limit this._dispatchEvent("changeWrapMode"); } }; // This should generally only be called by the renderer when a resize // is detected. this.adjustWrapLimit = function(desiredLimit) { var wrapLimit = this.$constrainWrapLimit(desiredLimit); if (wrapLimit != this.$wrapLimit && wrapLimit > 0) { this.$wrapLimit = wrapLimit; this.$modified = true; if (this.$useWrapMode) { this.$updateWrapData(0, this.getLength() - 1); this._dispatchEvent("changeWrapLimit"); } return true; } return false; }; this.$constrainWrapLimit = function(wrapLimit) { var min = this.$wrapLimitRange.min; 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); }; this.getWrapLimit = function() { return this.$wrapLimit; }; this.getWrapLimitRange = function() { // Avoid unexpected mutation by returning a copy return { min : this.$wrapLimitRange.min, max : this.$wrapLimitRange.max }; }; this.$updateWrapDataOnChange = function(e) { if (!this.$useWrapMode) { return; } var len; var action = e.data.action; var firstRow = e.data.range.start.row, lastRow = e.data.range.end.row; if (action.indexOf("Lines") != -1) { if (action == "insertLines") { lastRow = firstRow + (e.data.lines.length); } else { lastRow = firstRow; } len = e.data.lines.length; } else { len = lastRow - firstRow; } if (len != 0) { if (action.indexOf("remove") != -1) { this.$wrapData.splice(firstRow, len); lastRow = firstRow; } else { var args = [firstRow, 0]; for (var i = 0; i < len; i++) args.push([]); this.$wrapData.splice.apply(this.$wrapData, args); } } if (this.$wrapData.length != this.doc.$lines.length) { console.error("The length of doc.$lines and $wrapData have to be the same!"); } this.$updateWrapData(firstRow, lastRow); }; this.$updateWrapData = function(firstRow, lastRow) { var lines = this.doc.getAllLines(); var tabSize = this.getTabSize(); var wrapData = this.$wrapData; var wrapLimit = this.$wrapLimit; for (var row = firstRow; row <= lastRow; row++) { wrapData[row] = this.$computeWrapSplits(lines[row], wrapLimit, tabSize); } }; // "Tokens" var CHAR = 1, CHAR_EXT = 2, SPACE = 3, TAB = 4, TAB_SPACE = 5; this.$computeWrapSplits = function(textLine, wrapLimit, tabSize) { textLine = lang.stringTrimRight(textLine); if (textLine.length == 0) { return []; } var tabSize = this.getTabSize(); var splits = []; var tokens = this.$getDisplayTokens(textLine); var displayLength = tokens.length; var lastSplit = 0, lastDocSplit = 0; function addSplit(screenPos) { var displayed = tokens.slice(lastSplit, screenPos); // The document size is the current size - the extra width for tabs // and multipleWidth characters. var len = displayed.length; displayed.join(""). // Get all the tabs spaces. replace(/5/g, function(m) { len -= 1; }). // Get all the multipleWidth characters. replace(/2/g, function(m) { len -= 1; }); lastDocSplit += len; splits.push(lastDocSplit); lastSplit = screenPos; } while (displayLength - lastSplit > wrapLimit) { // This is, where the split should be. var split = lastSplit + wrapLimit; // If there is a space or tab at this split position. if (tokens[split] >= SPACE) { // Include all following spaces + tabs in this split as well. while (tokens[split] >= SPACE) { split ++; } addSplit(split); } else { // Search for the first non space/tab token. for (split; split != lastSplit - 1; split--) { if (tokens[split] >= SPACE) { split++; break; } } // If we found one, then add the split. if (split > lastSplit) { addSplit(split); } // No space or tab around? Well, force a split then. else { addSplit(lastSplit + wrapLimit); } } } return splits; } this.$getDisplayTokens = function(str) { var arr = []; var tabSize; for (var i = 0; i < str.length; i++) { var c = str.charCodeAt(i); // Tab if (c == 9) { tabSize = this.getScreenTabSize(arr.length); arr.push(TAB); for (var n = 1; n < tabSize; n++) { arr.push(TAB_SPACE); } } // Space else if(c == 32) { arr.push(SPACE); } // full width characters else if (isFullWidth(c)) { arr.push(CHAR, CHAR_EXT); } else { arr.push(CHAR); } } return arr; } /** * Calculates the width of the a string on the screen while assuming that * the string starts at the first column on the screen. * * @param string str String to calculate the screen width of * @return int number of columns for str on screen. */ this.$getStringScreenWidth = function(str) { var screenColumn = 0; var tabSize = this.getTabSize(); for (var i=0; i= wrapData[docRow].length + 1) { row -= wrapData[docRow].length + 1; docRow ++; } return [docRow, row]; }; this.screenToDocumentRow = function(screenRow) { return this.$screenToDocumentRow(screenRow)[0]; }; this.screenToDocumentColumn = function(screenRow, screenColumn) { return this.screenToDocumentPosition(screenRow, screenColumn).column; }; /** * Returns the width of a tab character at screenColumn. */ this.getScreenTabSize = function(screenColumn) { return this.$tabSize - screenColumn % this.$tabSize; }; this.screenToDocumentPosition = function(row, column) { var line; var docRow; var docColumn; var remaining = column; var linesCount = this.getLength(); if (!this.$useWrapMode) { docRow = row >= linesCount? linesCount-1 : (row < 0 ? 0 : row); row = 0; docColumn = 0; line = this.getLine(docRow); } else { var wrapData = this.$wrapData; var docRow = 0; while (docRow < linesCount && row >= wrapData[docRow].length + 1) { row -= wrapData[docRow].length + 1; docRow ++; } if (docRow >= linesCount) { docRow = linesCount-1 row = wrapData[docRow].length; } docColumn = wrapData[docRow][row - 1] || 0; line = this.getLine(docRow).substring(docColumn); } var tabSize, screenColumn = 0; for(var i = 0; i < line.length; i++) { var c = line.charCodeAt(i); if (remaining > 0) { docColumn += 1; // tab if (c == 9) { tabSize = this.getScreenTabSize(screenColumn); if (remaining >= tabSize) { remaining -= tabSize; screenColumn += tabSize; } else { remaining = 0; docColumn -= 1; } } // full width characters else if (isFullWidth(c)) { if (remaining >= 2) { remaining -= 2; } else { remaining = 0; docColumn -= 1; } } else { screenColumn += 1; remaining -= 1; } } else { break; } } // Clamp docColumn. if (this.$useWrapMode) { column = wrapData[docRow][row] if (docColumn >= column) { // We remove one character at the end such that the docColumn // position returned is not associated to the next row on the // screen. docColumn = column - 1; } } else if (line) { docColumn = Math.min(docColumn, line.length); } return { row: docRow, column: docColumn }; }; this.documentToScreenColumn = function(row, docColumn) { return this.documentToScreenPosition(row, docColumn).column; }; /** * * @return array[2] * - array[0]: The number of the row on the screen (aka screenRow) * - array[1]: The number of rows from the first docRow on the screen * (aka screenRowOffset); */ this.$documentToScreenRow = function(docRow, docColumn) { if (!this.$useWrapMode) { return [docRow, 0]; } var wrapData = this.$wrapData; var screenRow = 0; // Handle special case where the row is outside of the range of lines. if (docRow > wrapData.length - 1) { return [ this.getScreenLength(), wrapData.length == 0 ? 0 : (wrapData[wrapData.length - 1].length - 1) ]; } for (var i = 0; i < docRow; i++) { screenRow += wrapData[i].length + 1; } var screenRowOffset = 0; while (docColumn >= wrapData[docRow][screenRowOffset]) { screenRow ++; screenRowOffset++; } return [screenRow, screenRowOffset]; } this.documentToScreenRow = function(docRow, docColumn) { return this.$documentToScreenRow(docRow, docColumn)[0]; } this.documentToScreenPosition = function(pos, column) { var str; var tabSize = this.getTabSize(); // Normalize the passed in arguments. var row; if (column != null) { row = pos; } else { row = pos.row; column = pos.column; } if (!this.$useWrapMode) { str = this.getLine(row).substring(0, column); column = this.$getStringScreenWidth(str); return { row: row, column: column }; } var rowData = this.$documentToScreenRow(row, column); var screenRow = rowData[0]; if (row >= this.getLength()) { return { row: screenRow, column: 0 }; } var split; var wrapRowData = this.$wrapData[row]; var screenColumn; var screenRowOffset = rowData[1]; str = this.getLine(row).substring( wrapRowData[screenRowOffset - 1] || 0, column); screenColumn = this.$getStringScreenWidth(str); return { row: screenRow, column: screenColumn }; }; this.getScreenLength = function() { if (!this.$useWrapMode) { return this.getLength(); } var screenRows = 0; for (var row = 0; row < this.$wrapData.length; row++) { screenRows += this.$wrapData[row].length + 1; } return screenRows; } function isFullWidth(c) { return c >= 0x1100 && c <= 0x115F || c >= 0x11A3 && c <= 0x11A7 || c >= 0x11FA && c <= 0x11FF || c >= 0x2329 && c <= 0x232A || c >= 0x2E80 && c <= 0x2E99 || c >= 0x2E9B && c <= 0x2EF3 || c >= 0x2F00 && c <= 0x2FD5 || c >= 0x2FF0 && c <= 0x2FFB || c >= 0x3000 && c <= 0x303E || c >= 0x3041 && c <= 0x3096 || c >= 0x3099 && c <= 0x30FF || c >= 0x3105 && c <= 0x312D || c >= 0x3131 && c <= 0x318E || c >= 0x3190 && c <= 0x31BA || c >= 0x31C0 && c <= 0x31E3 || c >= 0x31F0 && c <= 0x321E || c >= 0x3220 && c <= 0x3247 || c >= 0x3250 && c <= 0x32FE || c >= 0x3300 && c <= 0x4DBF || c >= 0x4E00 && c <= 0xA48C || c >= 0xA490 && c <= 0xA4C6 || c >= 0xA960 && c <= 0xA97C || c >= 0xAC00 && c <= 0xD7A3 || c >= 0xD7B0 && c <= 0xD7C6 || c >= 0xD7CB && c <= 0xD7FB || c >= 0xF900 && c <= 0xFAFF || c >= 0xFE10 && c <= 0xFE19 || c >= 0xFE30 && c <= 0xFE52 || c >= 0xFE54 && c <= 0xFE66 || c >= 0xFE68 && c <= 0xFE6B || c >= 0xFF01 && c <= 0xFF60 || c >= 0xFFE0 && c <= 0xFFE6; } }).call(EditSession.prototype); exports.EditSession = EditSession; });