From 28ea247f17caab7de15fe9a2703ded1e5525d2bb Mon Sep 17 00:00:00 2001 From: Julian Viereck Date: Tue, 11 Jan 2011 15:47:55 +0100 Subject: [PATCH] Fix move to first/last character in line if in wrap mode. Fixes some other bugs on the way and add some simple unit tests. --- demo/demo_startup.js | 3 +- lib/ace/document.js | 154 ++++++++++++++++++++++++++-------- lib/ace/layer/cursor.js | 2 +- lib/ace/layer/marker.js | 4 +- lib/ace/range.js | 4 +- lib/ace/selection.js | 28 ++++--- lib/ace/test/document_test.js | 25 ++++++ lib/ace/virtual_renderer.js | 2 +- 8 files changed, 171 insertions(+), 51 deletions(-) diff --git a/demo/demo_startup.js b/demo/demo_startup.js index c754b1ad..6adaa88b 100644 --- a/demo/demo_startup.js +++ b/demo/demo_startup.js @@ -91,6 +91,7 @@ exports.launch = function(env) { var container = document.getElementById("editor"); env.editor = new Editor(new Renderer(container, theme)); + window.$editor = env.editor; function onDocChange() { var doc = getDoc(); @@ -124,7 +125,7 @@ exports.launch = function(env) { docEl.onchange = onDocChange; function getDoc() { - return docs[docEl.value]; + return docs.plain//docs[docEl.value]; } var modeEl = document.getElementById("mode"); diff --git a/lib/ace/document.js b/lib/ace/document.js index c4ca4290..40a4ed38 100644 --- a/lib/ace/document.js +++ b/lib/ace/document.js @@ -800,9 +800,12 @@ var Document = function(text, mode) { this.$useWrapMode = false; this.setUseWrapMode = function(useWrapMode) { - this.$useWrapMode = useWrapMode; - this.$updateWrapData(0, this.lines.length - 1); - this._dispatchEvent("changeWrapMode"); + if (useWrapMode != this.$useWrapMode) { + this.$useWrapMode = useWrapMode; + this.$updateWrapData(0, this.lines.length - 1); + this._dispatchEvent("changeWrapMode"); + this.fireChangeEvent(0); + } }; this.getUseWrapMode = function() { @@ -810,7 +813,10 @@ var Document = function(text, mode) { }; this.setWrapLimit = function(wrapLimit) { - this.$wrapLimit = wrapLimit; + if (wrapLimit != this.$wrapLimit) { + this.$wrapLimit = wrapLimit; + this.$updateWrapData(0, this.lines.length - 1); + } }; this.getWrapLimit = function() { @@ -1008,21 +1014,67 @@ var Document = function(text, mode) { return rows * config.lineHeight; }; - this.getScreenRowLength = function(screenRow) { + + /** + * 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 len = str.length; + str.replace("\t", function(m) { + len += tabSize-1; + return m; + }); + return len; + } + + this.getScreenLastRowColumn = function(screenRow, returnDocPosition) { if (!this.$useWrapMode) { - return this.getLine(screenRow).length; + 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]) { - return this.$wrapData[docRow][row] - (this.$wrapData[docRow][row - 1] || 0); + start = (this.$wrapData[docRow][row - 1] || 0); + end = this.$wrapData[docRow][row]; + returnDocPosition && end--; } else { - return this.lines[docRow].length - - (this.$wrapData[docRow][row - 1] || 0) ; + end = this.lines[docRow].length; + start = (this.$wrapData[docRow][row - 1] || 0); } + if (!returnDocPosition) { + return this.$getStringScreenWidth(this.getLine(docRow).substring(start, end)); + } else { + return end; + } + }; + + 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) { @@ -1033,6 +1085,12 @@ var Document = function(text, mode) { } }; + /** + * + * @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]; @@ -1063,13 +1121,14 @@ var Document = function(text, mode) { 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.lines.length; var rowData = this.$screenToDocumentRow(row); - var row = rowData[1]; + row = rowData[1]; docRow = rowData[0]; if (docRow >= this.lines.length) { @@ -1101,7 +1160,12 @@ var Document = function(text, mode) { // Clamp docColumn. if (docRow < linesCount && wrapData[docRow][row]) { - docColumn = Math.min(docColumn, 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.lines[docRow]) { docColumn = Math.min(docColumn, this.lines[docRow].length); } @@ -1116,33 +1180,57 @@ var Document = function(text, mode) { return this.documentToScreenPosition(row, docColumn).column; }; - this.documentToScreenRow = function(row) { + /** + * + * @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 row; + return [docRow, 0]; } var wrapData = this.$wrapData; var screenRow = 0; // Handle special case where the row is outside of the range of lines. - if (row > wrapData.length - 1) { - for (row = 0; row < wrapData.length; row ++) { - screenRow += wrapData[row].length + 1; - } - return screenRow; + if (docRow > wrapData.length - 1) { + return [this.getScreenLength(), + this.wrapData[this.wrapData.length - 1].length - 1]; } - for (var i = 0; i < row; i++) { + for (var i = 0; i < docRow; i++) { screenRow += wrapData[i].length + 1; } - return screenRow; + var screenRowOffset = 0; + while (docColumn >= wrapData[docRow][screenRowOffset]) { + screenRow ++; + screenRowOffset++; + } + + return [screenRow, screenRowOffset]; } - this.documentToScreenPosition = function(row, column) { + this.documentToScreenRow = function(docRow, docColumn) { + return this.$documentToScreenRow(docRow, docColumn)[0]; + } + + this.documentToScreenPosition = function(pos, column) { var str; var tabSize = this.getTabSize(); + // Normalize the passed in arguments. + var row; + if (column != null) { + row = pos; + } else { + row = pos.row; + column = pos.column; + } + if (!this.$useWrapMode) { str = this.getLine(row).substring(0, column); column += (str.split("\t").length - 1) * (tabSize - 1); @@ -1151,26 +1239,24 @@ var Document = function(text, mode) { column: column }; } - var screenRow = this.documentToScreenRow(row); if (row >= this.lines.length) { return { row: screenRow, column: 0 }; } - var screenColumn = column; - var wrapRowData = this.$wrapData[row]; - var split; - for (split = 0; wrapRowData && split < wrapRowData.length; split++) { - if (column > wrapRowData[split]) { - screenColumn = column - wrapRowData[split]; - screenRow ++; - } else { - break; - } - } - str = this.getLine(row).substring(wrapRowData[split - 1] || 0, column); + 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 { diff --git a/lib/ace/layer/cursor.js b/lib/ace/layer/cursor.js index 9791ffb8..4276ccb2 100644 --- a/lib/ace/layer/cursor.js +++ b/lib/ace/layer/cursor.js @@ -59,7 +59,7 @@ var Cursor = function(parentEl) { this.setCursor = function(position, overwrite) { this.position = - this.doc.documentToScreenPosition(position.row, position.column); + this.doc.documentToScreenPosition(position); if (overwrite) { dom.addCssClass(this.cursor, "ace_overwrite"); } else { diff --git a/lib/ace/layer/marker.js b/lib/ace/layer/marker.js index 0182f71e..5dbd2ef9 100644 --- a/lib/ace/layer/marker.js +++ b/lib/ace/layer/marker.js @@ -111,7 +111,7 @@ var Marker = function(parentEl) { // selection start var row = range.start.row; var lineRange = new Range(row, range.start.column, - row, this.doc.getScreenRowLength(row)); + row, this.doc.getScreenLastRowColumn(row)); this.drawSingleLineMarker(stringBuilder, lineRange, clazz, layerConfig); // selection end @@ -122,7 +122,7 @@ 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.doc.getScreenRowLength(row); + lineRange.end.column = this.doc.getScreenLastRowColumn(row); this.drawSingleLineMarker(stringBuilder, lineRange, clazz, layerConfig); } }; diff --git a/lib/ace/range.js b/lib/ace/range.js index 622ff59f..a59e2976 100644 --- a/lib/ace/range.js +++ b/lib/ace/range.js @@ -147,9 +147,9 @@ var Range = function(startRow, startColumn, endRow, endColumn) { this.toScreenRange = function(doc) { var screenPosStart = - doc.documentToScreenPosition(this.start.row, this.start.column); + doc.documentToScreenPosition(this.start); var screenPosEnd = - doc.documentToScreenPosition(this.end.row, this.end.column); + doc.documentToScreenPosition(this.end); return new Range( screenPosStart.row, screenPosStart.column, screenPosEnd.row, screenPosEnd.column diff --git a/lib/ace/selection.js b/lib/ace/selection.js index b630d763..e33cd085 100644 --- a/lib/ace/selection.js +++ b/lib/ace/selection.js @@ -170,7 +170,7 @@ var Selection = function(doc) { this.$updateDesiredColumn = function() { var cursor = this.getCursor(); if (cursor) { - this.$desiredColumn = this.doc.documentToScreenPosition(cursor.row, cursor.column).column; + this.$desiredColumn = this.doc.documentToScreenColumn(cursor.row, cursor.column); } }; @@ -311,19 +311,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() { diff --git a/lib/ace/test/document_test.js b/lib/ace/test/document_test.js index 9c64cdb8..ae383caf 100644 --- a/lib/ace/test/document_test.js +++ b/lib/ace/test/document_test.js @@ -321,6 +321,31 @@ var Test = { // 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); } }; diff --git a/lib/ace/virtual_renderer.js b/lib/ace/virtual_renderer.js index e63ef75e..385a7d61 100644 --- a/lib/ace/virtual_renderer.js +++ b/lib/ace/virtual_renderer.js @@ -585,7 +585,7 @@ var VirtualRenderer = function(container, theme) { var row = Math.floor((pageY + this.scrollTop - canvasPos.top) / this.lineHeight); - return this.doc.screenToDocumentPosition(row, col); + return this.doc.screenToDocumentPosition(row, Math.max(col, 0)); }; this.textToScreenCoordinates = function(row, column) {