diff --git a/demo/demo_startup.js b/demo/demo_startup.js index 6ffd8771..dcc9f42a 100644 --- a/demo/demo_startup.js +++ b/demo/demo_startup.js @@ -58,6 +58,7 @@ exports.launch = function(env) { var docs = {}; docs.js = new Document(document.getElementById("jstext").innerHTML); + docs.js.setUseWrapMode(true); docs.js.setMode(new JavaScriptMode()); docs.js.setUndoManager(new UndoManager()); diff --git a/lib/ace/document.js b/lib/ace/document.js index e60dbbd7..67b16daa 100644 --- a/lib/ace/document.js +++ b/lib/ace/document.js @@ -45,17 +45,18 @@ var TextMode = require("ace/mode/text").Mode; var Range = require("ace/range").Range; var Document = function(text, mode) { - + this.modified = true; this.lines = []; this.selection = new Selection(this); this.$breakpoints = []; + this.$wrapData = []; this.listeners = []; if (mode) { this.setMode(mode); } - + if (Array.isArray(text)) { this.$insertLines(0, text); } else { @@ -159,7 +160,7 @@ var Document = function(text, mode) { this.getTabSize = function() { return this.$tabSize; }; - + this.isTabStop = function(position) { return this.$useSoftTabs && (position.column % this.$tabSize == 0); }; @@ -199,13 +200,13 @@ var Document = function(text, mode) { this.$autoNewLine = "\n"; } }; - + this.tokenRe = /^[\w\d]+/g; this.nonTokenRe = /^[^\w\d]+/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); @@ -321,12 +322,12 @@ var Document = function(text, mode) { }; /** - * 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] || ""; }; - + /** * Get a line as it is displayed on screen. Tabs are replaced by spaces. */ @@ -455,18 +456,18 @@ var Document = function(text, mode) { : undefined); return end; }; - + /** * @param rows Array[Integer] sorted list of rows */ this.multiRowInsert = function(rows, column, text) { var lines = this.lines; - + for (var i=rows.length-1; i>=0; i--) { var row = rows[i]; if (row >= lines.length) continue; - + var diff = column - lines[row].length; if ( diff > 0) { var padded = lang.stringRepeat(" ", diff) + text; @@ -476,10 +477,10 @@ var Document = function(text, mode) { padded = text; offset = 0; } - + var end = this.$insert({row: row, column: column+offset}, padded, false); } - + if (end) { this.fireChangeEvent(rows[0], rows[rows.length-1] + end.row - rows[0]); return { @@ -593,16 +594,16 @@ var Document = 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.lines.length) continue; - + var end = this.$remove(new Range(row, range.start.column, row+height, range.end.column), false); } - + if (end) { if (height < 0) this.fireChangeEvent(rows[0]+height, undefined); @@ -610,7 +611,7 @@ var Document = function(text, mode) { this.fireChangeEvent(rows[0], height == 0 ? rows[rows.length-1] : undefined); } }; - + this.$remove = function(range, fromUndo) { if (range.isEmpty()) return; @@ -639,7 +640,7 @@ var Document = function(text, mode) { this.lines.splice(firstRow, lastRow - firstRow + 1, ""); return range.start; }; - + this.undoChanges = function(deltas) { this.selection.clearSelection(); for (var i=deltas.length-1; i>=0; i--) { @@ -685,7 +686,7 @@ var Document = function(text, mode) { return end; }; - this.indentRows = function(startRow, endRow, indentString) { + this.indentRows = function(startRow, endRow, indentString) { indentString = indentString.replace("\t", this.getTabString()); for (var row=startRow; row<=endRow; row++) { this.$insert({row: row, column:0}, indentString); @@ -698,15 +699,15 @@ var Document = 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; @@ -722,7 +723,7 @@ var Document = function(text, mode) { } this.fireChangeEvent(range.start.row, range.end.row); return range; - } + } this.moveLinesUp = function(firstRow, lastRow) { if (firstRow <= 0) return 0; @@ -730,7 +731,7 @@ var Document = function(text, mode) { var removed = this.lines.slice(firstRow, lastRow + 1); this.$remove(new Range(firstRow-1, this.lines[firstRow-1].length, lastRow, this.lines[lastRow].length)); this.$insertLines(firstRow - 1, removed); - + this.fireChangeEvent(firstRow - 1, lastRow); return -1; }; @@ -741,7 +742,7 @@ var Document = function(text, mode) { var removed = this.lines.slice(firstRow, lastRow + 1); this.$remove(new Range(firstRow, 0, lastRow + 1, 0)); this.$insertLines(firstRow+1, removed); - + this.fireChangeEvent(firstRow, lastRow + 1); return 1; }; @@ -763,26 +764,72 @@ var Document = function(text, mode) { return Math.max(0, Math.min(row, this.lines.length-1)); }; - this.documentToScreenColumn = function(row, docColumn) { - var tabSize = this.getTabSize(); +}).call(Document.prototype); - var screenColumn = 0; - var remaining = docColumn; - var line = this.getLine(row).split("\t"); - for (var i=0; i len) { - remaining -= (len + 1); - screenColumn += len + tabSize; +(function() { + this.$wrapLimit = 12; + this.$useWrapMode = false; + this.setUseWrapMode = function(useWrapMode) { + var _self = this; + function computeWrapData(e) { + var lines = _self.lines, wrapData = _self.$wrapData; + var wrapLimit = _self.$wrapLimit; + + if (!e.data.lastRow) { + e.data.lastRow = _self.lines.length - 1; } - else { - screenColumn += remaining; - break; + + 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); + } else { + this.removeEventListener("change", computeWrapData); + } + }; + + this.getUseWrapMode = function() { + return this.$useWrapMode; + }; + + this.setWrapLimit = function(wrapLimit) { + this.$wrapLimit = wrapLimit; + }; + + this.getWrapLimit = function() { + return this.$wrapLimit; + }; + + this.getRowHeight = function(config, row) { + var rows; + if (!this.$useWrapMode) { + rows = 1; + } else { + rows = this.$wrapData[row].length + 1; } - return screenColumn; + return rows * config.lineHeight; + }; + + this.getRowSplitData = function(row) { + if (!this.$useWrapMode) { + return undefined; + } else { + return this.$wrapData[row]; + } }; this.screenToDocumentColumn = function(row, screenColumn) { @@ -810,7 +857,96 @@ var Document = function(text, mode) { return docColumn; }; -}).call(Document.prototype); + this.screenToDocumentPosition = function(row, column) { + if (!this.$useWrapMode) { + return { + row: row, + column: this.screenToDocumentColumn(row, column) + } + } + + var wrapData = this.$wrapData, linesCount = this.lines.length; + + var docRow = 0; + while (docRow < linesCount && row >= wrapData[docRow].length + 1) { + row -= wrapData[docRow].length + 1; + docRow ++; + } + var docColumn = column + + (docRow < linesCount ? wrapData[docRow][row - 1] || 0 : 0); + + return { + row: docRow, + column: docColumn + }; + }; + + this.documentToScreenColumn = function(row, docColumn) { + var tabSize = this.getTabSize(); + + var screenColumn = 0; + var remaining = docColumn; + + var line = this.getLine(row).split("\t"); + for (var i=0; i len) { + remaining -= (len + 1); + screenColumn += len + tabSize; + } + else { + screenColumn += remaining; + break; + } + } + + return screenColumn; + }; + + this.documentToScreenPosition = function(row, column) { + if (!this.$useWrapMode) { + return { + row: row, + column: this.documentToScreenColumn(row, column) + } + } + + 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 { + row: screenRow, + column: 0 + } + } + + for (var i = 0; i < row; i++) { + screenRow += wrapData[i].length + 1; + } + + var screenColumn = column; + var wrapRowData = wrapData[row]; + for (var split = 0; split < wrapRowData.length; split++) { + if (column > wrapRowData[split]) { + screenColumn = column - wrapRowData[split]; + screenRow ++; + } else { + break; + } + } + + return { + row: screenRow, + column: screenColumn + }; + }; + +}).call(Document.prototype) exports.Document = Document; }); diff --git a/lib/ace/layer/cursor.js b/lib/ace/layer/cursor.js index be444747..8fcbb1c1 100644 --- a/lib/ace/layer/cursor.js +++ b/lib/ace/layer/cursor.js @@ -107,8 +107,16 @@ var Cursor = function(parentEl) { top : 0 }; } - - return this.config.getPixelPosition(this.position.row, this.position.column); + + var pos = this.doc.documentToScreenPosition(this.position.row, + this.position.column); + var cursorLeft = Math.round(pos.column * this.config.characterWidth); + var cursorTop = pos.row * this.config.lineHeight; + + return { + left : cursorLeft, + top : cursorTop + }; }; this.update = function(config) { diff --git a/lib/ace/layer/gutter.js b/lib/ace/layer/gutter.js index e4591e01..bd182ddb 100644 --- a/lib/ace/layer/gutter.js +++ b/lib/ace/layer/gutter.js @@ -48,14 +48,18 @@ var Gutter = function(parentEl) { (function() { + this.setDocument = function(doc) { + this.doc = doc; + }; + 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] = this.$decorations[row].replace(" ace_" + className, ""); } @@ -71,7 +75,7 @@ var Gutter = function(parentEl) { html.push("
", (i+1), "
"); + "' style='height:", this.doc.getRowHeight(config, i) + "px'>", (i+1), ""); html.push(""); } diff --git a/lib/ace/layer/marker.js b/lib/ace/layer/marker.js index be12014f..517f1775 100644 --- a/lib/ace/layer/marker.js +++ b/lib/ace/layer/marker.js @@ -85,10 +85,8 @@ var Marker = function(parentEl) { var range = marker.range.clipRows(config.firstRow, config.lastRow); if (range.isEmpty()) continue; - - // TODO: Add this conversion to the range object directly! - range.start = this.config.posToWrappedPos(range.start.row, range.start.column); - range.end = this.config.posToWrappedPos(range.end.row, range.end.column); + + range = range.toScreenRange(this.doc); if (range.isMultiLine()) { if (marker.type == "text") { @@ -105,7 +103,6 @@ var Marker = function(parentEl) { }; this.drawTextMarker = function(stringBuilder, range, clazz, layerConfig) { - // selection start var row = range.start.row; var lineRange = new Range(row, range.start.column, row, this.doc.getLine(row).length); @@ -125,9 +122,6 @@ var Marker = function(parentEl) { }; this.drawMultiLineMarker = function(stringBuilder, range, clazz, layerConfig) { - // TODO: Add this back. - // var range = range.toScreenRange(this.doc); - // from selection start to the end of the line var height = layerConfig.lineHeight; var width = Math.round(layerConfig.width - (range.start.column * layerConfig.characterWidth)); @@ -168,9 +162,6 @@ var Marker = function(parentEl) { }; this.drawSingleLineMarker = function(stringBuilder, range, clazz, layerConfig) { - // TODO: Add this back. - //var range = range.toScreenRange(this.doc); - var height = layerConfig.lineHeight; var width = Math.round((range.end.column - range.start.column) * layerConfig.characterWidth); var top = (range.start.row - layerConfig.firstRow) * layerConfig.lineHeight; diff --git a/lib/ace/layer/text.js b/lib/ace/layer/text.js index 371cd7e6..5c26a4f2 100644 --- a/lib/ace/layer/text.js +++ b/lib/ace/layer/text.js @@ -148,18 +148,19 @@ var Text = function(parentEl) { } }; - this.updateLines = function(layerConfig, firstRow, lastRow) { + this.updateLines = function(config, firstRow, lastRow) { + console.log("layer.text.updateLines", firstRow, lastRow); this.$computeTabString(); - this.config = layerConfig; + this.config = config; - var first = Math.max(firstRow, layerConfig.firstRow); - var last = Math.min(lastRow, layerConfig.lastRow); + var first = Math.max(firstRow, config.firstRow); + var last = Math.min(lastRow, config.lastRow); var lineElements = this.element.childNodes; var _self = this; this.tokenizer.getTokens(first, last, function(tokens) { for ( var i = first; i <= last; i++) { - var lineElement = lineElements[i - layerConfig.firstRow]; + var lineElement = lineElements[i - config.firstRow]; if (!lineElement) continue; @@ -168,7 +169,8 @@ var Text = function(parentEl) { lineElement.innerHTML = html.join(""); // The height of the line might have changed if wrapped mode // is active. - lineElement.style.height = (layerConfig.wrapped[i].length + 1) * layerConfig.lineHeight + "px"; + lineElement.style.height = + _self.doc.getRowHeight(config, i) + "px"; } }); }; @@ -229,7 +231,7 @@ var Text = function(parentEl) { var lineEl = document.createElement("div"); lineEl.className = "ace_line"; var style = lineEl.style; - style.height = (config.wrapped[row].length + 1) * config.lineHeight + "px"; + style.height = _self.doc.getRowHeight(config, row) + "px"; style.width = config.width + "px"; var html = []; @@ -242,6 +244,7 @@ var Text = function(parentEl) { }; this.update = function(config) { + console.log("layer.text.update()"); this.$computeTabString(); this.config = config; @@ -270,8 +273,6 @@ var Text = function(parentEl) { }; this.$renderLine = function(stringBuilder, row, tokens) { - stringBuilder.push("
"); - var wrappedInfo = this.config.wrapped[row]; // if (this.$showInvisibles) { // var self = this; // var spaceRe = /[\v\f \u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000]+/g; @@ -302,25 +303,31 @@ var Text = function(parentEl) { } } - var chars = 0; - var wrapSection = 0; - var maxChars = wrappedInfo[wrapSection] || 9999; - var value; + var splits = this.doc.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 + token.value.length < maxChars) { - addToken(token, token.value); - chars += token.value.length; + if (chars + value.length < splitChars) { + addToken(token, value); + chars += value.length; } else { - value = token.value; - while (chars + value.length >= maxChars) { - addToken(token, value.substring(0, maxChars - chars)); - value = value.substring(maxChars - chars); - chars = maxChars; + while (chars + value.length >= splitChars) { + addToken(token, value.substring(0, splitChars - chars)); + value = value.substring(splitChars - chars); + chars = splitChars; stringBuilder.push("
"); - wrapSection ++; - maxChars = wrappedInfo[wrapSection] || 9999; + split ++; + splitChars = splits[split] || Number.MAX_VALUE; } if (value.length != 0) { chars += value.length; diff --git a/lib/ace/range.js b/lib/ace/range.js index 65775cb0..622ff59f 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) @@ -146,9 +146,13 @@ var Range = function(startRow, startColumn, endRow, endColumn) { }; this.toScreenRange = function(doc) { + var screenPosStart = + doc.documentToScreenPosition(this.start.row, this.start.column); + var screenPosEnd = + doc.documentToScreenPosition(this.end.row, this.end.column); 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/virtual_renderer.js b/lib/ace/virtual_renderer.js index f3552a21..ab66a443 100644 --- a/lib/ace/virtual_renderer.js +++ b/lib/ace/virtual_renderer.js @@ -82,7 +82,7 @@ var VirtualRenderer = function(container, theme) { this.$cursorLayer = new CursorLayer(this.content); this.layers = [ this.$markerLayer, textLayer, this.$cursorLayer ]; - + this.scrollBar = new ScrollBar(container); this.scrollBar.addEventListener("scroll", this.onScroll.bind(this)); @@ -118,10 +118,6 @@ var VirtualRenderer = function(container, theme) { }; (function() { - this.layerConfig = { - wrapped: [] - }; - this.showGutter = true; this.CHANGE_CURSOR = 1; @@ -140,30 +136,16 @@ var VirtualRenderer = function(container, theme) { this.doc = doc; this.$cursorLayer.setDocument(doc); this.$markerLayer.setDocument(doc); + this.$gutterLayer.setDocument(doc); this.$textLayer.setDocument(doc); this.$loop.schedule(this.CHANGE_FULL); }; - this.$updateWrappedLinesInfo = function(firstRow, lastRow) { - var WRAPSIZE = 12; - var wrappedInfo = this.layerConfig.wrapped; - var lines = this.lines; - for (var row = firstRow; row <= lastRow; row++) { - var col = 12; - wrappedInfo[row] = []; - while (col < lines[row].length) { - wrappedInfo[row].push(col); - col += 12; - } - } - }; - /** * Triggers partial update of the text layer */ this.updateLines = function(firstRow, lastRow) { - this.$updateWrappedLinesInfo(firstRow, lastRow); console.log("updateLines", firstRow, lastRow); if (lastRow === undefined) lastRow = Infinity; @@ -351,7 +333,7 @@ var VirtualRenderer = function(container, theme) { this.$renderChanges = function(changes) { if (!changes || !this.doc || !this.$tokenizer) return; - + // text, scrolling and resize changes can cause the view port size to change if (!this.layerConfig || changes & this.CHANGE_FULL || @@ -419,7 +401,7 @@ var VirtualRenderer = function(container, theme) { var firstRow = Math.max(0, Math.round((this.scrollTop - offset) / this.lineHeight)); var lastRow = Math.max(0, Math.min(this.lines.length, firstRow + lineCount) - 1); - var layerConfig = oop.mixin(this.layerConfig, { + var layerConfig = this.layerConfig = { width : longestLine, padding : this.$padding, firstRow : firstRow, @@ -429,11 +411,7 @@ var VirtualRenderer = function(container, theme) { minHeight : minHeight, offset : offset, height : this.$size.scrollerHeight - }); - - // Ensure that there is a wrapped array for all the rows in the current - // view port. - this.$updateWrappedLinesInfo(firstRow, lastRow); + }; for ( var i = 0; i < this.layers.length; i++) { var layer = this.layers[i]; @@ -593,84 +571,22 @@ var VirtualRenderer = function(container, theme) { var row = Math.floor((pageY + this.scrollTop - canvasPos.top) / this.lineHeight); - return this.layerConfig.wrappedPosToPos( - row, - col - // TODO: Figure out how to calculate tabs here... - //this.doc.screenToDocumentColumn(Math.max(0, Math.min(row, this.doc.getLength()-1)), col) - ); + return this.doc.screenToDocumentPosition(row, col); }; this.textToScreenCoordinates = function(row, column) { var canvasPos = this.scroller.getBoundingClientRect(); + var pos = this.doc.documentToScreenPosition(row, column); - var x = this.padding + Math.round(this.doc.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(), pageY: canvasPos.top + y - this.getScrollTop() } }; - - this.wrappedPosToPos = function(row, column) { - var linesCount = this.wrapped.length; - var realRow = 0; - while (realRow < linesCount && row >= this.wrapped[realRow].length + 1) { - row -= this.wrapped[realRow].length + 1; - realRow ++; - } - return { - row: realRow, - column: column + (realRow < linesCount ? this.wrapped[realRow][row - 1] || 0 : 0) - }; - }; - - this.posToWrappedPos = function(row, column) { - // TODO: Why can it happen, that row is higher then the current count - // of lines (note lines in doc, not only in wrapped!). Happens when - // the cursor is in the last line and the marker "ace_active_line" is - // painted. - if (row > this.wrapped.length - 1) { - row = this.wrapped.length - 1; - column = 99999; - } - - var rows = 0; - for (var i = 0; i < row; i++) { - rows += this.wrapped[i].length + 1; - } - - var col = column; - for (var s = 0; s < this.wrapped[row].length; s++) { - if (column > this.wrapped[row][s]) { - col = column - this.wrapped[row][s]; - rows ++; - } else { - break; - } - } - return { - row: rows, - column: col - }; - }; - - this.getPixelPosition = function(row, column) { - var pos = this.posToWrappedPos(row, column); - var cursorLeft = Math.round(pos.column * this.characterWidth); - var cursorTop = pos.row * this.lineHeight; - - return { - left : cursorLeft, - top : cursorTop - }; - }; - - // TODO: This should get passed in a different way to the cursorLayer! - this.layerConfig.getPixelPosition = this.getPixelPosition; - this.layerConfig.posToWrappedPos = this.posToWrappedPos; - this.layerConfig.wrappedPosToPos = this.wrappedPosToPos; this.visualizeFocus = function() { dom.addCssClass(this.container, "ace_focus");