diff --git a/lib/ace/document.js b/lib/ace/document.js index 11ca900d..a0bc4a5f 100644 --- a/lib/ace/document.js +++ b/lib/ace/document.js @@ -51,6 +51,7 @@ var Document = function(text, mode) { this.selection = new Selection(this); this.$breakpoints = []; this.$wrapData = []; + this.$computeWrapDataFn = this.$computeWrapData.bind(this); this.listeners = []; if (mode) { @@ -775,39 +776,18 @@ var Document = function(text, mode) { (function() { this.$wrapLimit = 80; this.$useWrapMode = false; + this.setUseWrapMode = function(useWrapMode) { - var _self = this; - - function computeWrapData(e) { - var lines = _self.lines, wrapData = _self.$wrapData; - var wrapLimit = _self.$wrapLimit; - - // Remove lines that are no longer there. - wrapData.splice(lines.length, wrapData.length - lines.length); - - if (!e.data.lastRow) { - e.data.lastRow = _self.lines.length - 1; - } - - for (var row = e.data.firstRow; row <= e.data.lastRow; row++) { - var col = wrapLimit; - wrapData[row] = []; - while (col < lines[row].length) { - wrapData[row].push(col); - col += wrapLimit; - } - } - }; - this.$useWrapMode = useWrapMode; - computeWrapData({ data: { firstRow: 0 } }); - this._dispatchEvent("changeWrapMode"); if (useWrapMode) { - this.addEventListener("change", computeWrapData); + this.addEventListener("change", this.$computeWrapDataFn); } else { - this.removeEventListener("change", computeWrapData); + this.removeEventListener("change", this.$computeWrapDataFn); } + + this.$computeWrapDataFn({ data: { firstRow: 0 } }); + this._dispatchEvent("changeWrapMode"); }; this.getUseWrapMode = function() { @@ -822,6 +802,157 @@ var Document = function(text, mode) { return this.$wrapLimit; }; + this.$computeWrapSplits = function(textLine, wrapLimit, tabSize) { + if (textLine.length == 0) { + return []; + } + + var tabLine = textLine.split("\t"); + var wrapSplits = []; + + var screenColumn = 0; + var documentColumn = 0; + var lastSplitColumn = 0; + + var reSpaces = /^\s*$/; + + // Remove all tablines from the end of the tabLine array that are empty + // or contain only space. This is necessary to prevent splits at the + // line end if only tabs/spaces are following. + // E.g: "foo bar\t " -> "foo bar". + var toRemove = tabLine.length - 1; + while (toRemove != -1 && reSpaces.test(tabLine[toRemove])) { + toRemove --; + } + tabLine.splice(toRemove + 1); + + // Remove spaces from the last tabLine to prevent splits only for + // whitespaces as well. + // E.g: "foo \tbar " -> "foo \tbar" + tabLine[tabLine.length - 1] = tabLine[tabLine.length - 1].trimRight(); + + var value; + var addedNewSplit; + + function addSplit(incrDocumentColumn) { + var valueLenLeadingWhitespaces; + value = value.substring(incrDocumentColumn); + valueLenLeadingWhitespaces = value.length; + value = value.trimLeft(); + + incrDocumentColumn += valueLenLeadingWhitespaces - value.length; + + // If there was a split saved before, then add it to the resulting + // array. It's not possible to store the split column here directly, + // as there migth be tabs/spaces following the current column that + // will cause the split position to get incremented. + if (lastSplitColumn) { + wrapSplits.push(lastSplitColumn); + } + + lastSplitColumn = documentColumn += incrDocumentColumn; + screenColumn = 0; + + addedNewSplit = true; + } + + for (var i = 0; i < tabLine.length; i++) { + value = tabLine[i]; + while (screenColumn + value.length >= wrapLimit) { + // Find the last space that we use to split the line. + var lastSpaceIdx = + value.substring(0, wrapLimit - screenColumn + 1). + lastIndexOf(" "); + + // There is no space to break the line. + if (lastSpaceIdx == -1) { + // True if this is the first tabLine we face + // during this split. This means, there is no + // space or tab to make the split -> force split. + if (screenColumn == 0) { + addSplit(wrapLimit); + } + // True if we've added some content in this split line + // already. Add a split after this already existing content + // in the split. + else { + addSplit(0); + } + } + // There is at least one space to wrap the line at. + else { + addSplit(lastSpaceIdx); + } + } + + // Add new content to the current split. + if (!addedNewSplit) { + screenColumn += value.length + tabSize; + documentColumn += value.length + 1; + } + // True if at least one new split was added during the while loop. + else { + var lenBefore = value.length; + value = value.trimLeft(); + // True if value has some content. This content goes directly + // after the last split. + if (value.length != 0) { + documentColumn = lastSplitColumn += lenBefore - value.length; + screenColumn += value.length; + documentColumn += 1; // The tab after this for-loop. + } + // There is no value left after the split. This means, following + // spaces and tabs have to be ignored. + else { + // Tab for this for-loop. + documentColumn = lastSplitColumn += 1; + + // While the following tabLines are empty/only spaces, set + // the latest split position behind them. + while ((i + 1) < tabLine.length - 1 + && reSpaces.test(tabLine[i + 1])) + { + i++; + documentColumn = lastSplitColumn += tabLine[i].length + 1; + } + + // The tabLine[i + 1] is not empty. However, the trailing + // spaces have still to be removed. + value = tabLine[i + 1]; + lenBefore = value.length; + value = value.trimLeft(); + documentColumn = lastSplitColumn += lenBefore - value.length; + tabLine[i + 1] = value; // Store the trimed value. + } + addedNewSplit = false; + } + } + if (lastSplitColumn) { + wrapSplits.push(lastSplitColumn); + } + + return wrapSplits; + }; + + this.$computeWrapData = function(e) { + var lines = this.lines; + var tabSize = this.getTabSize(); + var wrapData = this.$wrapData; + var wrapLimit = this.$wrapLimit; + + // Remove lines that are no longer there. + wrapData.splice(lines.length, wrapData.length - lines.length); + + if (!e.data.lastRow) { + e.data.lastRow = lines.length - 1; + } + + for (var row = e.data.firstRow; row <= e.data.lastRow; row++) { + wrapData[row] = + this.$computeWrapSplits(lines[row], wrapLimit, tabSize); + } + }; + this.getRowHeight = function(config, row) { var rows; if (!this.$useWrapMode) { diff --git a/lib/ace/test/all.js b/lib/ace/test/all.js index 88c03478..f4bd2d8e 100644 --- a/lib/ace/test/all.js +++ b/lib/ace/test/all.js @@ -56,5 +56,5 @@ async.concat( require("./mode/javascript_tokenizer_test"), require("./mode/text_test"), require("./mode/xml_test"), - require("./mode/xml_tokenizer_test") + require("./mode/xml_tokenizer_test") ).exec(); diff --git a/lib/ace/test/document_test.js b/lib/ace/test/document_test.js index c3bc3958..a01f64f6 100644 --- a/lib/ace/test/document_test.js +++ b/lib/ace/test/document_test.js @@ -43,7 +43,7 @@ var Document = require("../document").Document, Range = require("../range").Range, assert = require("./assertions"), async = require("async"); - + var Test = { "test: find matching opening bracket" : function() { @@ -81,16 +81,16 @@ var Test = { "test: move lines down" : function() { var doc = new Document(["a1", "a2", "a3", "a4"]); - + doc.moveLinesDown(0, 1); assert.equal(doc.toString(), ["a3", "a1", "a2", "a4"].join("\n")); - + doc.moveLinesDown(1, 2); assert.equal(doc.toString(), ["a3", "a4", "a1", "a2"].join("\n")); - + doc.moveLinesDown(2, 3); assert.equal(doc.toString(), ["a3", "a4", "a1", "a2"].join("\n")); - + doc.moveLinesDown(2, 2); assert.equal(doc.toString(), ["a3", "a4", "a2", "a1"].join("\n")); }, @@ -100,13 +100,13 @@ var Test = { doc.moveLinesUp(2, 3); assert.equal(doc.toString(), ["a1", "a3", "a4", "a2"].join("\n")); - + doc.moveLinesUp(1, 2); assert.equal(doc.toString(), ["a3", "a4", "a1", "a2"].join("\n")); - + doc.moveLinesUp(0, 1); assert.equal(doc.toString(), ["a3", "a4", "a1", "a2"].join("\n")); - + doc.moveLinesUp(2, 2); assert.equal(doc.toString(), ["a3", "a1", "a4", "a2"].join("\n")); }, @@ -134,13 +134,13 @@ var Test = { "test: should handle unix style new lines" : function() { var doc = new Document(["1", "2", "3"]); - + assert.equal(doc.toString(), ["1", "2", "3"].join("\n")); }, "test: should handle windows style new lines" : function() { var doc = new Document(["1", "2", "3"].join("\r\n")); - + doc.setNewLineMode("unix"); assert.equal(doc.toString(), ["1", "2", "3"].join("\n")); }, @@ -153,19 +153,19 @@ var Test = { "test: set new line mode to 'unix' should use '\n' as new lines": function() { var doc = new Document(["1", "2", "3"].join("\r\n")); - + doc.setNewLineMode("unix"); assert.equal(doc.toString(), ["1", "2", "3"].join("\n")); }, "test: set new line mode to 'auto' should detect the incoming nl type": function() { var doc = new Document(["1", "2", "3"].join("\n")); - + doc.setNewLineMode("auto"); assert.equal(doc.toString(), ["1", "2", "3"].join("\n")); var doc = new Document(["1", "2", "3"].join("\r\n")); - + doc.setNewLineMode("auto"); assert.equal(doc.toString(), ["1", "2", "3"].join("\r\n")); @@ -218,57 +218,57 @@ var Test = { assert.equal(doc.screenToDocumentColumn(0, 15), 12); assert.equal(doc.screenToDocumentColumn(0, 19), 13); }, - + "test: insert text in multiple rows": function() { var doc = new Document(["12", "", "abcd"]); - + var inserted = doc.multiRowInsert([0, 1, 2], 2, "juhu 1"); assert.equal(inserted.rows, 0); assert.equal(inserted.columns, 6); - + assert.equal(doc.toString(), ["12juhu 1", " juhu 1", "abjuhu 1cd"].join("\n")); }, "test: undo insert text in multiple rows": function() { var doc = new Document(["12", "", "abcd"]); - + var undoManager = new UndoManager(); doc.setUndoManager(undoManager); - + doc.multiRowInsert([0, 1, 2], 2, "juhu 1"); doc.$informUndoManager.call(); assert.equal(doc.toString(), ["12juhu 1", " juhu 1", "abjuhu 1cd"].join("\n")); undoManager.undo(); assert.equal(doc.toString(), ["12", "", "abcd"].join("\n")); - + undoManager.redo(); assert.equal(doc.toString(), ["12juhu 1", " juhu 1", "abjuhu 1cd"].join("\n")); }, - + "test: insert new line in multiple rows": function() { var doc = new Document(["12", "", "abcd"]); - + var inserted = doc.multiRowInsert([0, 1, 2], 2, "\n"); assert.equal(inserted.rows, 1); assert.equal(doc.toString(), ["12\n", " \n", "ab\ncd"].join("\n")); }, - + "test: insert multi line text in multiple rows": function() { var doc = new Document(["12", "", "abcd"]); - + var inserted = doc.multiRowInsert([0, 1, 2], 2, "juhu\n12"); assert.equal(inserted.rows, 1); assert.equal(doc.toString(), ["12juhu\n12", " juhu\n12", "abjuhu\n12cd"].join("\n")); }, - + "test: remove right in multiple rows" : function() { var doc = new Document(["12", "", "abcd"]); doc.multiRowRemove([0, 1, 2], new Range(0, 2, 0, 3)); assert.equal(doc.toString(), ["12", "", "abd"].join("\n")); }, - + "test: undo remove right in multiple rows" : function() { var doc = new Document(["12", "", "abcd"]); var undoManager = new UndoManager(); @@ -277,12 +277,49 @@ var Test = { doc.multiRowRemove([0, 1, 2], new Range(0, 1, 0, 3)); doc.$informUndoManager.call(); assert.equal(doc.toString(), ["1", "", "ad"].join("\n")); - + undoManager.undo(); assert.equal(doc.toString(), ["12", "", "abcd"].join("\n")); - + undoManager.redo(); assert.equal(doc.toString(), ["1", "", "ad"].join("\n")); + }, + + "test: wrapLine split function" : function() { + var splits; + var tabSize = 4; + var wrapLimit = 12; + var computeWrapSplits = Document.prototype.$computeWrapSplits; + var c = 0; + + function computeAndAssert(line, assertEqual) { + splits = computeWrapSplits(line, wrapLimit, tabSize); + assert.ok(splits.length == assertEqual.length); + for (var i = 0; i < splits.length; i++) { + assert.ok(splits[i] == assertEqual[i]); + } + } + + // Basic splitting. + computeAndAssert("foo bar foo bar", [ 12 ]); + computeAndAssert("foo bar f bar", [ 12 ]); + computeAndAssert("foo bar f r", [ 14 ]); + computeAndAssert("foo bar foo bar foo bara foo", [12, 25]); + + // Don't split if there is only whitespaces/tabs at the end of the line. + computeAndAssert("foo foo foo \t \t", [ ]); + + // If there is no space to split, force split. + computeAndAssert("foooooooooooooo", [ 12 ]); + computeAndAssert("fooooooooooooooooooooooooooo", [12, 24]); + computeAndAssert("foo bar fooooooooooobooooooo", [8, 20]); + + // Basic splitting + tabs. + computeAndAssert("foo \t\tbar", [ 6 ]); + computeAndAssert("foo \t \tbar", [ 7 ]); + + // Ignore spaces/tabs at beginning of split. + computeAndAssert("foo \t \t \t \t bar", [ 14]); } }; @@ -292,4 +329,4 @@ module.exports = require("async/test").testcase(Test); if (module === require.main) { require("../../../support/paths"); exports.exec() -} \ No newline at end of file +}