/* 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): * Julian Viereck * * 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) { "use strict"; var Range = require("../range").Range; var FoldLine = require("./fold_line").FoldLine; var Fold = require("./fold").Fold; var TokenIterator = require("../token_iterator").TokenIterator; function Folding() { /* * Looks up a fold at a given row/column. Possible values for side: * -1: ignore a fold if fold.start = row/column * +1: ignore a fold if fold.end = row/column */ this.getFoldAt = function(row, column, side) { var foldLine = this.getFoldLine(row); if (!foldLine) return null; var folds = foldLine.folds; for (var i = 0; i < folds.length; i++) { var fold = folds[i]; if (fold.range.contains(row, column)) { if (side == 1 && fold.range.isEnd(row, column)) { continue; } else if (side == -1 && fold.range.isStart(row, column)) { continue; } return fold; } } }; /* * Returns all folds in the given range. Note, that this will return folds * */ this.getFoldsInRange = function(range) { range = range.clone(); var start = range.start; var end = range.end; var foldLines = this.$foldData; var foundFolds = []; start.column += 1; end.column -= 1; for (var i = 0; i < foldLines.length; i++) { var cmp = foldLines[i].range.compareRange(range); if (cmp == 2) { // Range is before foldLine. No intersection. This means, // there might be other foldLines that intersect. continue; } else if (cmp == -2) { // Range is after foldLine. There can't be any other foldLines then, // so let's give up. break; } var folds = foldLines[i].folds; for (var j = 0; j < folds.length; j++) { var fold = folds[j]; cmp = fold.range.compareRange(range); if (cmp == -2) { break; } else if (cmp == 2) { continue; } else // WTF-state: Can happen due to -1/+1 to start/end column. if (cmp == 42) { break; } foundFolds.push(fold); } } return foundFolds; }; /* * Returns all folds in the document */ this.getAllFolds = function() { var folds = []; var foldLines = this.$foldData; function addFold(fold) { folds.push(fold); if (!fold.subFolds) return; for (var i = 0; i < fold.subFolds.length; i++) addFold(fold.subFolds[i]); } for (var i = 0; i < foldLines.length; i++) for (var j = 0; j < foldLines[i].folds.length; j++) addFold(foldLines[i].folds[j]); return folds; }; /* * Returns the string between folds at the given position. * E.g. * foob|arwolrd -> "bar" * foobarwol|rd -> "world" * foobarwolrd -> * * where | means the position of row/column * * The trim option determs if the return string should be trimed according * to the "side" passed with the trim value: * * E.g. * foob|arwolrd -trim=-1> "b" * foobarwol|rd -trim=+1> "rld" * fo|obarwolrd -trim=00> "foo" */ this.getFoldStringAt = function(row, column, trim, foldLine) { foldLine = foldLine || this.getFoldLine(row); if (!foldLine) return null; var lastFold = { end: { column: 0 } }; // TODO: Refactor to use getNextFoldTo function. var str, fold; for (var i = 0; i < foldLine.folds.length; i++) { fold = foldLine.folds[i]; var cmp = fold.range.compareEnd(row, column); if (cmp == -1) { str = this .getLine(fold.start.row) .substring(lastFold.end.column, fold.start.column); break; } else if (cmp === 0) { return null; } lastFold = fold; } if (!str) str = this.getLine(fold.start.row).substring(lastFold.end.column); if (trim == -1) return str.substring(0, column - lastFold.end.column); else if (trim == 1) return str.substring(column - lastFold.end.column); else return str; }; this.getFoldLine = function(docRow, startFoldLine) { var foldData = this.$foldData; var i = 0; if (startFoldLine) i = foldData.indexOf(startFoldLine); if (i == -1) i = 0; for (i; i < foldData.length; i++) { var foldLine = foldData[i]; if (foldLine.start.row <= docRow && foldLine.end.row >= docRow) { return foldLine; } else if (foldLine.end.row > docRow) { return null; } } return null; }; // returns the fold which starts after or contains docRow this.getNextFoldLine = function(docRow, startFoldLine) { var foldData = this.$foldData; var i = 0; if (startFoldLine) i = foldData.indexOf(startFoldLine); if (i == -1) i = 0; for (i; i < foldData.length; i++) { var foldLine = foldData[i]; if (foldLine.end.row >= docRow) { return foldLine; } } return null; }; this.getFoldedRowCount = function(first, last) { var foldData = this.$foldData, rowCount = last-first+1; for (var i = 0; i < foldData.length; i++) { var foldLine = foldData[i], end = foldLine.end.row, start = foldLine.start.row; if (end >= last) { if(start < last) { if(start >= first) rowCount -= last-start; else rowCount = 0;//in one fold } break; } else if(end >= first){ if (start >= first) //fold inside range rowCount -= end-start; else rowCount -= end-first+1; } } return rowCount; }; this.$addFoldLine = function(foldLine) { this.$foldData.push(foldLine); this.$foldData.sort(function(a, b) { return a.start.row - b.start.row; }); return foldLine; }; /* * Adds a new fold. * * @returns * The new created Fold object or an existing fold object in case the * passed in range fits an existing fold exactly. */ this.addFold = function(placeholder, range) { var foldData = this.$foldData; var added = false; var fold; if (placeholder instanceof Fold) fold = placeholder; else fold = new Fold(range, placeholder); this.$clipRangeToDocument(fold.range); var startRow = fold.start.row; var startColumn = fold.start.column; var endRow = fold.end.row; var endColumn = fold.end.column; // --- Some checking --- if (fold.placeholder.length < 2) throw "Placeholder has to be at least 2 characters"; if (startRow == endRow && endColumn - startColumn < 2) throw "The range has to be at least 2 characters width"; var startFold = this.getFoldAt(startRow, startColumn, 1); var endFold = this.getFoldAt(endRow, endColumn, -1); if (startFold && endFold == startFold) return startFold.addSubFold(fold); if ( (startFold && !startFold.range.isStart(startRow, startColumn)) || (endFold && !endFold.range.isEnd(endRow, endColumn)) ) { throw "A fold can't intersect already existing fold" + fold.range + startFold.range; } // Check if there are folds in the range we create the new fold for. var folds = this.getFoldsInRange(fold.range); if (folds.length > 0) { // Remove the folds from fold data. this.removeFolds(folds); // Add the removed folds as subfolds on the new fold. fold.subFolds = folds; } for (var i = 0; i < foldData.length; i++) { var foldLine = foldData[i]; if (endRow == foldLine.start.row) { foldLine.addFold(fold); added = true; break; } else if (startRow == foldLine.end.row) { foldLine.addFold(fold); added = true; if (!fold.sameRow) { // Check if we might have to merge two FoldLines. var foldLineNext = foldData[i + 1]; if (foldLineNext && foldLineNext.start.row == endRow) { // We need to merge! foldLine.merge(foldLineNext); break; } } break; } else if (endRow <= foldLine.start.row) { break; } } if (!added) foldLine = this.$addFoldLine(new FoldLine(this.$foldData, fold)); if (this.$useWrapMode) this.$updateWrapData(foldLine.start.row, foldLine.start.row); else this.$updateRowLengthCache(foldLine.start.row, foldLine.start.row); // Notify that fold data has changed. this.$modified = true; this._emit("changeFold", { data: fold }); return fold; }; this.addFolds = function(folds) { folds.forEach(function(fold) { this.addFold(fold); }, this); }; this.removeFold = function(fold) { var foldLine = fold.foldLine; var startRow = foldLine.start.row; var endRow = foldLine.end.row; var foldLines = this.$foldData; var folds = foldLine.folds; // Simple case where there is only one fold in the FoldLine such that // the entire fold line can get removed directly. if (folds.length == 1) { foldLines.splice(foldLines.indexOf(foldLine), 1); } else // If the fold is the last fold of the foldLine, just remove it. if (foldLine.range.isEnd(fold.end.row, fold.end.column)) { folds.pop(); foldLine.end.row = folds[folds.length - 1].end.row; foldLine.end.column = folds[folds.length - 1].end.column; } else // If the fold is the first fold of the foldLine, just remove it. if (foldLine.range.isStart(fold.start.row, fold.start.column)) { folds.shift(); foldLine.start.row = folds[0].start.row; foldLine.start.column = folds[0].start.column; } else // We know there are more then 2 folds and the fold is not at the edge. // This means, the fold is somewhere in between. // // If the fold is in one row, we just can remove it. if (fold.sameRow) { folds.splice(folds.indexOf(fold), 1); } else // The fold goes over more then one row. This means remvoing this fold // will cause the fold line to get splitted up. newFoldLine is the second part { var newFoldLine = foldLine.split(fold.start.row, fold.start.column); folds = newFoldLine.folds; folds.shift(); newFoldLine.start.row = folds[0].start.row; newFoldLine.start.column = folds[0].start.column; } if (this.$useWrapMode) this.$updateWrapData(startRow, endRow); else this.$updateRowLengthCache(startRow, endRow); // Notify that fold data has changed. this.$modified = true; this._emit("changeFold", { data: fold }); }; this.removeFolds = function(folds) { // We need to clone the folds array passed in as it might be the folds // array of a fold line and as we call this.removeFold(fold), folds // are removed from folds and changes the current index. var cloneFolds = []; for (var i = 0; i < folds.length; i++) { cloneFolds.push(folds[i]); } cloneFolds.forEach(function(fold) { this.removeFold(fold); }, this); this.$modified = true; }; this.expandFold = function(fold) { this.removeFold(fold); fold.subFolds.forEach(function(fold) { this.addFold(fold); }, this); fold.subFolds = []; }; this.expandFolds = function(folds) { folds.forEach(function(fold) { this.expandFold(fold); }, this); }; this.unfold = function(location, expandInner) { var range, folds; if (location == null) range = new Range(0, 0, this.getLength(), 0); else if (typeof location == "number") range = new Range(location, 0, location, this.getLine(location).length); else if ("row" in location) range = Range.fromPoints(location, location); else range = location; folds = this.getFoldsInRange(range); if (expandInner) { this.removeFolds(folds); } else { // TODO: might need to remove and add folds in one go instead of using // expandFolds several times. while (folds.length) { this.expandFolds(folds); folds = this.getFoldsInRange(range); } } }; /* * Checks if a given documentRow is folded. This is true if there are some * folded parts such that some parts of the line is still visible. **/ this.isRowFolded = function(docRow, startFoldRow) { return !!this.getFoldLine(docRow, startFoldRow); }; this.getRowFoldEnd = function(docRow, startFoldRow) { var foldLine = this.getFoldLine(docRow, startFoldRow); return (foldLine ? foldLine.end.row : docRow); }; this.getFoldDisplayLine = function(foldLine, endRow, endColumn, startRow, startColumn) { if (startRow == null) { startRow = foldLine.start.row; startColumn = 0; } if (endRow == null) { endRow = foldLine.end.row; endColumn = this.getLine(endRow).length; } // Build the textline using the FoldLine walker. var doc = this.doc; var textLine = ""; foldLine.walk(function(placeholder, row, column, lastColumn) { if (row < startRow) { return; } else if (row == startRow) { if (column < startColumn) { return; } lastColumn = Math.max(startColumn, lastColumn); } if (placeholder) { textLine += placeholder; } else { textLine += doc.getLine(row).substring(lastColumn, column); } }.bind(this), endRow, endColumn); return textLine; }; this.getDisplayLine = function(row, endColumn, startRow, startColumn) { var foldLine = this.getFoldLine(row); if (!foldLine) { var line; line = this.doc.getLine(row); return line.substring(startColumn || 0, endColumn || line.length); } else { return this.getFoldDisplayLine( foldLine, row, endColumn, startRow, startColumn); } }; this.$cloneFoldData = function() { var fd = []; fd = this.$foldData.map(function(foldLine) { var folds = foldLine.folds.map(function(fold) { return fold.clone(); }); return new FoldLine(fd, folds); }); return fd; }; this.toggleFold = function(tryToUnfold) { var selection = this.selection; var range = selection.getRange(); var fold; var bracketPos; if (range.isEmpty()) { var cursor = range.start; fold = this.getFoldAt(cursor.row, cursor.column); if (fold) { this.expandFold(fold); return; } else if (bracketPos = this.findMatchingBracket(cursor)) { if (range.comparePoint(bracketPos) == 1) { range.end = bracketPos; } else { range.start = bracketPos; range.start.column++; range.end.column--; } } else if (bracketPos = this.findMatchingBracket({row: cursor.row, column: cursor.column + 1})) { if (range.comparePoint(bracketPos) == 1) range.end = bracketPos; else range.start = bracketPos; range.start.column++; } else { range = this.getCommentFoldRange(cursor.row, cursor.column) || range; } } else { var folds = this.getFoldsInRange(range); if (tryToUnfold && folds.length) { this.expandFolds(folds); return; } else if (folds.length == 1 ) { fold = folds[0]; } } if (!fold) fold = this.getFoldAt(range.start.row, range.start.column); if (fold && fold.range.toString() == range.toString()) { this.expandFold(fold); return; } var placeholder = "..."; if (!range.isMultiLine()) { placeholder = this.getTextRange(range); if(placeholder.length < 4) return; placeholder = placeholder.trim().substring(0, 2) + ".."; } this.addFold(placeholder, range); }; this.getCommentFoldRange = function(row, column) { var iterator = new TokenIterator(this, row, column); var token = iterator.getCurrentToken(); if (token && /^comment|string/.test(token.type)) { var range = new Range(); var re = new RegExp(token.type.replace(/\..*/, "\\.")); do { token = iterator.stepBackward(); } while(token && re.test(token.type)); iterator.stepForward(); range.start.row = iterator.getCurrentTokenRow(); range.start.column = iterator.getCurrentTokenColumn() + 2; iterator = new TokenIterator(this, row, column); do { token = iterator.stepForward(); } while(token && re.test(token.type)); token = iterator.stepBackward(); range.end.row = iterator.getCurrentTokenRow(); range.end.column = iterator.getCurrentTokenColumn() + token.value.length; return range; } }; this.foldAll = function(startRow, endRow) { var foldWidgets = this.foldWidgets; endRow = endRow || this.getLength(); for (var row = startRow || 0; row < endRow; row++) { if (foldWidgets[row] == null) foldWidgets[row] = this.getFoldWidget(row); if (foldWidgets[row] != "start") continue; var range = this.getFoldWidgetRange(row); // sometimes range can be incompatible with existing fold // wouldn't it be better for addFold to return null istead of throwing? if (range && range.end.row < endRow) try { this.addFold("...", range); } catch(e) {} } }; this.$foldStyles = { "manual": 1, "markbegin": 1, "markbeginend": 1 }; this.$foldStyle = "markbegin"; this.setFoldStyle = function(style) { if (!this.$foldStyles[style]) throw new Error("invalid fold style: " + style + "[" + Object.keys(this.$foldStyles).join(", ") + "]"); if (this.$foldStyle == style) return; this.$foldStyle = style; if (style == "manual") this.unfold(); // reset folding var mode = this.$foldMode; this.$setFolding(null); this.$setFolding(mode); }; // structured folding this.$setFolding = function(foldMode) { if (this.$foldMode == foldMode) return; this.$foldMode = foldMode; this.removeListener('change', this.$updateFoldWidgets); this._emit("changeAnnotation"); if (!foldMode || this.$foldStyle == "manual") { this.foldWidgets = null; return; } this.foldWidgets = []; this.getFoldWidget = foldMode.getFoldWidget.bind(foldMode, this, this.$foldStyle); this.getFoldWidgetRange = foldMode.getFoldWidgetRange.bind(foldMode, this, this.$foldStyle); this.$updateFoldWidgets = this.updateFoldWidgets.bind(this); this.on('change', this.$updateFoldWidgets); }; this.onFoldWidgetClick = function(row, e) { var type = this.getFoldWidget(row); var line = this.getLine(row); var onlySubfolds = e.shiftKey; var addSubfolds = onlySubfolds || e.ctrlKey || e.altKey || e.metaKey; var fold; if (type == "end") fold = this.getFoldAt(row, 0, -1); else fold = this.getFoldAt(row, line.length, 1); if (fold) { if (addSubfolds) this.removeFold(fold); else this.expandFold(fold); return; } var range = this.getFoldWidgetRange(row); if (range) { // sometimes singleline folds can be missed by the code above if (!range.isMultiLine()) { fold = this.getFoldAt(range.start.row, range.start.column, 1); if (fold && range.isEqual(fold.range)) { this.removeFold(fold); return; } } if (!onlySubfolds) this.addFold("...", range); if (addSubfolds) this.foldAll(range.start.row + 1, range.end.row); } else { if (addSubfolds) this.foldAll(row + 1, this.getLength()); e.target.className += " invalid" } }; this.updateFoldWidgets = function(e) { var delta = e.data; var range = delta.range; var firstRow = range.start.row; var len = range.end.row - firstRow; if (len === 0) { this.foldWidgets[firstRow] = null; } else if (delta.action == "removeText" || delta.action == "removeLines") { this.foldWidgets.splice(firstRow, len + 1, null); } else { var args = Array(len + 1); args.unshift(firstRow, 1); this.foldWidgets.splice.apply(this.foldWidgets, args); } }; } exports.Folding = Folding; });