diff --git a/demo/demo.js b/demo/demo.js index 9c64de10..67ee5c86 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -147,6 +147,18 @@ exports.launch = function(env) { var container = document.getElementById("editor"); env.editor = new Editor(new Renderer(container, theme)); + // BEGING TESTING + var Range = require("ace/range").Range; + docs.js.addFold(new Range(1,0, 2, 999), "foo..."); + window.s = docs.js; + window.e = env.editor; + setTimeout(function() { + env.editor.selection.addEventListener("changeCursor", function() { + console.log(env.editor.selection.getRange() + ""); + }) + }, 500) + // END TESTING + var modes = { text: new TextMode(), textile: new TextileMode(), diff --git a/lib/ace/edit_session.js b/lib/ace/edit_session.js index 2ba5e9ab..55d48473 100644 --- a/lib/ace/edit_session.js +++ b/lib/ace/edit_session.js @@ -67,6 +67,12 @@ var EditSession = function(text, mode) { this.setMode(mode); else this.setMode(new TextMode()); + + // Set the initial foldData content. + var foldData = this.$foldData = []; + for (var row = 0; row < this.doc.$lines.length; row++) { + foldData.push(false); + } }; @@ -94,8 +100,7 @@ var EditSession = function(text, mode) { this.$informUndoManager.schedule(); } - this.$updateWrapDataOnChange(e); - + this.$updateInternalDataOnChange(e); this.bgTokenizer.start(delta.range.start.row); this._dispatchEvent("change", e); }; @@ -118,7 +123,7 @@ var EditSession = function(text, mode) { this.getState = function(row) { return this.bgTokenizer.getState(row); }; - + this.getTokens = function(firstRow, lastRow) { return this.bgTokenizer.getTokens(firstRow, lastRow); }; @@ -411,7 +416,7 @@ var EditSession = function(text, mode) { } else { this.bgTokenizer.setTokenizer(tokenizer); } - + this.bgTokenizer.setDocument(this.getDocument()); this.bgTokenizer.start(0); @@ -848,11 +853,9 @@ var EditSession = function(text, mode) { }; }; - this.$updateWrapDataOnChange = function(e) { - if (!this.$useWrapMode) { - return; - } - + // TODO: Really want to keep this name? + this.$updateInternalDataOnChange = function(e) { + var useWrapMode = this.$useWrapMode; var len; var action = e.data.action; var firstRow = e.data.range.start.row, @@ -871,20 +874,35 @@ var EditSession = function(text, mode) { if (len != 0) { if (action.indexOf("remove") != -1) { - this.$wrapData.splice(firstRow, len); + useWrapMode && this.$wrapData.splice(firstRow, len); + // TODO: More checking needed here. + this.$foldData.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); + var args; + if (useWrapMode) { + args = [firstRow, 0]; + for (var i = 0; i < len; i++) args.push([]); + this.$wrapData.splice.apply(this.$wrapData, args); + } + + args = [firstRow, 0]; + for (var i = 0; i < len; i++) args.push(false); + // TODO: More checking needed here. + this.$foldData.splice.apply(this.$foldData, args); } } - if (this.$wrapData.length != this.doc.$lines.length) { + if (useWrapMode && this.$wrapData.length != this.doc.$lines.length) { console.error("The length of doc.$lines and $wrapData have to be the same!"); } + if (this.$foldData.length != this.doc.$lines.length) { + console.error("The length of doc.$lines and $foldData have to be the same!"); + } - this.$updateWrapData(firstRow, lastRow); + // TODO: + // this.$updateFoldData(firstRow, lastRow); + useWrapMode && this.$updateWrapData(firstRow, lastRow); }; this.$updateWrapData = function(firstRow, lastRow) { @@ -1312,7 +1330,7 @@ var EditSession = function(text, mode) { } return screenRows; } - + // For every keystroke this gets called once per char in the whole doc!! // Wouldn't hurt to make it a bit faster for c >= 0x1100 function isFullWidth(c) { @@ -1350,8 +1368,84 @@ var EditSession = function(text, mode) { c >= 0xFE68 && c <= 0xFE6B || c >= 0xFF01 && c <= 0xFF60 || c >= 0xFFE0 && c <= 0xFFE6; + }; + + + // == Folding Code ======================================================== + + /** + * Simple fold-data struct. + **/ + function Fold(range, placeholder) { + this.placeholder = placeholder; + this.range = range; + this.start = range.start; + this.end = range.end; } + /** + * Adds a new fold. + */ + this.addFold = function(range, placeholder) { + var startRow = range.start.row, + endRow = range.end.row, + foldData = this.$foldData; + + // In case there is no fold data for the start row yet. + if (!Array.isArray(foldData[startRow])) { + foldData[startRow] = []; + } + + var fold = new Fold(range, placeholder); + foldData[startRow].push(fold); + + // Mark all lines folded by this fold as folded. + for (var row = startRow + 1; row <= endRow; row++) { + foldData[row] = fold; + } + + // TODO: Recalculate wrapData + // TODO: Recalculate width etc. + // TODO: Mark as dirty etc. + + // Notify that fold data has changed. + this._dispatchEvent("changeFold"); + }; + + /** + * Checks if a given documentRow is folded. This is true if there are some + * folded parts such that some parts of the line is still visible. + **/ + this.isRowFolded = function(docRow) { + return this.$foldData[docRow]; + }; + + this.getRowLastFold = function(docRow) { + var fold = this.$foldData[docRow]; + if (!fold) { + return false; + } else if (Array.isArray(fold)) { + return fold[fold.length - 1]; + } else { + return fold; + } + }; + + this.getRowFoldEnd = function(docRow) { + return (this.$foldData[docRow] + ? this.getRowLastFold(docRow).end.row + : docRow); + }; + + /** + * Checks if a given documentRow is visible or not. Not beeing visible means + * it's folded completly. + **/ + this.isRowVisible = function(docRow) { + var fold = this.$foldData[docRow]; + return !fold || Array.isArray(fold); + }; + }).call(EditSession.prototype); exports.EditSession = EditSession; diff --git a/lib/ace/editor.js b/lib/ace/editor.js index 691ca58b..51d86da7 100644 --- a/lib/ace/editor.js +++ b/lib/ace/editor.js @@ -62,7 +62,7 @@ var Editor =function(renderer, session) { this.textInput = new TextInput(renderer.getTextAreaContainer(), this); this.keyBinding = new KeyBinding(this); - + // TODO detect touch event support if (useragent.isIPad) { //this.$mouseHandler = new TouchHandler(this); @@ -125,6 +125,7 @@ var Editor =function(renderer, session) { this.session.removeEventListener("changeTabSize", this.$onChangeTabSize); this.session.removeEventListener("changeWrapLimit", this.$onChangeWrapLimit); this.session.removeEventListener("changeWrapMode", this.$onChangeWrapMode); + this.session.removeEventListener("onChangeFold", this.$onChangeFold); this.session.removeEventListener("changeFrontMarker", this.$onChangeFrontMarker); this.session.removeEventListener("changeBackMarker", this.$onChangeBackMarker); this.session.removeEventListener("changeBreakpoint", this.$onChangeBreakpoint); @@ -159,18 +160,21 @@ var Editor =function(renderer, session) { this.$onChangeWrapMode = this.onChangeWrapMode.bind(this); session.addEventListener("changeWrapMode", this.$onChangeWrapMode); + this.$onChangeFold = this.onChangeFold.bind(this); + session.addEventListener("changeFold", this.$onChangeFold); + this.$onChangeFrontMarker = this.onChangeFrontMarker.bind(this); this.session.addEventListener("changeFrontMarker", this.$onChangeFrontMarker); - + this.$onChangeBackMarker = this.onChangeBackMarker.bind(this); this.session.addEventListener("changeBackMarker", this.$onChangeBackMarker); - + this.$onChangeBreakpoint = this.onChangeBreakpoint.bind(this); this.session.addEventListener("changeBreakpoint", this.$onChangeBreakpoint); this.$onChangeAnnotation = this.onChangeAnnotation.bind(this); this.session.addEventListener("changeAnnotation", this.$onChangeAnnotation); - + this.$onCursorChange = this.onCursorChange.bind(this); this.session.addEventListener("changeOverwrite", this.$onCursorChange); @@ -251,7 +255,7 @@ var Editor =function(renderer, session) { // to be on the save side we do both // except for IE var _self = this; - if (!useragent.isIE) { + if (!useragent.isIE) { setTimeout(function() { _self.textInput.focus(); }); @@ -311,7 +315,7 @@ var Editor =function(renderer, session) { this.$updateHighlightActiveLine = function() { var session = this.getSession(); - + if (session.$highlightLineMarker) { session.removeMarker(session.$highlightLineMarker); } @@ -326,7 +330,7 @@ var Editor =function(renderer, session) { this.onSelectionChange = function(e) { var session = this.getSession(); - + if (session.$selectionMarker) { session.removeMarker(session.$selectionMarker); } @@ -347,11 +351,11 @@ var Editor =function(renderer, session) { this.onChangeFrontMarker = function() { this.renderer.updateFrontMarkers(); }; - + this.onChangeBackMarker = function() { this.renderer.updateBackMarkers(); }; - + this.onChangeBreakpoint = function() { this.renderer.setBreakpoints(this.session.getBreakpoints()); }; @@ -372,6 +376,11 @@ var Editor =function(renderer, session) { this.renderer.onResize(true); }; + this.onChangeFold = function() { + // TODO: This might be too much updating. Okay for now. + this.render.updateFull(); + }; + this.getCopyText = function() { if (!this.selection.isEmpty()) { return this.session.getTextRange(this.getSelectionRange()); @@ -397,7 +406,7 @@ var Editor =function(renderer, session) { var session = this.session; var mode = session.getMode(); - + var cursor = this.getCursorPosition(); text = text.replace("\t", this.session.getTabString()); @@ -420,14 +429,14 @@ var Editor =function(renderer, session) { var lineIndent = mode.getNextLineIndent(lineState, line.slice(0, cursor.column), session.getTabString()); var end = session.insert(cursor, text); - var lineState = session.getState(cursor.row); + // TODO disabled multiline auto indent // possibly doing the indent before inserting the text // if (cursor.row !== end.row) { if (session.getDocument().isNewLine(text)) { this.moveCursorTo(cursor.row+1, 0); - + var size = session.getTabSize(); var minIndent = Number.MAX_VALUE; @@ -462,7 +471,7 @@ var Editor =function(renderer, session) { if (shouldOutdent) { mode.autoOutdent(lineState, session, cursor.row); } - }; + }; } this.onTextInput = function(text) { @@ -591,7 +600,7 @@ var Editor =function(renderer, session) { this.session.remove(this.getSelectionRange()); this.clearSelection(); }; - + this.removeWordRight = function() { if (this.$readOnly) return; @@ -602,7 +611,7 @@ var Editor =function(renderer, session) { this.session.remove(this.getSelectionRange()); this.clearSelection(); }; - + this.removeWordLeft = function() { if (this.$readOnly) return; @@ -613,7 +622,7 @@ var Editor =function(renderer, session) { this.session.remove(this.getSelectionRange()); this.clearSelection(); }; - + this.removeToLineStart = function() { if (this.$readOnly) return; @@ -624,7 +633,7 @@ var Editor =function(renderer, session) { this.session.remove(this.getSelectionRange()); this.clearSelection(); }; - + this.removeToLineEnd = function() { if (this.$readOnly) return; @@ -639,30 +648,30 @@ var Editor =function(renderer, session) { this.splitLine = function() { if (this.$readOnly) return; - + if (!this.selection.isEmpty()) { this.session.remove(this.getSelectionRange()); this.clearSelection(); } - + var cursor = this.getCursorPosition(); this.insert("\n"); this.moveCursorToPosition(cursor); }; - + this.transposeLetters = function() { if (this.$readOnly) return; - + if (!this.selection.isEmpty()) { return; } - + var cursor = this.getCursorPosition(); var column = cursor.column; if (column == 0) return; - + var line = this.session.getLine(cursor.row); if (column < line.length) { var swap = line.charAt(column) + line.charAt(column-1); @@ -674,7 +683,7 @@ var Editor =function(renderer, session) { } this.session.replace(range, swap); }; - + this.indent = function() { if (this.$readOnly) return; @@ -887,7 +896,7 @@ var Editor =function(renderer, session) { this.scrollToLine = function(line, center) { this.renderer.scrollToLine(line, center); }; - + this.centerSelection = function() { var range = this.getSelectionRange(); var line = Math.floor(range.start.row + (range.end.row - range.start.row) / 2); @@ -912,7 +921,7 @@ var Editor =function(renderer, session) { this.selection.selectAll(); this.$blockScrolling -= 1; }; - + this.clearSelection = function() { this.selection.clearSelection(); }; @@ -1039,7 +1048,7 @@ var Editor =function(renderer, session) { this.$blockScrolling += 1; for (var i = ranges.length - 1; i >= 0; --i) this.$tryReplace(ranges[i], replacement); - + this.selection.setSelectionRange(selection); this.$blockScrolling -= 1; }, diff --git a/lib/ace/layer/gutter.js b/lib/ace/layer/gutter.js index b4354b5b..e5fb1c9c 100644 --- a/lib/ace/layer/gutter.js +++ b/lib/ace/layer/gutter.js @@ -73,12 +73,12 @@ var Gutter = function(parentEl) { this.setAnnotations = function(annotations) { // iterate over sparse array - this.$annotations = []; + this.$annotations = []; for (var row in annotations) if (annotations.hasOwnProperty(row)) { var rowAnnotations = annotations[row]; if (!rowAnnotations) continue; - + var rowInfo = this.$annotations[row] = { text: [] }; @@ -111,6 +111,9 @@ var Gutter = function(parentEl) { annotation.className, "' title='", annotation.text.join("\n"), "' style='height:", this.session.getRowHeight(config, i), "px;'>", (i+1), ""); + + + i = this.session.getRowFoldEnd(i); } this.element = dom.setInnerHtml(this.element, html.join("")); this.element.style.height = config.minHeight + "px"; diff --git a/lib/ace/layer/text.js b/lib/ace/layer/text.js index a4fdee3d..50bf0f5c 100644 --- a/lib/ace/layer/text.js +++ b/lib/ace/layer/text.js @@ -186,18 +186,30 @@ var Text = function(parentEl) { var first = Math.max(firstRow, config.firstRow); var last = Math.min(lastRow, config.lastRow); - var lineElements = this.element.childNodes; - var tokens = this.session.getTokens(first, last); + var lineElements = this.element.childNodes, + lineElementsIdx = 0; + + for (var row = config.firstRow; row < first; row++) { + lineElementsIdx ++; + if (this.session.isRowFolded(row)) { + var fold = this.session.getRowLastFold(row); + row = fold.end.row; + } + } + for (var i=first; i<=last; i++) { - var lineElement = lineElements[i - config.firstRow]; + var lineElement = lineElements[lineElementsIdx++]; if (!lineElement) continue; var html = []; - this.$renderLine(html, i, tokens[i-first].tokens); + var tokens = this.session.getTokens(i, i); + this.$renderLine(html, i, tokens[0].tokens); lineElement = dom.setInnerHtml(lineElement, html.join("")); lineElement.style.height = this.session.getRowHeight(config, i) + "px"; + + i = this.session.getRowFoldEnd(i); } }; @@ -238,17 +250,23 @@ var Text = function(parentEl) { this.$renderLinesFragment = function(config, firstRow, lastRow) { var fragment = document.createDocumentFragment(); - var tokens = this.session.getTokens(firstRow, lastRow); for (var row=firstRow; row<=lastRow; row++) { var lineEl = dom.createElement("div"); + lineEl.className = "ace_line"; var style = lineEl.style; style.height = this.session.getRowHeight(config, row) + "px"; style.width = config.width + "px"; var html = []; - if (tokens.length > row-firstRow) - this.$renderLine(html, row, tokens[row-firstRow].tokens); + // Get the tokens per line as there might be some lines in between + // beeing folded. + // OPTIMIZE: If there is a long block of unfolded lines, just make + // this call once for that big block of unfolded lines. + var tokens = this.session.getTokens(row, row); + if (tokens.length == 1) + this.$renderLine(html, row, tokens[0].tokens); + // don't use setInnerHtml since we are working with an empty DIV lineEl.innerHTML = html.join(""); fragment.appendChild(lineEl); @@ -276,6 +294,13 @@ var Text = function(parentEl) { }; this.$renderLine = function(stringBuilder, row, tokens) { + // Nothing to do if the entire line is folded. + // TODO: Remove this, once the folding feature is done. Only for + // developing stuff at the moment. + if (!this.session.isRowVisible(row)) { + throw "Calling renderLine on folded line doesn't make sense?"; + } + var _self = this, characterWidth = this.config.characterWidth, screenColumn = 0; diff --git a/lib/ace/virtual_renderer.js b/lib/ace/virtual_renderer.js index 6ab0f8f1..d7f66b2d 100644 --- a/lib/ace/virtual_renderer.js +++ b/lib/ace/virtual_renderer.js @@ -275,7 +275,7 @@ var VirtualRenderer = function(container, theme) { this.getPrintMarginColumn = function() { return this.$printMarginColumn; }; - + this.getShowGutter = function(){ return this.showGutter; } @@ -590,7 +590,7 @@ var VirtualRenderer = function(container, theme) { // the editor is not visible if (this.$size.scrollerHeight === 0) return; - + var pos = this.$cursorLayer.getPixelPosition(); var left = pos.left + this.$padding; @@ -644,7 +644,7 @@ var VirtualRenderer = function(container, theme) { for (var l = 1; l < line; l++) { offset += this.session.getRowHeight(lineHeight, l-1); } - + if (center) { offset -= this.$size.scrollerHeight / 2; }