870 lines
29 KiB
JavaScript
870 lines
29 KiB
JavaScript
|
/* ***** 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
|
||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
|
*
|
||
|
* ***** 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) {
|
||
|
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);
|
||
|
}
|
||
|
}
|
||
|
start.column -= 1;
|
||
|
end.column += 1;
|
||
|
|
||
|
return foundFolds;
|
||
|
};
|
||
|
|
||
|
this.getFoldsInRangeList = function(ranges) {
|
||
|
if (Array.isArray(ranges)) {
|
||
|
var folds = [];
|
||
|
ranges.forEach(function(range) {
|
||
|
folds = folds.concat(this.getFoldsInRange(range));
|
||
|
}, this);
|
||
|
} else {
|
||
|
var folds = this.getFoldsInRange(ranges);
|
||
|
}
|
||
|
return folds;
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
* Returns all folds in the document
|
||
|
*/
|
||
|
this.getAllFolds = function() {
|
||
|
var folds = [];
|
||
|
var foldLines = this.$foldData;
|
||
|
|
||
|
for (var i = 0; i < foldLines.length; i++)
|
||
|
for (var j = 0; j < foldLines[i].folds.length; j++)
|
||
|
folds.push(foldLines[i].folds[j]);
|
||
|
|
||
|
return folds;
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
* Returns the string between folds at the given position.
|
||
|
* E.g.
|
||
|
* foo<fold>b|ar<fold>wolrd -> "bar"
|
||
|
* foo<fold>bar<fold>wol|rd -> "world"
|
||
|
* foo<fold>bar<fo|ld>wolrd -> <null>
|
||
|
*
|
||
|
* 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.
|
||
|
* foo<fold>b|ar<fold>wolrd -trim=-1> "b"
|
||
|
* foo<fold>bar<fold>wol|rd -trim=+1> "rld"
|
||
|
* fo|o<fold>bar<fold>wolrd -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);
|
||
|
fold.collapseChildren = range.collapseChildren;
|
||
|
}
|
||
|
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 (!(startRow < endRow ||
|
||
|
startRow == endRow && startColumn <= endColumn - 2))
|
||
|
throw new Error("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))
|
||
|
this.removeFold(startFold);
|
||
|
|
||
|
if (endFold && !endFold.range.isEnd(endRow, endColumn))
|
||
|
this.removeFold(endFold);
|
||
|
|
||
|
// 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.
|
||
|
folds.forEach(function(subFold) {
|
||
|
fold.addSubFold(subFold);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
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._signal("changeFold", { data: fold, action: "add" });
|
||
|
|
||
|
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.$updating) {
|
||
|
if (this.$useWrapMode)
|
||
|
this.$updateWrapData(startRow, endRow);
|
||
|
else
|
||
|
this.$updateRowLengthCache(startRow, endRow);
|
||
|
}
|
||
|
|
||
|
// Notify that fold data has changed.
|
||
|
this.$modified = true;
|
||
|
this._signal("changeFold", { data: fold, action: "remove" });
|
||
|
};
|
||
|
|
||
|
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(subFold) {
|
||
|
fold.restoreRange(subFold);
|
||
|
this.addFold(subFold);
|
||
|
}, this);
|
||
|
if (fold.collapseChildren > 0) {
|
||
|
this.foldAll(fold.start.row+1, fold.end.row, fold.collapseChildren-1);
|
||
|
}
|
||
|
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);
|
||
|
expandInner = true;
|
||
|
} 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.getFoldsInRangeList(range);
|
||
|
if (expandInner) {
|
||
|
this.removeFolds(folds);
|
||
|
} else {
|
||
|
var subFolds = folds;
|
||
|
// TODO: might be better to remove and add folds in one go instead of using
|
||
|
// expandFolds several times.
|
||
|
while (subFolds.length) {
|
||
|
this.expandFolds(subFolds);
|
||
|
subFolds = this.getFoldsInRangeList(range);
|
||
|
}
|
||
|
}
|
||
|
if (folds.length)
|
||
|
return folds;
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
* 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.getRowFoldStart = function(docRow, startFoldRow) {
|
||
|
var foldLine = this.getFoldLine(docRow, startFoldRow);
|
||
|
return foldLine ? foldLine.start.row : docRow;
|
||
|
};
|
||
|
|
||
|
this.getFoldDisplayLine = function(foldLine, endRow, endColumn, startRow, startColumn) {
|
||
|
if (startRow == null)
|
||
|
startRow = foldLine.start.row;
|
||
|
if (startColumn == null)
|
||
|
startColumn = 0;
|
||
|
if (endRow == null)
|
||
|
endRow = foldLine.end.row;
|
||
|
if (endColumn == null)
|
||
|
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;
|
||
|
if (row == startRow) {
|
||
|
if (column < startColumn)
|
||
|
return;
|
||
|
lastColumn = Math.max(startColumn, lastColumn);
|
||
|
}
|
||
|
|
||
|
if (placeholder != null) {
|
||
|
textLine += placeholder;
|
||
|
} else {
|
||
|
textLine += doc.getLine(row).substring(lastColumn, column);
|
||
|
}
|
||
|
}, 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, dir) {
|
||
|
var iterator = new TokenIterator(this, row, column);
|
||
|
var token = iterator.getCurrentToken();
|
||
|
var type = token.type;
|
||
|
if (token && /^comment|string/.test(type)) {
|
||
|
type = type.match(/comment|string/)[0];
|
||
|
if (type == "comment")
|
||
|
type += "|doc-start";
|
||
|
var re = new RegExp(type);
|
||
|
var range = new Range();
|
||
|
if (dir != 1) {
|
||
|
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);
|
||
|
|
||
|
if (dir != -1) {
|
||
|
var lastRow = -1;
|
||
|
do {
|
||
|
token = iterator.stepForward();
|
||
|
if (lastRow == -1) {
|
||
|
var state = this.getState(iterator.$row);
|
||
|
if (!re.test(state))
|
||
|
lastRow = iterator.$row;
|
||
|
} else if (iterator.$row > lastRow) {
|
||
|
break;
|
||
|
}
|
||
|
} while (token && re.test(token.type));
|
||
|
token = iterator.stepBackward();
|
||
|
} else
|
||
|
token = iterator.getCurrentToken();
|
||
|
|
||
|
range.end.row = iterator.getCurrentTokenRow();
|
||
|
range.end.column = iterator.getCurrentTokenColumn() + token.value.length - 2;
|
||
|
return range;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.foldAll = function(startRow, endRow, depth) {
|
||
|
if (depth == undefined)
|
||
|
depth = 100000; // JSON.stringify doesn't hanle Infinity
|
||
|
var foldWidgets = this.foldWidgets;
|
||
|
if (!foldWidgets)
|
||
|
return; // mode doesn't support folding
|
||
|
endRow = endRow || this.getLength();
|
||
|
startRow = startRow || 0;
|
||
|
for (var row = startRow; 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
|
||
|
// TODO change addFold to return null istead of throwing
|
||
|
if (range && range.isMultiLine()
|
||
|
&& range.end.row <= endRow
|
||
|
&& range.start.row >= startRow
|
||
|
) {
|
||
|
row = range.end.row;
|
||
|
try {
|
||
|
// addFold can change the range
|
||
|
var fold = this.addFold("...", range);
|
||
|
if (fold)
|
||
|
fold.collapseChildren = depth;
|
||
|
} catch(e) {}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// structured folding
|
||
|
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);
|
||
|
};
|
||
|
|
||
|
this.$setFolding = function(foldMode) {
|
||
|
if (this.$foldMode == foldMode)
|
||
|
return;
|
||
|
|
||
|
this.$foldMode = foldMode;
|
||
|
|
||
|
this.off('change', this.$updateFoldWidgets);
|
||
|
this.off('tokenizerUpdate', this.$tokenizerUpdateFoldWidgets);
|
||
|
this._signal("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.$tokenizerUpdateFoldWidgets = this.tokenizerUpdateFoldWidgets.bind(this);
|
||
|
this.on('change', this.$updateFoldWidgets);
|
||
|
this.on('tokenizerUpdate', this.$tokenizerUpdateFoldWidgets);
|
||
|
};
|
||
|
|
||
|
this.getParentFoldRangeData = function (row, ignoreCurrent) {
|
||
|
var fw = this.foldWidgets;
|
||
|
if (!fw || (ignoreCurrent && fw[row]))
|
||
|
return {};
|
||
|
|
||
|
var i = row - 1, firstRange;
|
||
|
while (i >= 0) {
|
||
|
var c = fw[i];
|
||
|
if (c == null)
|
||
|
c = fw[i] = this.getFoldWidget(i);
|
||
|
|
||
|
if (c == "start") {
|
||
|
var range = this.getFoldWidgetRange(i);
|
||
|
if (!firstRange)
|
||
|
firstRange = range;
|
||
|
if (range && range.end.row >= row)
|
||
|
break;
|
||
|
}
|
||
|
i--;
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
range: i !== -1 && range,
|
||
|
firstRange: firstRange
|
||
|
};
|
||
|
};
|
||
|
|
||
|
this.onFoldWidgetClick = function(row, e) {
|
||
|
e = e.domEvent;
|
||
|
var options = {
|
||
|
children: e.shiftKey,
|
||
|
all: e.ctrlKey || e.metaKey,
|
||
|
siblings: e.altKey
|
||
|
};
|
||
|
|
||
|
var range = this.$toggleFoldWidget(row, options);
|
||
|
if (!range) {
|
||
|
var el = (e.target || e.srcElement);
|
||
|
if (el && /ace_fold-widget/.test(el.className))
|
||
|
el.className += " ace_invalid";
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.$toggleFoldWidget = function(row, options) {
|
||
|
if (!this.getFoldWidget)
|
||
|
return;
|
||
|
var type = this.getFoldWidget(row);
|
||
|
var line = this.getLine(row);
|
||
|
|
||
|
var dir = type === "end" ? -1 : 1;
|
||
|
var fold = this.getFoldAt(row, dir === -1 ? 0 : line.length, dir);
|
||
|
|
||
|
if (fold) {
|
||
|
if (options.children || options.all)
|
||
|
this.removeFold(fold);
|
||
|
else
|
||
|
this.expandFold(fold);
|
||
|
return fold;
|
||
|
}
|
||
|
|
||
|
var range = this.getFoldWidgetRange(row, true);
|
||
|
// sometimes singleline folds can be missed by the code above
|
||
|
if (range && !range.isMultiLine()) {
|
||
|
fold = this.getFoldAt(range.start.row, range.start.column, 1);
|
||
|
if (fold && range.isEqual(fold.range)) {
|
||
|
this.removeFold(fold);
|
||
|
return fold;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (options.siblings) {
|
||
|
var data = this.getParentFoldRangeData(row);
|
||
|
if (data.range) {
|
||
|
var startRow = data.range.start.row + 1;
|
||
|
var endRow = data.range.end.row;
|
||
|
}
|
||
|
this.foldAll(startRow, endRow, options.all ? 10000 : 0);
|
||
|
} else if (options.children) {
|
||
|
endRow = range ? range.end.row : this.getLength();
|
||
|
this.foldAll(row + 1, endRow, options.all ? 10000 : 0);
|
||
|
} else if (range) {
|
||
|
if (options.all)
|
||
|
range.collapseChildren = 10000;
|
||
|
this.addFold("...", range);
|
||
|
}
|
||
|
|
||
|
return range;
|
||
|
};
|
||
|
|
||
|
|
||
|
|
||
|
this.toggleFoldWidget = function(toggleParent) {
|
||
|
var row = this.selection.getCursor().row;
|
||
|
row = this.getRowFoldStart(row);
|
||
|
var range = this.$toggleFoldWidget(row, {});
|
||
|
|
||
|
if (range)
|
||
|
return;
|
||
|
// handle toggleParent
|
||
|
var data = this.getParentFoldRangeData(row, true);
|
||
|
range = data.range || data.firstRange;
|
||
|
|
||
|
if (range) {
|
||
|
row = range.start.row;
|
||
|
var fold = this.getFoldAt(row, this.getLine(row).length, 1);
|
||
|
|
||
|
if (fold) {
|
||
|
this.removeFold(fold);
|
||
|
} else {
|
||
|
this.addFold("...", range);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
this.updateFoldWidgets = function(delta) {
|
||
|
var firstRow = delta.start.row;
|
||
|
var len = delta.end.row - firstRow;
|
||
|
|
||
|
if (len === 0) {
|
||
|
this.foldWidgets[firstRow] = null;
|
||
|
} else if (delta.action == 'remove') {
|
||
|
this.foldWidgets.splice(firstRow, len + 1, null);
|
||
|
} else {
|
||
|
var args = Array(len + 1);
|
||
|
args.unshift(firstRow, 1);
|
||
|
this.foldWidgets.splice.apply(this.foldWidgets, args);
|
||
|
}
|
||
|
};
|
||
|
this.tokenizerUpdateFoldWidgets = function(e) {
|
||
|
var rows = e.data;
|
||
|
if (rows.first != rows.last) {
|
||
|
if (this.foldWidgets.length > rows.first)
|
||
|
this.foldWidgets.splice(rows.first, this.foldWidgets.length);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
exports.Folding = Folding;
|
||
|
|
||
|
});
|