diff --git a/demo/startup.js b/demo/startup.js index 1403fd1b..fcb41ae4 100644 --- a/demo/startup.js +++ b/demo/startup.js @@ -44,7 +44,7 @@ exports.launch = function(env) { var event = require("pilot/event"); var Editor = require("ace/editor").Editor; var Renderer = require("ace/virtual_renderer").VirtualRenderer; - var theme = require("ace/theme/textmate"); + var theme = require("ace/theme/textmate"); var EditSession = require("ace/edit_session").EditSession; var JavaScriptMode = require("ace/mode/javascript").Mode; var CssMode = require("ace/mode/css").Mode; @@ -58,17 +58,27 @@ exports.launch = function(env) { var vim = require("ace/keyboard/keybinding/vim").Vim; var emacs = require("ace/keyboard/keybinding/emacs").Emacs; var HashHandler = require("ace/keyboard/hash_handler").HashHandler; - + var docs = {}; + // Make the lorem ipsum text a little bit longer. + var loreIpsum = document.getElementById("plaintext").innerHTML; + for (var i = 0; i < 5; i++) { + loreIpsum += loreIpsum; + } + docs.plain = new EditSession(loreIpsum); + docs.plain.setUseWrapMode(true); + docs.plain.setMode(new TextMode()); + docs.plain.setUndoManager(new UndoManager()); + docs.js = new EditSession(document.getElementById("jstext").innerHTML); docs.js.setMode(new JavaScriptMode()); docs.js.setUndoManager(new UndoManager()); - + if (false && window.Worker) { var worker = new WorkerClient("../..", ["ace", "pilot"], "ace/worker/mirror", "Mirror"); worker.call("setValue", [docs.js.getValue()]); - + docs.js.getDocument().on("change", function(e) { e.range = { start: e.data.range.start, @@ -76,7 +86,7 @@ exports.launch = function(env) { }; worker.emit("change", e); }); - + worker.on("jslint", function(results) { var errors = []; for (var i=0; i
- +
@@ -21,6 +21,7 @@ + @@ -48,25 +49,25 @@ - + - + - +
-
+
- + + diff --git a/lib/ace/document.js b/lib/ace/document.js index 7de5cd9b..c5c99b4e 100644 --- a/lib/ace/document.js +++ b/lib/ace/document.js @@ -64,9 +64,9 @@ var Document = function(text) { this.getValue = function() { return this.$lines.join(this.getNewLineCharacter()); }; - + // check for IE split bug - if ("aaa".split(/a/).length == 0) + if ("aaa".split(/a/).length == 0) this.$split = function(text) { return text.replace(/\r\n|\r/g, "\n").split("\n"); } @@ -75,7 +75,7 @@ var Document = function(text) { return text.split(/\r\n|\r|\n/); }; - + this.$detectNewLine = function(text) { var match = text.match(/^.*?(\r?\n)/m); if (match) { @@ -84,7 +84,7 @@ var Document = function(text) { this.$autoNewLine = "\n"; } }; - + this.getNewLineCharacter = function() { switch (this.$newLineMode) { case "windows": @@ -109,24 +109,24 @@ var Document = function(text) { this.getNewLineMode = function() { return this.$newLineMode; }; - + this.isNewLine = function(text) { return (text == "\r\n" || text == "\r" || text == "\n"); }; /** - * Get a verbatim copy of the given line as it is in the document + * Get a verbatim copy of the given line as it is in the document */ this.getLine = function(row) { return this.$lines[row] || ""; }; - + this.getLines = function(firstRow, lastRow) { return this.$lines.slice(firstRow, lastRow+1); }; - + /** - * Returns all lines in the document as string array. Warning: The caller + * Returns all lines in the document as string array. Warning: The caller * should not modify this array! */ this.getAllLines = function() { @@ -157,15 +157,15 @@ var Document = function(text) { position.row = Math.max(0, length - 1); position.column = this.getLine(length-1).length; } - return position; + return position; } this.insert = function(position, text) { if (text.length == 0) return position; - + position = this.$clipPosition(position); - + if (this.getLength() <= 1) this.$detectNewLine(text); @@ -182,56 +182,56 @@ var Document = function(text) { this.insertNewLine(end); if (newLines.length > 2) this.insertLines(position.row+1, newLines.slice(1, newLines.length-1)); - + var end = this.insertInLine({row: position.row + newLines.length - 1, column: 0}, newLines[newLines.length-1]); } return end; }; - + this.insertLines = function(row, lines) { if (lines.length == 0) return {row: row, column: 0}; - + var args = [row, 0]; args.push.apply(args, lines); this.$lines.splice.apply(this.$lines, args); - + var range = new Range(row, 0, row + lines.length - 1, 0); var delta = { action: "insertLines", range: range, lines: lines }; - this._dispatchEvent("change", { data: delta }); + this._dispatchEvent("change", { data: delta }); return range.end; }, - + this.insertNewLine = function(position) { position = this.$clipPosition(position); var line = this.$lines[position.row] || ""; this.$lines[position.row] = line.substring(0, position.column); this.$lines.splice(position.row + 1, 0, line.substring(position.column, line.length)); - + var end = { row : position.row + 1, column : 0 }; - + var delta = { action: "insertText", range: Range.fromPoints(position, end), text: this.getNewLineCharacter() }; this._dispatchEvent("change", { data: delta }); - + return end; }; - + this.insertInLine = function(position, text) { if (text.length == 0) return position; - + var line = this.$lines[position.row] || ""; this.$lines[position.row] = line.substring(0, position.column) + text + line.substring(position.column); @@ -240,14 +240,14 @@ var Document = function(text) { row : position.row, column : position.column + text.length }; - + var delta = { action: "insertText", range: Range.fromPoints(position, end), text: text }; this._dispatchEvent("change", { data: delta }); - + return end; }; @@ -258,12 +258,12 @@ var Document = function(text) { if (range.isEmpty()) return range.start; - + var firstRow = range.start.row; var lastRow = range.end.row; if (range.isMultiLine()) { - + // TODO removeInLine can be optimized away! this.removeInLine(lastRow, 0, range.end.column); if (lastRow - firstRow >= 2) @@ -276,17 +276,17 @@ var Document = function(text) { } return range.start; }; - + this.removeInLine = function(row, startColumn, endColumn) { if (startColumn == endColumn) return; - + var range = new Range(row, startColumn, row, endColumn); var line = this.getLine(row); var removed = line.substring(startColumn, endColumn); var newLine = line.substring(0, startColumn) + line.substring(endColumn, line.length); this.$lines.splice(row, 1, newLine); - + var delta = { action: "removeText", range: range, @@ -295,18 +295,18 @@ var Document = function(text) { this._dispatchEvent("change", { data: delta }); return range.start; }; - + /** * Removes a range of full lines - * + * * @param firstRow {Integer} The first row to be removed * @param lastRow {Integer} The last row to be removed * @return {String[]} The removed lines */ this.removeLines = function(firstRow, lastRow) { - var range = new Range(firstRow, 0, lastRow, this.$lines[lastRow].length); + var range = new Range(firstRow, 0, lastRow, this.$lines[lastRow].length); var removed = this.$lines.splice(firstRow, lastRow - firstRow + 1); - + var delta = { action: "removeLines", range: range, @@ -316,16 +316,16 @@ var Document = function(text) { this._dispatchEvent("change", { data: delta }); return removed; }; - + this.removeNewLine = function(row) { var firstLine = this.getLine(row); var secondLine = this.getLine(row+1); - + var range = new Range(row, firstLine.length, row+1, 0); var line = firstLine + secondLine; - + this.$lines.splice(row, 2, line); - + var delta = { action: "removeText", range: range, @@ -333,16 +333,16 @@ var Document = function(text) { }; this._dispatchEvent("change", { data: delta }); }; - + this.replace = function(range, text) { if (text.length == 0 && range.isEmpty()) return range.start; - + // Shortcut: If the text we want to insert is the same as it is already // in the document, we don't have to replace anything. if (text == this.getTextRange(range)) return range.end; - + this.remove(range); if (text) { var end = this.insert(range.start, text); @@ -350,15 +350,15 @@ var Document = function(text) { else { end = range.start; } - + return end; }; - + this.applyDeltas = function(deltas) { for (var i=0; i=0; i--) { var delta = deltas[i]; var range = Range.fromPoints(delta.range.start, delta.range.end); - + if (delta.action == "insertLines") this.removeLines(range.start.row, range.end.row) else if (delta.action == "insertText") @@ -383,7 +383,7 @@ var Document = function(text) { this.insertLines(range.start.row, delta.lines) else if (delta.action == "removeText") this.insert(range.start, delta.text) - } + } }; }).call(Document.prototype); diff --git a/lib/ace/edit_session.js b/lib/ace/edit_session.js index 5ae0998c..e3cbcbee 100644 --- a/lib/ace/edit_session.js +++ b/lib/ace/edit_session.js @@ -51,13 +51,14 @@ var EditSession = function(text, mode) { this.$modified = true; this.selection = new Selection(this); this.$breakpoints = []; + this.$wrapData = []; if (text instanceof Document) { this.setDocument(text) } else { this.setDocument(new Document(text)); } - + if (mode) this.setMode(mode); }; @@ -70,15 +71,15 @@ var EditSession = function(text, mode) { 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; @@ -86,6 +87,8 @@ var EditSession = function(text, mode) { this.$deltas.push(delta); this.$informUndoManager.schedule(); } + + this.$updateWrapDataOnChange(e); this._dispatchEvent("change", e); }; @@ -98,7 +101,7 @@ var EditSession = function(text, mode) { this.toString = function() { return this.doc.getValue(); }; - + this.getSelection = function() { return this.selection; }; @@ -164,7 +167,7 @@ var EditSession = function(text, mode) { this.getTabSize = function() { return this.$tabSize; }; - + this.isTabStop = function(position) { return this.$useSoftTabs && (position.column % this.$tabSize == 0); }; @@ -201,7 +204,7 @@ var EditSession = function(text, mode) { }; /** - * Error: + * Error: * { * row: 12, * column: 2, //can be undefined @@ -225,7 +228,7 @@ var EditSession = function(text, mode) { this.getAnnotations = function() { return this.$annotations; }; - + this.clearAnnotations = function() { this.$annotations = []; this._dispatchEvent("changeAnnotation", {}); @@ -239,12 +242,12 @@ var EditSession = function(text, mode) { this.$autoNewLine = "\n"; } }; - + this.tokenRe = /^[\w\d]+/g; this.nonTokenRe = /^(?:[^\w\d|[\u3040-\u309F]|[\u30A0-\u30FF]|[\u4E00-\u9FFF\uF900-\uFAFF\u3400-\u4DBF])+/g this.getWordRange = function(row, column) { var line = this.getLine(row); - + var inToken = false; if (column > 0) { inToken = !!line.charAt(column - 1).match(this.tokenRe); @@ -287,7 +290,7 @@ var EditSession = function(text, mode) { if (this.$worker) this.$worker.terminate(); - + if (window.Worker) this.$worker = mode.createWorker(this); @@ -315,18 +318,18 @@ var EditSession = function(text, mode) { }; this.getWidth = function() { - this.$computeWidth(); + this.$computeWidth(!this.width); return this.width; }; this.getScreenWidth = function() { - this.$computeWidth(); + this.$computeWidth(!this.screenWidth); return this.screenWidth; }; - this.$computeWidth = function() { - if (this.$modified) { - this.$modified = false; + this.$computeWidth = function(force) { + if (this.modified || force) { + this.modified = false; var lines = this.doc.getAllLines(); var longestLine = 0; @@ -337,24 +340,29 @@ var EditSession = function(text, mode) { var len = lines[i].length; longestLine = Math.max(longestLine, len); - lines[i].replace(/\t/g, function(m) { + lines[i].replace("\t", function(m) { len += tabSize-1; return m; }); longestScreenLine = Math.max(longestScreenLine, len); } this.width = longestLine; - this.screenWidth = longestScreenLine; + + 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 + * Get a verbatim copy of the given line as it is in the document */ this.getLine = function(row) { return this.doc.getLine(row); }; - + /** * Get a line as it is displayed on screen. Tabs are replaced by spaces. */ @@ -470,16 +478,16 @@ var EditSession = function(text, mode) { this.insert = function(position, text) { return this.doc.insert(position, text); }; - + /** * @param rows Array[Integer] sorted list of rows */ - this.multiRowInsert = function(rows, column, text) { + this.multiRowInsert = function(rows, column, text) { for (var i=rows.length-1; i>=0; i--) { var row = rows[i]; if (row >= this.doc.getLength()) continue; - + var diff = column - this.doc.getLine(row).length; if ( diff > 0) { var padded = lang.stringRepeat(" ", diff) + text; @@ -489,10 +497,10 @@ var EditSession = function(text, mode) { padded = text; offset = 0; } - + var end = this.insert({row: row, column: column+offset}, padded); } - + return { rows: end ? end.row - rows[0] : 0, columns: end ? end.column - column : 0 @@ -506,29 +514,29 @@ var EditSession = function(text, mode) { this.multiRowRemove = function(rows, range) { if (range.start.row !== rows[0]) throw new TypeError("range must start in the first row!"); - + var height = range.end.row - rows[0]; for (var i=rows.length-1; i>=0; i--) { var row = rows[i]; if (row >= this.doc.getLength()) continue; - + var end = this.remove(new Range(row, range.start.column, row+height, range.end.column)); } }; - + this.undoChanges = function(deltas) { if (!deltas.length) return; - + this.$fromUndo = true; this.doc.revertDeltas(deltas); this.$fromUndo = false; - + // update the selection var firstDelta = deltas[0]; var lastDelta = deltas[deltas.length-1]; - + this.selection.clearSelection(); if (firstDelta.action == "insertText" || firstDelta.action == "insertLines") this.selection.moveCursorToPosition(firstDelta.range.start); @@ -539,15 +547,15 @@ var EditSession = function(text, mode) { this.redoChanges = function(deltas) { if (!deltas.length) return; - + this.$fromUndo = true; this.doc.applyDeltas(deltas); this.$fromUndo = false; - + // update the selection var firstDelta = deltas[0]; var lastDelta = deltas[deltas.length-1]; - + this.selection.clearSelection(); if (firstDelta.action == "insertText" || firstDelta.action == "insertLines") this.selection.setSelectionRange(Range.fromPoints(firstDelta.range.start, lastDelta.range.end)); @@ -571,15 +579,15 @@ var EditSession = function(text, mode) { var rowRange = range.collapseRows(); var deleteRange = new Range(0, 0, 0, 0); var size = this.getTabSize(); - + for (var i = rowRange.start.row; i <= rowRange.end.row; ++i) { var line = this.getLine(i); - + deleteRange.start.row = i; deleteRange.end.row = i; for (var j = 0; j < size; ++j) if (line.charAt(j) != ' ') - break; + break; if (j < size && line.charAt(j) == '\t') { deleteRange.start.column = j; deleteRange.end.column = j + 1; @@ -594,7 +602,7 @@ var EditSession = function(text, mode) { this.remove(deleteRange); } return range; - } + } this.moveLinesUp = function(firstRow, lastRow) { if (firstRow <= 0) return 0; @@ -627,15 +635,228 @@ var EditSession = function(text, mode) { return Math.max(0, Math.min(row, this.doc.getLength()-1)); }; - this.documentToScreenColumn = function(row, docColumn) { + // WRAPMODE + this.$wrapLimit = 80; + this.$useWrapMode = false; + + this.setUseWrapMode = function(useWrapMode) { + if (useWrapMode != this.$useWrapMode) { + this.$useWrapMode = useWrapMode; + this.$updateWrapData(0, this.getLength() - 1); + this._dispatchEvent("changeWrapMode"); + // TODO: Add this one back. + // this.fireChangeEvent(0); + } + }; + + this.getUseWrapMode = function() { + return this.$useWrapMode; + }; + + this.setWrapLimit = function(wrapLimit) { + if (wrapLimit != this.$wrapLimit) { + this.$wrapLimit = wrapLimit; + this.$updateWrapData(0, this.getLength() - 1); + } + }; + + this.getWrapLimit = function() { + return this.$wrapLimit; + }; + + this.$updateWrapDataOnChange = function(e) { + if (!this.$useWrapMode) { + return; + } + + 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 { + firstRow = lastRow - e.data.lines.length; + } + } + + if (firstRow != lastRow) { + var len = lastRow - firstRow; + 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); + } + } + + 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; + + // Remove lines that are no longer there. + wrapData.splice(lines.length, wrapData.length - lines.length); + + for (var row = firstRow; row <= lastRow; row++) { + wrapData[row] = + this.$computeWrapSplits(lines[row], wrapLimit, tabSize); + } + }; + + this.$computeWrapSplits = function(textLine, wrapLimit, tabSize) { + textLine = textLine.trimRight(); + if (textLine.length == 0) { + return []; + } + + var reSpaces = /^\s*$/; + var tabLine = textLine.split("\t"); + var wrapSplits = []; + var screenColumn = 0; + var documentColumn = 0; + var lastSplitColumn = 0; + + 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 there is no content but we are in the last tabLine, we + // already know that there is no following tabLine and we can + // shortcut by using this if instead of the following else. + if (value.length != 0 || i == tabLine.length - 1) { + 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.getRowHeight = function(config, row) { + var rows; + if (!this.$useWrapMode) { + rows = 1; + } else { + rows = this.$wrapData[row].length + 1; + } + + return rows * config.lineHeight; + }; + + + /** + * 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 remaining = docColumn; - var line = this.getLine(row); - for (var i=0; i 0) { remaining -= 1; // tab @@ -647,7 +868,7 @@ var EditSession = function(text, mode) { c >= 0x3040 && c <= 0x309F || // Hiragana c >= 0x30A0 && c <= 0x30FF || // Katakana c >= 0x4E00 && c <= 0x9FFF || // Single CJK ideographs - c >= 0xF900 && c <= 0xFAFF || + c >= 0xF900 && c <= 0xFAFF || c >= 0x3400 && c <= 0x4DBF ) { screenColumn += 2; @@ -658,56 +879,258 @@ var EditSession = function(text, mode) { break; } } + + return screenColumn; + } - return screenColumn; + this.getScreenLastRowColumn = function(screenRow, returnDocPosition) { + if (!this.$useWrapMode) { + return this.$getStringScreenWidth(this.getLine(screenRow)); + } + + var rowData = this.$screenToDocumentRow(screenRow); + var docRow = rowData[0], + row = rowData[1]; + + var start, end; + if (this.$wrapData[docRow][row]) { + start = (this.$wrapData[docRow][row - 1] || 0); + end = this.$wrapData[docRow][row]; + returnDocPosition && end--; + } else { + end = this.getLine(docRow).length; + start = (this.$wrapData[docRow][row - 1] || 0); + } + if (!returnDocPosition) { + return this.$getStringScreenWidth(this.getLine(docRow).substring(start, end)); + } else { + return end; + } }; - this.screenToDocumentColumn = function(row, screenColumn) { + this.getDocumentLastRowColumn = function(docRow, docColumn) { + if (!this.$useWrapMode) { + return this.getLine(docRow).length; + } + + var screenRow = this.documentToScreenRow(docRow, docColumn); + return this.getScreenLastRowColumn(screenRow, true); + } + + this.getScreenFirstRowColumn = function(screenRow) { + if (!this.$useWrapMode) { + return 0; + } + + var rowData = this.$screenToDocumentRow(screenRow); + var docRow = rowData[0], + row = rowData[1]; + + return this.$wrapData[docRow][row - 1] || 0; + }; + + this.getRowSplitData = function(row) { + if (!this.$useWrapMode) { + return undefined; + } else { + return this.$wrapData[row]; + } + }; + + /** + * + * @returns array + * - array[0]: The documentRow aquivalent. + * - array[1]: The screenRowOffset to the first documentRow on the screen. + */ + this.$screenToDocumentRow = function(row) { + if (!this.$useWrapMode) { + return [row, 0]; + } + + var wrapData = this.$wrapData, linesCount = this.getLength(); + var docRow = 0; + while (docRow < linesCount && row >= 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; + }; + + this.screenToDocumentPosition = function(row, column) { + var line; + var docRow; + var docColumn; + var remaining = column; + if (!this.$useWrapMode) { + docRow = row; + row = 0; + docColumn = 0; + line = this.getLine(docRow).split("\t"); + } else { + var wrapData = this.$wrapData, linesCount = this.getLength(); + + var rowData = this.$screenToDocumentRow(row); + row = rowData[1]; + docRow = rowData[0]; + + if (docRow >= linesCount) { + return { + row: docRow, + column: 0 + }; + } + docColumn = wrapData[docRow][row - 1] || 0; + line = this.getLine(docRow).substring(docColumn).split("\t"); + } + + var tabSize = this.getTabSize(); + for (var i=0; i= len + tabSize) { + remaining -= (len + tabSize); + docColumn += (len + 1); + } + else if (remaining > len){ + docColumn += len; + break; + } + else { + docColumn += remaining; + break; + } + } + + // Clamp docColumn. + if (docRow < linesCount && wrapData[docRow][row]) { + if (docColumn >= wrapData[docRow][row]) { + // We remove one character at the end such that the docColumn + // position returned is not associated to the next row on the + // screen. + docColumn = wrapData[docRow][row] - 1; + } + } else if (this.getLine(docRow)) { + docColumn = Math.min(docColumn, this.getLine(docRow).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[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(); - var docColumn = 0; - var remaining = screenColumn; - var line = this.getLine(row); - - for(var i=0; i 0) { - docColumn += 1; - // tab - if (c == 9) { - if (remaining >= tabSize) { - remaining -= tabSize; - } else { - remaining = 0; - docColumn -= 1; - } - } - // CJK characters - else if ( - c >= 0x3040 && c <= 0x309F || // Hiragana - c >= 0x30A0 && c <= 0x30FF || // Katakana - c >= 0x4E00 && c <= 0x9FFF || // Single CJK ideographs - c >= 0xF900 && c <= 0xFAFF || - c >= 0x3400 && c <= 0x4DBF - ) { - if (remaining >= 2) { - remaining -= 2; - } else { - remaining = 0; - docColumn -= 1; - } - } else { - remaining -= 1; - } - } else { - break; - } - } - - return docColumn; + // 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 += (str.split("\t").length - 1) * (tabSize - 1); + return { + row: row, + column: column + }; + } + if (row >= this.getLength()) { + return { + row: screenRow, + column: 0 + }; + } + + var split; + var wrapRowData = this.$wrapData[row]; + var screenRow, screenRowOffset; + var screenColumn; + var rowData = this.$documentToScreenRow(row, column); + screenRow = rowData[0]; + screenRowOffset = rowData[1]; + screenColumn = column - (wrapRowData[screenRowOffset - 1] || 0); + + str = this.getLine(row).substring( + wrapRowData[screenRowOffset - 1] || 0, column); + screenColumn += (str.split("\t").length - 1) * (tabSize - 1); + + 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; + } + }).call(EditSession.prototype); exports.EditSession = EditSession; diff --git a/lib/ace/editor.js b/lib/ace/editor.js index 39e5ca4e..00447554 100644 --- a/lib/ace/editor.js +++ b/lib/ace/editor.js @@ -22,6 +22,7 @@ * Contributor(s): * Fabian Jakobs * Irakli Gozalishvili (http://jeditoolkit.com) + * 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 @@ -128,6 +129,7 @@ var Editor =function(renderer, session) { this.session.removeEventListener("change", this.$onDocumentChange); this.session.removeEventListener("changeMode", this.$onDocumentModeChange); this.session.removeEventListener("changeTabSize", this.$onDocumentChangeTabSize); + this.session.removeEventListener("changeWrapMode", this.$onDocumentChangeWrapMode); this.session.removeEventListener("changeBreakpoint", this.$onDocumentChangeBreakpoint); this.session.removeEventListener("changeAnnotation", this.$onDocumentChangeAnnotation); @@ -150,14 +152,16 @@ var Editor =function(renderer, session) { this.$onDocumentChangeTabSize = this.renderer.updateText.bind(this.renderer); session.addEventListener("changeTabSize", this.$onDocumentChangeTabSize); + this.$onDocumentChangeWrapMode = this.renderer.updateFull.bind(this.renderer); + session.addEventListener("changeWrapMode", this.$onDocumentChangeWrapMode); + this.$onDocumentChangeBreakpoint = this.onDocumentChangeBreakpoint.bind(this); this.session.addEventListener("changeBreakpoint", this.$onDocumentChangeBreakpoint); - + this.$onDocumentChangeAnnotation = this.onDocumentChangeAnnotation.bind(this); this.session.addEventListener("changeAnnotation", this.$onDocumentChangeAnnotation); this.selection = session.getSelection(); - this.$desiredColumn = 0; this.$onCursorChange = this.onCursorChange.bind(this); this.selection.addEventListener("changeCursor", this.$onCursorChange); @@ -175,7 +179,7 @@ var Editor =function(renderer, session) { this.onDocumentChangeAnnotation(); this.renderer.scrollToRow(session.getScrollTopRow()); this.renderer.updateFull(); - + this._dispatchEvent("changeSession", { session: session, oldSession: oldSession @@ -258,11 +262,11 @@ var Editor =function(renderer, session) { this.onDocumentChange = function(e) { var delta = e.data; var range = delta.range; - + this.bgTokenizer.start(range.start.row); if (range.start.row == range.end.row) var lastRow = range.end.row; - else + else lastRow = Infinity; this.renderer.updateLines(range.start.row, lastRow); @@ -414,13 +418,11 @@ var Editor =function(renderer, session) { this.onMouseDoubleClick = function(e) { this.selection.selectWord(); this.$clickSelection = this.getSelectionRange(); - this.$updateDesiredColumn(); }; this.onMouseTripleClick = function(e) { this.selection.selectLine(); this.$clickSelection = this.getSelectionRange(); - this.$updateDesiredColumn(); }; this.onMouseWheel = function(e) { @@ -684,7 +686,6 @@ var Editor =function(renderer, session) { var range = this.session.outdentRows(selection.getRange()); selection.setSelectionRange(range, selection.isBackwards()); - this.$updateDesiredColumn(); }; this.toggleCommentLines = function() { @@ -874,17 +875,14 @@ var Editor =function(renderer, session) { this.clearSelection = function() { this.selection.clearSelection(); - this.$updateDesiredColumn(); }; this.moveCursorTo = function(row, column) { this.selection.moveCursorTo(row, column); - this.$updateDesiredColumn(); }; this.moveCursorToPosition = function(pos) { this.selection.moveCursorToPosition(pos); - this.$updateDesiredColumn(); }; @@ -903,34 +901,16 @@ var Editor =function(renderer, session) { this.navigateTo = function(row, column) { this.clearSelection(); this.moveCursorTo(row, column); - this.$updateDesiredColumn(column); }; this.navigateUp = function(times) { this.selection.clearSelection(); - this.selection.moveCursorBy(-(times || 1), 0); - - if (this.$desiredColumn) { - var cursor = this.getCursorPosition(); - var column = this.session.screenToDocumentColumn(cursor.row, this.$desiredColumn); - this.selection.moveCursorTo(cursor.row, column); - } + this.selection.moveCursorBy(-1, 0); }; this.navigateDown = function(times) { this.selection.clearSelection(); - this.selection.moveCursorBy(times || 1, 0); - - if (this.$desiredColumn) { - var cursor = this.getCursorPosition(); - var column = this.session.screenToDocumentColumn(cursor.row, this.$desiredColumn); - this.selection.moveCursorTo(cursor.row, column); - } - }; - - this.$updateDesiredColumn = function() { - var cursor = this.getCursorPosition(); - this.$desiredColumn = this.session.documentToScreenColumn(cursor.row, cursor.column); + this.selection.moveCursorBy(1, 0); }; this.navigateLeft = function(times) { @@ -999,7 +979,6 @@ var Editor =function(renderer, session) { this.$tryReplace(range, replacement); if (range !== null) this.selection.setSelectionRange(range); - this.$updateDesiredColumn(); }, this.replaceAll = function(replacement, options) { @@ -1018,7 +997,6 @@ var Editor =function(renderer, session) { this.$tryReplace(ranges[i], replacement); if (ranges[0] !== null) this.selection.setSelectionRange(ranges[0]); - this.$updateDesiredColumn(); }, this.$tryReplace = function(range, replacement) { @@ -1071,7 +1049,6 @@ var Editor =function(renderer, session) { var range = this.$search.find(this.session); if (range) { this.gotoLine(range.end.row+1, range.end.column); - this.$updateDesiredColumn(); this.selection.setSelectionRange(range); } }; diff --git a/lib/ace/layer/cursor.js b/lib/ace/layer/cursor.js index 60a6938e..ffc2130c 100644 --- a/lib/ace/layer/cursor.js +++ b/lib/ace/layer/cursor.js @@ -20,6 +20,7 @@ * * Contributor(s): * Fabian Jakobs + * 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 @@ -57,10 +58,9 @@ var Cursor = function(parentEl) { }; this.setCursor = function(position, overwrite) { - this.position = { - row : position.row, - column : this.session.documentToScreenColumn(position.row, position.column) - }; + this.position = + this.session.documentToScreenPosition(position); + if (overwrite) { dom.addCssClass(this.cursor, "ace_overwrite"); } else { @@ -100,7 +100,7 @@ var Cursor = function(parentEl) { }, 1000); }; - this.getPixelPosition = function() { + this.getPixelPosition = function(onScreen) { if (!this.config || !this.position) { return { left : 0, @@ -108,10 +108,12 @@ var Cursor = function(parentEl) { }; } - var cursorLeft = Math.round(this.position.column * this.config.characterWidth); - var cursorTop = this.position.row * this.config.lineHeight; + var pos = this.position; + var cursorLeft = Math.round(pos.column * this.config.characterWidth); + var cursorTop = (pos.row - (onScreen ? this.config.firstRowScreen : 0)) * + this.config.lineHeight; - return { + return { left : cursorLeft, top : cursorTop }; @@ -123,17 +125,10 @@ var Cursor = function(parentEl) { this.config = config; - var cursorLeft = Math.round(this.position.column * config.characterWidth); - var cursorTop = this.position.row * config.lineHeight; + this.pixelPos = this.getPixelPosition(true); - this.pixelPos = { - left : cursorLeft, - top : cursorTop - }; - - this.cursor.style.left = cursorLeft + "px"; - this.cursor.style.top = (cursorTop - (config.firstRow * config.lineHeight)) - + "px"; + this.cursor.style.left = this.pixelPos.left + "px"; + this.cursor.style.top = this.pixelPos.top + "px"; this.cursor.style.width = config.characterWidth + "px"; this.cursor.style.height = config.lineHeight + "px"; diff --git a/lib/ace/layer/gutter.js b/lib/ace/layer/gutter.js index 13c85bad..a7cd82f1 100644 --- a/lib/ace/layer/gutter.js +++ b/lib/ace/layer/gutter.js @@ -20,6 +20,7 @@ * * Contributor(s): * Fabian Jakobs + * 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 @@ -51,12 +52,16 @@ var Gutter = function(parentEl) { (function() { + this.setSession = function(session) { + this.session = session; + }; + this.addGutterDecoration = function(row, className){ - if (!this.$decorations[row]) + if (!this.$decorations[row]) this.$decorations[row] = ""; this.$decorations[row] += " ace_" + className; } - + this.removeGutterDecoration = function(row, className){ this.$decorations[row] = this.$decorations[row].replace(" ace_" + className, ""); }; @@ -101,7 +106,7 @@ var Gutter = function(parentEl) { this.$breakpoints[i] ? " ace_breakpoint " : " ", annotation.className, "' title='", annotation.text.join("\n"), - "' style='height:", config.lineHeight, "px;'>", (i+1), "
"); + "' style='height:", this.session.getRowHeight(config, i), "px;'>", (i+1), ""); html.push(""); } this.element = dom.setInnerHtml(this.element, html.join("")); diff --git a/lib/ace/layer/marker.js b/lib/ace/layer/marker.js index 027212b4..30f777ed 100644 --- a/lib/ace/layer/marker.js +++ b/lib/ace/layer/marker.js @@ -20,6 +20,7 @@ * * Contributor(s): * Fabian Jakobs + * 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 @@ -87,6 +88,8 @@ var Marker = function(parentEl) { var range = marker.range.clipRows(config.firstRow, config.lastRow); if (range.isEmpty()) continue; + range = range.toScreenRange(this.session); + if (range.isMultiLine()) { if (marker.type == "text") { this.drawTextMarker(html, range, marker.clazz, config); @@ -101,11 +104,16 @@ var Marker = function(parentEl) { this.element = dom.setInnerHtml(this.element, html.join("")); }; - this.drawTextMarker = function(stringBuilder, range, clazz, layerConfig) { + this.$getTop = function(row, layerConfig) { + return (row - layerConfig.firstRowScreen) * layerConfig.lineHeight; + }; + this.drawTextMarker = function(stringBuilder, range, clazz, layerConfig) { // selection start var row = range.start.row; - var lineRange = new Range(row, range.start.column, row, this.session.getLine(row).length); + + var lineRange = new Range(row, range.start.column, + row, this.session.getScreenLastRowColumn(row)); this.drawSingleLineMarker(stringBuilder, lineRange, clazz, layerConfig, 1); // selection end @@ -116,18 +124,16 @@ var Marker = function(parentEl) { for (var row = range.start.row + 1; row < range.end.row; row++) { lineRange.start.row = row; lineRange.end.row = row; - lineRange.end.column = this.session.getLine(row).length; // account for endofline characters + lineRange.end.column = this.session.getScreenLastRowColumn(row); this.drawSingleLineMarker(stringBuilder, lineRange, clazz, layerConfig, 1); } }; this.drawMultiLineMarker = function(stringBuilder, range, clazz, layerConfig) { - var range = range.toScreenRange(this.session); - // from selection start to the end of the line var height = layerConfig.lineHeight; var width = Math.round(layerConfig.width - (range.start.column * layerConfig.characterWidth)); - var top = (range.start.row - layerConfig.firstRow) * layerConfig.lineHeight; + var top = this.$getTop(range.start.row, layerConfig); var left = Math.round(range.start.column * layerConfig.characterWidth); stringBuilder.push( @@ -139,7 +145,7 @@ var Marker = function(parentEl) { ); // from start of the last line to the selection end - var top = (range.end.row - layerConfig.firstRow) * layerConfig.lineHeight; + var top = this.$getTop(range.end.row, layerConfig); var width = Math.round(range.end.column * layerConfig.characterWidth); stringBuilder.push( @@ -153,7 +159,7 @@ var Marker = function(parentEl) { var height = (range.end.row - range.start.row - 1) * layerConfig.lineHeight; if (height < 0) return; - var top = (range.start.row + 1 - layerConfig.firstRow) * layerConfig.lineHeight; + var top = this.$getTop(range.start.row + 1, layerConfig); stringBuilder.push( "
oldConfig.lastRow) { var fragment = this.$renderLinesFragment(config, oldConfig.lastRow + 1, config.lastRow); el.appendChild(fragment); @@ -214,7 +223,7 @@ var Text = function(parentEl) { var lineEl = document.createElement("div"); lineEl.className = "ace_line"; var style = lineEl.style; - style.height = this.$characterSize.height + "px"; + style.height = this.session.getRowHeight(config, row) + "px"; style.width = config.width + "px"; var html = []; @@ -231,14 +240,12 @@ var Text = function(parentEl) { this.config = config; var html = []; - var tokens = this.tokenizer.getTokens(config.firstRow, config.lastRow); - for (var i=config.firstRow; i<=config.lastRow; i++) { - html.push("
"); - this.$renderLine(html, i, tokens[i-config.firstRow].tokens), html.push("
"); - } + var tokens = this.tokenizer.getTokens(config.firstRow, config.lastRow) + var fragment = this.$renderLinesFragment(config, config.firstRow, config.lastRow); - this.element = dom.setInnerHtml(this.element, html.join("")); + // Clear the current content of the element and add the rendered fragment. + this.element.innerHTML = ""; + this.element.appendChild(fragment); }; this.$textToken = { @@ -258,7 +265,7 @@ var Text = function(parentEl) { var space = new Array(space.length+1).join(self.SPACE_CHAR); return "" + space + ""; } - + }; } else { @@ -266,27 +273,62 @@ var Text = function(parentEl) { var spaceReplace = " "; } - var characterWidth = this.config.characterWidth; - - for ( var i = 0; i < tokens.length; i++) { - var token = tokens[i]; - - var output = token.value + var _self = this; + function addToken(token, value) { + var output = value .replace(/&/g, "&") .replace(/" + c + "" }); - - if (!this.$textToken[token.type]) { + + if (!_self.$textToken[token.type]) { var classes = "ace_" + token.type.replace(/\./g, " ace_"); stringBuilder.push("", output, ""); } else { stringBuilder.push(output); } + } + + var splits = this.session.getRowSplitData(row); + var chars = 0, split = 0, splitChars; + + if (!splits || splits.length == 0) { + splitChars = Number.MAX_VALUE; + } else { + splitChars = splits[0]; + } + + stringBuilder.push("
"); + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + var value = token.value; + + if (chars + value.length < splitChars) { + addToken(token, value); + chars += value.length; + } else { + while (chars + value.length >= splitChars) { + addToken(token, value.substring(0, splitChars - chars)); + value = value.substring(splitChars - chars); + chars = splitChars; + stringBuilder.push("
", + "
"); + split ++; + splitChars = splits[split] || Number.MAX_VALUE; + } + if (value.length != 0) { + chars += value.length; + addToken(token, value); + } + } }; if (this.showInvisibles) { @@ -296,6 +338,7 @@ var Text = function(parentEl) { stringBuilder.push("" + this.EOF_CHAR + ""); } } + stringBuilder.push("
"); }; }).call(Text.prototype); diff --git a/lib/ace/range.js b/lib/ace/range.js index e6727053..4c613699 100644 --- a/lib/ace/range.js +++ b/lib/ace/range.js @@ -137,7 +137,7 @@ var Range = function(startRow, startColumn, endRow, endColumn) { this.clone = function() { return Range.fromPoints(this.start, this.end); }; - + this.collapseRows = function() { if (this.end.column == 0) return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0) @@ -145,10 +145,14 @@ var Range = function(startRow, startColumn, endRow, endColumn) { return new Range(this.start.row, 0, this.end.row, 0) }; - this.toScreenRange = function(doc) { + this.toScreenRange = function(session) { + var screenPosStart = + session.documentToScreenPosition(this.start); + var screenPosEnd = + session.documentToScreenPosition(this.end); return new Range( - this.start.row, doc.documentToScreenColumn(this.start.row, this.start.column), - this.end.row, doc.documentToScreenColumn(this.end.row, this.end.column) + screenPosStart.row, screenPosStart.column, + screenPosEnd.row, screenPosEnd.column ); }; diff --git a/lib/ace/selection.js b/lib/ace/selection.js index 832f0598..2adc54be 100644 --- a/lib/ace/selection.js +++ b/lib/ace/selection.js @@ -20,6 +20,7 @@ * * Contributor(s): * Fabian Jakobs + * 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 @@ -165,11 +166,19 @@ var Selection = function(doc) { this.setSelectionRange = function(range, reverse) { if (reverse) { this.setSelectionAnchor(range.end.row, range.end.column); - this.selectTo(range.start.row, range.start.column); + this.selectTo(range.start.row, range.start.column); } else { this.setSelectionAnchor(range.start.row, range.start.column); this.selectTo(range.end.row, range.end.column); } + this.$updateDesiredColumn(); + }; + + this.$updateDesiredColumn = function() { + var cursor = this.getCursor(); + if (cursor) { + this.$desiredColumn = this.doc.documentToScreenColumn(cursor.row, cursor.column); + } }; this.$moveSelection = function(mover) { @@ -248,7 +257,7 @@ var Selection = function(doc) { var column = cursor.column; var range = this.doc.getWordRange(cursor.row, column); this.setSelectionRange(range); - + /*this.setSelectionAnchor(cursor.row, start); this.$moveSelection(function() { this.moveCursorTo(cursor.row, end); @@ -272,7 +281,7 @@ var Selection = function(doc) { this.moveCursorLeft = function() { if (this.selectionLead.column == 0) { - // cursor is a line start + // cursor is a line (start if (this.selectionLead.row > 0) { this.moveCursorTo(this.selectionLead.row - 1, this.doc .getLine(this.selectionLead.row - 1).length); @@ -309,19 +318,27 @@ var Selection = function(doc) { this.moveCursorLineStart = function() { var row = this.selectionLead.row; var column = this.selectionLead.column; - var beforeCursor = this.doc.getLine(row).slice(0, column); + var screenRow = this.doc.documentToScreenRow(row, column); + var firstRowColumn = this.doc.getScreenFirstRowColumn(screenRow); + var beforeCursor = this.doc.getLine(row).slice(firstRowColumn, column); var leadingSpace = beforeCursor.match(/^\s*/); - if (leadingSpace[0].length == 0) - this.moveCursorTo(row, this.doc.getLine(row).match(/^\s*/)[0].length); - else if (leadingSpace[0].length >= column) - this.moveCursorTo(row, 0); - else - this.moveCursorTo(row, leadingSpace[0].length); + if (leadingSpace[0].length == 0) { + var lastRowColumn = this.doc.getDocumentLastRowColumn(row, column); + leadingSpace = this.doc.getLine(row). + substring(firstRowColumn, lastRowColumn). + match(/^\s*/); + this.moveCursorTo(row, firstRowColumn + leadingSpace[0].length); + } else if (leadingSpace[0].length >= column) { + this.moveCursorTo(row, firstRowColumn); + } else { + this.moveCursorTo(row, firstRowColumn + leadingSpace[0].length); + } }; this.moveCursorLineEnd = function() { - this.moveCursorTo(this.selectionLead.row, - this.doc.getLine(this.selectionLead.row).length); + var selLead = this.selectionLead; + this.moveCursorTo(selLead.row, + this.doc.getDocumentLastRowColumn(selLead.row, selLead.column)); }; this.moveCursorFileEnd = function() { @@ -387,7 +404,21 @@ var Selection = function(doc) { }; this.moveCursorBy = function(rows, chars) { - this.moveCursorTo(this.selectionLead.row + rows, this.selectionLead.column + chars); + if (this.doc.getUseWrapMode()) { + var screenPos = this.doc.documentToScreenPosition( + this.selectionLead.row, this.selectionLead.column); + var screenCol = + (chars == 0 && this.$desiredColumn) || screenPos.column; + + var docPos = this.doc.screenToDocumentPosition( + screenPos.row + rows, screenCol); + this.moveCursorTo(docPos.row, docPos.column + chars, chars == 0); + } else { + var docColumn = + (chars == 0 && this.$desiredColumn) || this.selectionLead.column; + this.moveCursorTo( + this.selectionLead.row + rows, docColumn + chars, chars == 0); + } }; @@ -395,20 +426,17 @@ var Selection = function(doc) { this.moveCursorTo(position.row, position.column); }; - this.moveCursorTo = function(row, column) { + this.moveCursorTo = function(row, column, preventUpdateDesiredColumn) { var cursor = this.$clipPositionToDocument(row, column); // only dispatch change if the cursor actually changed if (cursor.row !== this.selectionLead.row || cursor.column !== this.selectionLead.column) { this.selectionLead = cursor; + !preventUpdateDesiredColumn && this.$updateDesiredColumn(column); this._dispatchEvent("changeCursor", { data: this.getCursor() }); } }; - this.moveCursorUp = function() { - this.moveCursorBy(-1, 0); - }; - this.$clipPositionToDocument = function(row, column) { var pos = {}; diff --git a/lib/ace/test/all.js b/lib/ace/test/all.js index 00df7b59..46b5b55e 100644 --- a/lib/ace/test/all.js +++ b/lib/ace/test/all.js @@ -57,5 +57,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 c46602cd..58cda084 100644 --- a/lib/ace/test/document_test.js +++ b/lib/ace/test/document_test.js @@ -20,6 +20,7 @@ * * Contributor(s): * Fabian Jakobs + * 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 @@ -39,7 +40,7 @@ var Document = require("../document").Document, Range = require("../range").Range, assert = require("./assertions"), async = require("asyncjs"); - + var Test = { "test: insert text in line" : function() { @@ -47,194 +48,194 @@ var Test = { var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - + doc.insert({row: 0, column: 1}, "juhu"); assert.equal(doc.getValue(), ["1juhu2", "34"].join("\n")); - - var d = deltas.concat(); + + var d = deltas.concat(); doc.revertDeltas(d); assert.equal(doc.getValue(), ["12", "34"].join("\n")); - + doc.applyDeltas(d); assert.equal(doc.getValue(), ["1juhu2", "34"].join("\n")); }, - + "test: insert new line" : function() { var doc = new Document(["12", "34"]); - + var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); doc.insertNewLine({row: 0, column: 1}); assert.equal(doc.getValue(), ["1", "2", "34"].join("\n")); - - var d = deltas.concat(); + + var d = deltas.concat(); doc.revertDeltas(d); assert.equal(doc.getValue(), ["12", "34"].join("\n")); - + doc.applyDeltas(d); assert.equal(doc.getValue(), ["1", "2", "34"].join("\n")); }, - + "test: insert lines at the beginning" : function() { var doc = new Document(["12", "34"]); - + var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - + doc.insertLines(0, ["aa", "bb"]); assert.equal(doc.getValue(), ["aa", "bb", "12", "34"].join("\n")); - - var d = deltas.concat(); + + var d = deltas.concat(); doc.revertDeltas(d); assert.equal(doc.getValue(), ["12", "34"].join("\n")); - + doc.applyDeltas(d); assert.equal(doc.getValue(), ["aa", "bb", "12", "34"].join("\n")); }, - + "test: insert lines at the end" : function() { var doc = new Document(["12", "34"]); - + var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - + doc.insertLines(2, ["aa", "bb"]); assert.equal(doc.getValue(), ["12", "34", "aa", "bb"].join("\n")); }, - + "test: insert lines in the middle" : function() { var doc = new Document(["12", "34"]); - + var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - + doc.insertLines(1, ["aa", "bb"]); assert.equal(doc.getValue(), ["12", "aa", "bb", "34"].join("\n")); - - var d = deltas.concat(); + + var d = deltas.concat(); doc.revertDeltas(d); assert.equal(doc.getValue(), ["12", "34"].join("\n")); - + doc.applyDeltas(d); assert.equal(doc.getValue(), ["12", "aa", "bb", "34"].join("\n")); }, - + "test: insert multi line string at the start" : function() { var doc = new Document(["12", "34"]); - + var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - + doc.insert({row: 0, column: 0}, "aa\nbb\ncc"); assert.equal(doc.getValue(), ["aa", "bb", "cc12", "34"].join("\n")); - - var d = deltas.concat(); + + var d = deltas.concat(); doc.revertDeltas(d); assert.equal(doc.getValue(), ["12", "34"].join("\n")); - + doc.applyDeltas(d); assert.equal(doc.getValue(), ["aa", "bb", "cc12", "34"].join("\n")); }, - + "test: insert multi line string at the end" : function() { var doc = new Document(["12", "34"]); - + var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - + doc.insert({row: 2, column: 0}, "aa\nbb\ncc"); assert.equal(doc.getValue(), ["12", "34aa", "bb", "cc"].join("\n")); - - var d = deltas.concat(); + + var d = deltas.concat(); doc.revertDeltas(d); assert.equal(doc.getValue(), ["12", "34"].join("\n")); - + doc.applyDeltas(d); assert.equal(doc.getValue(), ["12", "34aa", "bb", "cc"].join("\n")); }, - + "test: insert multi line string in the middle" : function() { var doc = new Document(["12", "34"]); - + var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - + doc.insert({row: 0, column: 1}, "aa\nbb\ncc"); assert.equal(doc.getValue(), ["1aa", "bb", "cc2", "34"].join("\n")); - - var d = deltas.concat(); + + var d = deltas.concat(); doc.revertDeltas(d); assert.equal(doc.getValue(), ["12", "34"].join("\n")); - + doc.applyDeltas(d); assert.equal(doc.getValue(), ["1aa", "bb", "cc2", "34"].join("\n")); }, - + "test: delete in line" : function() { var doc = new Document(["1234", "5678"]); - + var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - + doc.remove(new Range(0, 1, 0, 3)); assert.equal(doc.getValue(), ["14", "5678"].join("\n")); - - var d = deltas.concat(); + + var d = deltas.concat(); doc.revertDeltas(d); assert.equal(doc.getValue(), ["1234", "5678"].join("\n")); - + doc.applyDeltas(d); assert.equal(doc.getValue(), ["14", "5678"].join("\n")); }, "test: delete new line" : function() { var doc = new Document(["1234", "5678"]); - + var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - + doc.remove(new Range(0, 4, 1, 0)); assert.equal(doc.getValue(), ["12345678"].join("\n")); - - var d = deltas.concat(); + + var d = deltas.concat(); doc.revertDeltas(d); assert.equal(doc.getValue(), ["1234", "5678"].join("\n")); - + doc.applyDeltas(d); assert.equal(doc.getValue(), ["12345678"].join("\n")); }, - + "test: delete multi line range line" : function() { var doc = new Document(["1234", "5678", "abcd"]); - + var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - + doc.remove(new Range(0, 2, 2, 2)); assert.equal(doc.getValue(), ["12cd"].join("\n")); - - var d = deltas.concat(); + + var d = deltas.concat(); doc.revertDeltas(d); assert.equal(doc.getValue(), ["1234", "5678", "abcd"].join("\n")); - + doc.applyDeltas(d); assert.equal(doc.getValue(), ["12cd"].join("\n")); }, - + "test: delete full lines" : function() { var doc = new Document(["1234", "5678", "abcd"]); - + var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - + doc.remove(new Range(1, 0, 3, 0)); assert.equal(doc.getValue(), ["1234", ""].join("\n")); }, - + "test: remove lines should return the removed lines" : function() { var doc = new Document(["1234", "5678", "abcd"]); - + var removed = doc.removeLines(1, 2); assert.equal(removed.join("\n"), ["5678", "abcd"].join("\n")); }, @@ -246,7 +247,7 @@ var Test = { "test: should handle windows style new lines" : function() { var doc = new Document(["1", "2", "3"].join("\r\n")); - + doc.setNewLineMode("unix"); assert.equal(doc.getValue(), ["1", "2", "3"].join("\n")); }, @@ -259,24 +260,86 @@ 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.getValue(), ["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.getValue(), ["1", "2", "3"].join("\n")); var doc = new Document(["1", "2", "3"].join("\r\n")); - + doc.setNewLineMode("auto"); assert.equal(doc.getValue(), ["1", "2", "3"].join("\r\n")); doc.replace(new Range(0, 0, 2, 1), ["4", "5", "6"].join("\n")); assert.equal(["4", "5", "6"].join("\n"), doc.getValue()); + }, + + "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]); + }, + + "test: documentToScreen": function() { + var tabSize = 4; + var wrapLimit = 12; + var doc = new Document(["foo bar foo bar"]); + doc.setUseWrapMode(true); + doc.setWrapLimit(12); + + assert.position(doc.documentToScreenPosition(0, 11), 0, 11); + assert.position(doc.documentToScreenPosition(0, 12), 1, 0); + }, + + "test: screenToDocument": function() { + var tabSize = 4; + var wrapLimit = 12; + var doc = new Document(["foo bar foo bar"]); + doc.setUseWrapMode(true); + doc.setWrapLimit(12); + + assert.position(doc.screenToDocumentPosition(1, 0), 0, 12); + assert.position(doc.screenToDocumentPosition(0, 11), 0, 11); + // Check if the position is clamped the right way. + assert.position(doc.screenToDocumentPosition(0, 12), 0, 11); + assert.position(doc.screenToDocumentPosition(0, 20), 0, 11); } }; @@ -285,4 +348,4 @@ module.exports = require("asyncjs/test").testcase(Test); if (module === require.main) { require("../../../support/paths"); exports.exec() -} \ No newline at end of file +} diff --git a/lib/ace/virtual_renderer.js b/lib/ace/virtual_renderer.js index 47444ee4..ff8ebcd1 100644 --- a/lib/ace/virtual_renderer.js +++ b/lib/ace/virtual_renderer.js @@ -22,6 +22,7 @@ * Contributor(s): * Fabian Jakobs * Irakli Gozalishvili (http://jeditoolkit.com) + * 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 @@ -120,7 +121,6 @@ var VirtualRenderer = function(container, theme) { }; (function() { - this.showGutter = true; this.CHANGE_CURSOR = 1; @@ -138,8 +138,8 @@ var VirtualRenderer = function(container, theme) { this.session = session; this.$cursorLayer.setSession(session); this.$markerLayer.setSession(session); + this.$gutterLayer.setSession(session); this.$textLayer.setSession(session); - this.$loop.schedule(this.CHANGE_FULL); }; @@ -327,14 +327,14 @@ var VirtualRenderer = function(container, theme) { }; this.$updateScrollBar = function() { - this.scrollBar.setInnerHeight(this.session.getLength() * this.lineHeight); + this.scrollBar.setInnerHeight(this.session.getScreenLength() * this.lineHeight); this.scrollBar.setScrollTop(this.scrollTop); }; this.$renderChanges = function(changes) { if (!changes || !this.session || !this.$tokenizer) return; - + // text, scrolling and resize changes can cause the view port size to change if (!this.layerConfig || changes & this.CHANGE_FULL || @@ -352,6 +352,7 @@ var VirtualRenderer = function(container, theme) { this.$markerLayer.update(this.layerConfig); this.$cursorLayer.update(this.layerConfig); this.$updateScrollBar(); + this.scrollCursorIntoView(); return; } @@ -392,20 +393,36 @@ var VirtualRenderer = function(container, theme) { }; this.$computeLayerConfig = function() { + var session = this.session; + var offset = this.scrollTop % this.lineHeight; var minHeight = this.$size.scrollerHeight + this.lineHeight; var longestLine = this.$getLongestLine(); var widthChanged = !this.layerConfig ? true : (this.layerConfig.width != longestLine); - var lineCount = Math.ceil(minHeight / this.lineHeight); + var lineCount = Math.ceil(minHeight / this.lineHeight) - 1; var firstRow = Math.max(0, Math.round((this.scrollTop - offset) / this.lineHeight)); - var lastRow = Math.max(0, Math.min(this.session.getLength(), firstRow + lineCount) - 1); + var lastRow = firstRow + lineCount; + + // Map lines on the screen to lines in the document. + var firstRowScreen, firstRowHeight; + var lineHeight = { lineHeight: this.lineHeight }; + firstRow = session.screenToDocumentRow(firstRow); + firstRowScreen = session.documentToScreenRow(firstRow); + firstRowHeight = session.getRowHeight(lineHeight, firstRow); + + lastRow = Math.min(session.screenToDocumentRow(lastRow), session.getLength() - 1); + minHeight = this.$size.scrollerHeight + session.getRowHeight(lineHeight, lastRow)+ + firstRowHeight; + + offset = this.scrollTop - firstRowScreen * this.lineHeight; var layerConfig = this.layerConfig = { width : longestLine, padding : this.$padding, firstRow : firstRow, + firstRowScreen: firstRowScreen, lastRow : lastRow, lineHeight : this.lineHeight, characterWidth : this.characterWidth, @@ -540,7 +557,7 @@ var VirtualRenderer = function(container, theme) { }; this.scrollToY = function(scrollTop) { - var maxHeight = this.session.getLength() * this.lineHeight - this.$size.scrollerHeight; + var maxHeight = this.session.getScreenLength() * this.lineHeight - this.$size.scrollerHeight; var scrollTop = Math.max(0, Math.min(maxHeight, scrollTop)); if (this.scrollTop !== scrollTop) { @@ -569,17 +586,15 @@ var VirtualRenderer = function(container, theme) { var row = Math.floor((pageY + this.scrollTop - canvasPos.top) / this.lineHeight); - return { - row : row, - column : this.session.screenToDocumentColumn(Math.max(0, Math.min(row, this.session.getLength()-1)), col) - }; + return this.session.screenToDocumentPosition(row, Math.max(col, 0)); }; this.textToScreenCoordinates = function(row, column) { var canvasPos = this.scroller.getBoundingClientRect(); + var pos = this.session.documentToScreenPosition(row, column); - var x = this.$padding + Math.round(this.session.documentToScreenColumn(row, column) * this.characterWidth); - var y = row * this.lineHeight; + var x = this.$padding + Math.round(pos.column * this.characterWidth); + var y = pos.row * this.lineHeight; return { pageX: canvasPos.left + x - this.getScrollLeft(), @@ -604,7 +619,7 @@ var VirtualRenderer = function(container, theme) { } this.$composition.innerHTML = " "; - + var pos = this.$cursorLayer.getPixelPosition(); var style = this.$composition.style; style.top = pos.top + "px"; @@ -624,7 +639,7 @@ var VirtualRenderer = function(container, theme) { if (!this.$composition) return; - + var style = this.$composition.style; style.top = "-10000px"; style.left = "-10000px"; diff --git a/support/paths.js b/support/paths.js index b081ec7a..04902dfb 100644 --- a/support/paths.js +++ b/support/paths.js @@ -1,4 +1,4 @@ require("./requireJS-node"); require.paths.unshift(__dirname + "/../lib"); require.paths.unshift(__dirname + "/pilot/lib"); -require.paths.unshift(__dirname); \ No newline at end of file +require.paths.unshift(__dirname);