/* vim:ts=4:sts=4:sw=4: * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Ajax.org Code Editor (ACE). * * The Initial Developer of the Original Code is * Ajax.org B.V. * Portions created by the Initial Developer are Copyright (C) 2010 * the Initial Developer. All Rights Reserved. * * 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 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ define(function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); var dom = require("./lib/dom"); var event = require("./lib/event"); var useragent = require("./lib/useragent"); var config = require("./config"); var net = require("./lib/net"); var GutterLayer = require("./layer/gutter").Gutter; var MarkerLayer = require("./layer/marker").Marker; var TextLayer = require("./layer/text").Text; var CursorLayer = require("./layer/cursor").Cursor; var ScrollBar = require("./scrollbar").ScrollBar; var RenderLoop = require("./renderloop").RenderLoop; var EventEmitter = require("./lib/event_emitter").EventEmitter; var editorCss = require("ace/requirejs/text!./css/editor.css"); dom.importCssString(editorCss, "ace_editor"); /** * class VirtualRenderer * * The class that is responsible for drawing everything you see on the screen! * **/ /** * new VirtualRenderer(container, theme) * - container (DOMElement): The root element of the editor * - theme (String): The starting theme * * Constructs a new `VirtualRenderer` within the `container` specified, applying the given `theme`. * **/ var VirtualRenderer = function(container, theme) { var _self = this; this.container = container; // TODO: this breaks rendering in Cloud9 with multiple ace instances // // Imports CSS once per DOM document ('ace_editor' serves as an identifier). // dom.importCssString(editorCss, "ace_editor", container.ownerDocument); // in IE <= 9 the native cursor always shines through this.$keepTextAreaAtCursor = !useragent.isIE; dom.addCssClass(container, "ace_editor"); this.setTheme(theme); this.$gutter = dom.createElement("div"); this.$gutter.className = "ace_gutter"; this.container.appendChild(this.$gutter); this.scroller = dom.createElement("div"); this.scroller.className = "ace_scroller"; this.container.appendChild(this.scroller); this.content = dom.createElement("div"); this.content.className = "ace_content"; this.scroller.appendChild(this.content); this.$gutterLayer = new GutterLayer(this.$gutter); this.$gutterLayer.on("changeGutterWidth", this.onResize.bind(this, true)); this.setFadeFoldWidgets(true); this.$markerBack = new MarkerLayer(this.content); var textLayer = this.$textLayer = new TextLayer(this.content); this.canvas = textLayer.element; this.$markerFront = new MarkerLayer(this.content); this.characterWidth = textLayer.getCharacterWidth(); this.lineHeight = textLayer.getLineHeight(); this.$cursorLayer = new CursorLayer(this.content); this.$cursorPadding = 8; // Indicates whether the horizontal scrollbar is visible this.$horizScroll = false; this.$horizScrollAlwaysVisible = false; this.$animatedScroll = false; this.scrollBar = new ScrollBar(container); this.scrollBar.addEventListener("scroll", function(e) { if (!_self.$inScrollAnimation) _self.session.setScrollTop(e.data); }); this.scrollTop = 0; this.scrollLeft = 0; event.addListener(this.scroller, "scroll", function() { var scrollLeft = _self.scroller.scrollLeft; _self.scrollLeft = scrollLeft; _self.session.setScrollLeft(scrollLeft); }); this.cursorPos = { row : 0, column : 0 }; this.$textLayer.addEventListener("changeCharacterSize", function() { _self.characterWidth = textLayer.getCharacterWidth(); _self.lineHeight = textLayer.getLineHeight(); _self.$updatePrintMargin(); _self.onResize(true); _self.$loop.schedule(_self.CHANGE_FULL); }); this.$size = { width: 0, height: 0, scrollerHeight: 0, scrollerWidth: 0 }; this.layerConfig = { width : 1, padding : 0, firstRow : 0, firstRowScreen: 0, lastRow : 0, lineHeight : 1, characterWidth : 1, minHeight : 1, maxHeight : 1, offset : 0, height : 1 }; this.$loop = new RenderLoop( this.$renderChanges.bind(this), this.container.ownerDocument.defaultView ); this.$loop.schedule(this.CHANGE_FULL); this.setPadding(4); this.$updatePrintMargin(); }; (function() { this.showGutter = true; this.CHANGE_CURSOR = 1; this.CHANGE_MARKER = 2; this.CHANGE_GUTTER = 4; this.CHANGE_SCROLL = 8; this.CHANGE_LINES = 16; this.CHANGE_TEXT = 32; this.CHANGE_SIZE = 64; this.CHANGE_MARKER_BACK = 128; this.CHANGE_MARKER_FRONT = 256; this.CHANGE_FULL = 512; this.CHANGE_H_SCROLL = 1024; oop.implement(this, EventEmitter); /** * VirtualRenderer.setSession(session) -> Void * * Associates an [[EditSession `EditSession`]]. **/ this.setSession = function(session) { this.session = session; this.scroller.className = "ace_scroller"; this.$cursorLayer.setSession(session); this.$markerBack.setSession(session); this.$markerFront.setSession(session); this.$gutterLayer.setSession(session); this.$textLayer.setSession(session); this.$loop.schedule(this.CHANGE_FULL); }; /** * VirtualRenderer.updateLines(firstRow, lastRow) -> Void * - firstRow (Number): The first row to update * - lastRow (Number): The last row to update * * Triggers a partial update of the text, from the range given by the two parameters. **/ this.updateLines = function(firstRow, lastRow) { if (lastRow === undefined) lastRow = Infinity; if (!this.$changedLines) { this.$changedLines = { firstRow: firstRow, lastRow: lastRow }; } else { if (this.$changedLines.firstRow > firstRow) this.$changedLines.firstRow = firstRow; if (this.$changedLines.lastRow < lastRow) this.$changedLines.lastRow = lastRow; } this.$loop.schedule(this.CHANGE_LINES); }; /** * VirtualRenderer.updateText() -> Void * * Triggers a full update of the text, for all the rows. **/ this.updateText = function() { this.$loop.schedule(this.CHANGE_TEXT); }; /** * VirtualRenderer.updateFull() -> Void * * Triggers a full update of all the layers, for all the rows. **/ this.updateFull = function(force) { if (force){ this.$renderChanges(this.CHANGE_FULL, true); } else { this.$loop.schedule(this.CHANGE_FULL); } }; /** * VirtualRenderer.updateFontSize() -> Void * * Updates the font size. **/ this.updateFontSize = function() { this.$textLayer.checkForSizeChanges(); }; /** * VirtualRenderer.onResize(force) -> Void * - force (Boolean): If `true`, recomputes the size, even if the height and width haven't changed * * [Triggers a resize of the editor.]{: #VirtualRenderer.onResize} **/ this.onResize = function(force, gutterWidth, width, height) { var changes = this.CHANGE_SIZE; var size = this.$size; if (this.resizing > 2) return; else if (this.resizing > 1) this.resizing++; else this.resizing = force ? 1 : 0; if (!height) height = dom.getInnerHeight(this.container); if (force || size.height != height) { size.height = height; this.scroller.style.height = height + "px"; size.scrollerHeight = this.scroller.clientHeight; this.scrollBar.setHeight(size.scrollerHeight); if (this.session) { this.session.setScrollTop(this.getScrollTop()); changes = changes | this.CHANGE_FULL; } } if (!width) width = dom.getInnerWidth(this.container); if (force || this.resizing > 1 || size.width != width) { size.width = width; var gutterWidth = this.showGutter ? this.$gutter.offsetWidth : 0; this.scroller.style.left = gutterWidth + "px"; size.scrollerWidth = Math.max(0, width - gutterWidth - this.scrollBar.getWidth()); this.scroller.style.right = this.scrollBar.getWidth() + "px"; if (this.session.getUseWrapMode() && this.adjustWrapLimit() || force) changes = changes | this.CHANGE_FULL; } if (force) this.$renderChanges(changes, true); else this.$loop.schedule(changes); if (force) delete this.resizing; }; /** * VirtualRenderer.adjustWrapLimit() -> Void * * Adjusts the wrap limit, which is the number of characters that can fit within the width of the edit area on screen. **/ this.adjustWrapLimit = function() { var availableWidth = this.$size.scrollerWidth - this.$padding * 2; var limit = Math.floor(availableWidth / this.characterWidth); return this.session.adjustWrapLimit(limit); }; /** * VirtualRenderer.setAnimatedScroll(shouldAnimate) -> Void * - shouldAnimate (Boolean): Set to `true` to show animated scrolls * * Identifies whether you want to have an animated scroll or not. * **/ this.setAnimatedScroll = function(shouldAnimate){ this.$animatedScroll = shouldAnimate; }; /** * VirtualRenderer.getAnimatedScroll() -> Boolean * * Returns whether an animated scroll happens or not. **/ this.getAnimatedScroll = function() { return this.$animatedScroll; }; /** * VirtualRenderer.setShowInvisibles(showInvisibles) -> Void * - showInvisibles (Boolean): Set to `true` to show invisibles * * Identifies whether you want to show invisible characters or not. * **/ this.setShowInvisibles = function(showInvisibles) { if (this.$textLayer.setShowInvisibles(showInvisibles)) this.$loop.schedule(this.CHANGE_TEXT); }; /** * VirtualRenderer.getShowInvisibles() -> Boolean * * Returns whether invisible characters are being shown or not. **/ this.getShowInvisibles = function() { return this.$textLayer.showInvisibles; }; this.$showPrintMargin = true; /** * VirtualRenderer.setShowPrintMargin(showPrintMargin) * - showPrintMargin (Boolean): Set to `true` to show the print margin * * Identifies whether you want to show the print margin or not. * **/ this.setShowPrintMargin = function(showPrintMargin) { this.$showPrintMargin = showPrintMargin; this.$updatePrintMargin(); }; /** * VirtualRenderer.getShowPrintMargin() -> Boolean * * Returns whetherthe print margin is being shown or not. **/ this.getShowPrintMargin = function() { return this.$showPrintMargin; }; this.$printMarginColumn = 80; /** * VirtualRenderer.setPrintMarginColumn(showPrintMargin) * - showPrintMargin (Boolean): Set to `true` to show the print margin column * * Identifies whether you want to show the print margin column or not. * **/ this.setPrintMarginColumn = function(showPrintMargin) { this.$printMarginColumn = showPrintMargin; this.$updatePrintMargin(); }; /** * VirtualRenderer.getPrintMarginColumn() -> Boolean * * Returns whether the print margin column is being shown or not. **/ this.getPrintMarginColumn = function() { return this.$printMarginColumn; }; /** * VirtualRenderer.getShowGutter() -> Boolean * * Returns `true` if the gutter is being shown. **/ this.getShowGutter = function(){ return this.showGutter; }; /** * VirtualRenderer.setShowGutter(show) -> Void * - show (Boolean): Set to `true` to show the gutter * * Identifies whether you want to show the gutter or not. **/ this.setShowGutter = function(show){ if(this.showGutter === show) return; this.$gutter.style.display = show ? "block" : "none"; this.showGutter = show; this.onResize(true); }; this.getFadeFoldWidgets = function(){ return dom.hasCssClass(this.$gutter, "ace_fade-fold-widgets"); }; this.setFadeFoldWidgets = function(show) { if (show) dom.addCssClass(this.$gutter, "ace_fade-fold-widgets"); else dom.removeCssClass(this.$gutter, "ace_fade-fold-widgets"); }; this.$highlightGutterLine = true; this.setHighlightGutterLine = function(shouldHighlight) { if (this.$highlightGutterLine == shouldHighlight) return; this.$highlightGutterLine = shouldHighlight; this.$loop.schedule(this.CHANGE_GUTTER); }; this.getHighlightGutterLine = function() { return this.$highlightGutterLine; }; this.$updateGutterLineHighlight = function(gutterReady) { var i = this.session.selection.lead.row; if (i == this.$gutterLineHighlight) return; if (!gutterReady) { var lineEl, ch = this.$gutterLayer.element.children; var index = this.$gutterLineHighlight - this.layerConfig.firstRow; if (index >= 0 && (lineEl = ch[index])) dom.removeCssClass(lineEl, "ace_gutter_active_line"); index = i - this.layerConfig.firstRow; if (index >= 0 && (lineEl = ch[index])) dom.addCssClass(lineEl, "ace_gutter_active_line"); } this.$gutterLayer.removeGutterDecoration(this.$gutterLineHighlight, "ace_gutter_active_line"); this.$gutterLayer.addGutterDecoration(i, "ace_gutter_active_line"); this.$gutterLineHighlight = i; }; this.$updatePrintMargin = function() { var containerEl; if (!this.$showPrintMargin && !this.$printMarginEl) return; if (!this.$printMarginEl) { containerEl = dom.createElement("div"); containerEl.className = "ace_print_margin_layer"; this.$printMarginEl = dom.createElement("div"); this.$printMarginEl.className = "ace_print_margin"; containerEl.appendChild(this.$printMarginEl); this.content.insertBefore(containerEl, this.$textLayer.element); } var style = this.$printMarginEl.style; style.left = ((this.characterWidth * this.$printMarginColumn) + this.$padding) + "px"; style.visibility = this.$showPrintMargin ? "visible" : "hidden"; }; /** * VirtualRenderer.getContainerElement() -> DOMElement * * Returns the root element containing this renderer. **/ this.getContainerElement = function() { return this.container; }; /** * VirtualRenderer.getMouseEventTarget() -> DOMElement * * Returns the element that the mouse events are attached to **/ this.getMouseEventTarget = function() { return this.content; }; /** * VirtualRenderer.getTextAreaContainer() -> DOMElement * * Returns the element to which the hidden text area is added. **/ this.getTextAreaContainer = function() { return this.container; }; // move text input over the cursor // this is required for iOS and IME this.$moveTextAreaToCursor = function() { if (!this.$keepTextAreaAtCursor) return; var posTop = this.$cursorLayer.$pixelPos.top; var posLeft = this.$cursorLayer.$pixelPos.left; posTop -= this.layerConfig.offset; if (posTop < 0 || posTop > this.layerConfig.height - this.lineHeight) return; var w = this.characterWidth; if (this.$composition) w += this.textarea.scrollWidth; posLeft -= this.scrollLeft; if (posLeft > this.$size.scrollerWidth - w) posLeft = this.$size.scrollerWidth - w; if (this.showGutter) posLeft += this.$gutterLayer.gutterWidth; this.textarea.style.height = this.lineHeight + "px"; this.textarea.style.width = w + "px"; this.textarea.style.left = posLeft + "px"; this.textarea.style.top = posTop - 1 + "px"; }; /** * VirtualRenderer.getFirstVisibleRow() -> Number * * [Returns the index of the first visible row.]{: #VirtualRenderer.getFirstVisibleRow} **/ this.getFirstVisibleRow = function() { return this.layerConfig.firstRow; }; /** * VirtualRenderer.getFirstFullyVisibleRow() -> Number * * Returns the index of the first fully visible row. "Fully" here means that the characters in the row are not truncated; that the top and the bottom of the row are on the screen. **/ this.getFirstFullyVisibleRow = function() { return this.layerConfig.firstRow + (this.layerConfig.offset === 0 ? 0 : 1); }; /** * VirtualRenderer.getLastFullyVisibleRow() -> Number * * Returns the index of the last fully visible row. "Fully" here means that the characters in the row are not truncated; that the top and the bottom of the row are on the screen. **/ this.getLastFullyVisibleRow = function() { var flint = Math.floor((this.layerConfig.height + this.layerConfig.offset) / this.layerConfig.lineHeight); return this.layerConfig.firstRow - 1 + flint; }; /** * VirtualRenderer.getLastVisibleRow() -> Number * * [Returns the index of the last visible row.]{: #VirtualRenderer.getLastVisibleRow} **/ this.getLastVisibleRow = function() { return this.layerConfig.lastRow; }; this.$padding = null; /** * VirtualRenderer.setPadding(padding) -> Void * - padding (Number): A new padding value (in pixels) * * Sets the padding for all the layers. * **/ this.setPadding = function(padding) { this.$padding = padding; this.$textLayer.setPadding(padding); this.$cursorLayer.setPadding(padding); this.$markerFront.setPadding(padding); this.$markerBack.setPadding(padding); this.$loop.schedule(this.CHANGE_FULL); this.$updatePrintMargin(); }; /** * VirtualRenderer.getHScrollBarAlwaysVisible() -> Boolean * * Returns whether the horizontal scrollbar is set to be always visible. **/ this.getHScrollBarAlwaysVisible = function() { return this.$horizScrollAlwaysVisible; }; /** * VirtualRenderer.setHScrollBarAlwaysVisible(alwaysVisible) -> Void * - alwaysVisible (Boolean): Set to `true` to make the horizontal scroll bar visible * * Identifies whether you want to show the horizontal scrollbar or not. **/ this.setHScrollBarAlwaysVisible = function(alwaysVisible) { if (this.$horizScrollAlwaysVisible != alwaysVisible) { this.$horizScrollAlwaysVisible = alwaysVisible; if (!this.$horizScrollAlwaysVisible || !this.$horizScroll) this.$loop.schedule(this.CHANGE_SCROLL); } }; this.$updateScrollBar = function() { this.scrollBar.setInnerHeight(this.layerConfig.maxHeight); this.scrollBar.setScrollTop(this.scrollTop); }; this.$renderChanges = function(changes, force) { if (!force && (!changes || !this.session || !this.container.offsetWidth)) return; // text, scrolling and resize changes can cause the view port size to change if (changes & this.CHANGE_FULL || changes & this.CHANGE_SIZE || changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES || changes & this.CHANGE_SCROLL ) this.$computeLayerConfig(); // horizontal scrolling if (changes & this.CHANGE_H_SCROLL) { this.scroller.scrollLeft = this.scrollLeft; // read the value after writing it since the value might get clipped var scrollLeft = this.scroller.scrollLeft; this.scrollLeft = scrollLeft; this.session.setScrollLeft(scrollLeft); this.scroller.className = this.scrollLeft == 0 ? "ace_scroller" : "ace_scroller horscroll"; } // full if (changes & this.CHANGE_FULL) { this.$textLayer.checkForSizeChanges(); // update scrollbar first to not lose scroll position when gutter calls resize this.$updateScrollBar(); this.$textLayer.update(this.layerConfig); if (this.showGutter) { if (this.$highlightGutterLine) this.$updateGutterLineHighlight(true); this.$gutterLayer.update(this.layerConfig); } this.$markerBack.update(this.layerConfig); this.$markerFront.update(this.layerConfig); this.$cursorLayer.update(this.layerConfig); this.$moveTextAreaToCursor(); return; } // scrolling if (changes & this.CHANGE_SCROLL) { this.$updateScrollBar(); if (changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES) this.$textLayer.update(this.layerConfig); else this.$textLayer.scrollLines(this.layerConfig); if (this.showGutter) { if (this.$highlightGutterLine) this.$updateGutterLineHighlight(true); this.$gutterLayer.update(this.layerConfig); } this.$markerBack.update(this.layerConfig); this.$markerFront.update(this.layerConfig); this.$cursorLayer.update(this.layerConfig); this.$moveTextAreaToCursor(); return; } if (changes & this.CHANGE_TEXT) { this.$textLayer.update(this.layerConfig); if (this.showGutter) this.$gutterLayer.update(this.layerConfig); } else if (changes & this.CHANGE_LINES) { if (this.$updateLines()) { this.$updateScrollBar(); if (this.showGutter) this.$gutterLayer.update(this.layerConfig); } } else if (changes & this.CHANGE_GUTTER) { if (this.showGutter) this.$gutterLayer.update(this.layerConfig); } if (changes & this.CHANGE_CURSOR) { this.$cursorLayer.update(this.layerConfig); this.$moveTextAreaToCursor(); this.$highlightGutterLine && this.$updateGutterLineHighlight(false); } if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_FRONT)) { this.$markerFront.update(this.layerConfig); } if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_BACK)) { this.$markerBack.update(this.layerConfig); } if (changes & this.CHANGE_SIZE) this.$updateScrollBar(); }; 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 horizScroll = this.$horizScrollAlwaysVisible || this.$size.scrollerWidth - longestLine < 0; var horizScrollChanged = this.$horizScroll !== horizScroll; this.$horizScroll = horizScroll; if (horizScrollChanged) { this.scroller.style.overflowX = horizScroll ? "scroll" : "hidden"; // when we hide scrollbar scroll event isn't emited // leaving session with wrong scrollLeft value if (!horizScroll) this.session.setScrollLeft(0); } var maxHeight = this.session.getScreenLength() * this.lineHeight; this.session.setScrollTop(Math.max(0, Math.min(this.scrollTop, maxHeight - this.$size.scrollerHeight))); var lineCount = Math.ceil(minHeight / this.lineHeight) - 1; var firstRow = Math.max(0, Math.round((this.scrollTop - offset) / this.lineHeight)); 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, 0); // Check if firstRow is inside of a foldLine. If true, then use the first // row of the foldLine. var foldLine = session.getFoldLine(firstRow); if (foldLine) { firstRow = foldLine.start.row; } firstRowScreen = session.documentToScreenRow(firstRow, 0); firstRowHeight = session.getRowHeight(lineHeight, firstRow); lastRow = Math.min(session.screenToDocumentRow(lastRow, 0), session.getLength() - 1); minHeight = this.$size.scrollerHeight + session.getRowHeight(lineHeight, lastRow)+ firstRowHeight; offset = this.scrollTop - firstRowScreen * this.lineHeight; this.layerConfig = { width : longestLine, padding : this.$padding, firstRow : firstRow, firstRowScreen: firstRowScreen, lastRow : lastRow, lineHeight : this.lineHeight, characterWidth : this.characterWidth, minHeight : minHeight, maxHeight : maxHeight, offset : offset, height : this.$size.scrollerHeight }; // For debugging. // console.log(JSON.stringify(this.layerConfig)); this.$gutterLayer.element.style.marginTop = (-offset) + "px"; this.content.style.marginTop = (-offset) + "px"; this.content.style.width = longestLine + 2 * this.$padding + "px"; this.content.style.height = minHeight + "px"; // Horizontal scrollbar visibility may have changed, which changes // the client height of the scroller if (horizScrollChanged) this.onResize(true); }; this.$updateLines = function() { var firstRow = this.$changedLines.firstRow; var lastRow = this.$changedLines.lastRow; this.$changedLines = null; var layerConfig = this.layerConfig; if (firstRow > layerConfig.lastRow + 1) { return; } if (lastRow < layerConfig.firstRow) { return; } // if the last row is unknown -> redraw everything if (lastRow === Infinity) { if (this.showGutter) this.$gutterLayer.update(layerConfig); this.$textLayer.update(layerConfig); return; } // else update only the changed rows this.$textLayer.updateLines(layerConfig, firstRow, lastRow); return true; }; this.$getLongestLine = function() { var charCount = this.session.getScreenWidth(); if (this.$textLayer.showInvisibles) charCount += 1; return Math.max(this.$size.scrollerWidth - 2 * this.$padding, Math.round(charCount * this.characterWidth)); }; /** * VirtualRenderer.updateFrontMarkers() -> Void * * Schedules an update to all the front markers in the document. **/ this.updateFrontMarkers = function() { this.$markerFront.setMarkers(this.session.getMarkers(true)); this.$loop.schedule(this.CHANGE_MARKER_FRONT); }; /** * VirtualRenderer.updateBackMarkers() -> Void * * Schedules an update to all the back markers in the document. **/ this.updateBackMarkers = function() { this.$markerBack.setMarkers(this.session.getMarkers()); this.$loop.schedule(this.CHANGE_MARKER_BACK); }; /** * VirtualRenderer.addGutterDecoration(row, className) -> Void * - row (Number): The row number * - className (String): The class to add * * Adds `className` to the `row`, to be used for CSS stylings and whatnot. **/ this.addGutterDecoration = function(row, className){ this.$gutterLayer.addGutterDecoration(row, className); this.$loop.schedule(this.CHANGE_GUTTER); }; /** * VirtualRenderer.removeGutterDecoration(row, className)-> Void * - row (Number): The row number * - className (String): The class to add * * Removes `className` from the `row`. **/ this.removeGutterDecoration = function(row, className){ this.$gutterLayer.removeGutterDecoration(row, className); this.$loop.schedule(this.CHANGE_GUTTER); }; /** * VirtualRenderer.updateBreakpoints() -> Void * * Redraw breakpoints. **/ this.updateBreakpoints = function(rows) { this.$loop.schedule(this.CHANGE_GUTTER); }; /** * VirtualRenderer.setAnnotations(annotations) -> Void * - annotations (Array): An array containing annotations * * Sets annotations for the gutter. **/ this.setAnnotations = function(annotations) { this.$gutterLayer.setAnnotations(annotations); this.$loop.schedule(this.CHANGE_GUTTER); }; /** * VirtualRenderer.updateCursor() -> Void * * Updates the cursor icon. **/ this.updateCursor = function() { this.$loop.schedule(this.CHANGE_CURSOR); }; /** * VirtualRenderer.hideCursor() -> Void * * Hides the cursor icon. **/ this.hideCursor = function() { this.$cursorLayer.hideCursor(); }; /** * VirtualRenderer.showCursor() -> Void * * Shows the cursor icon. **/ this.showCursor = function() { this.$cursorLayer.showCursor(); }; this.scrollSelectionIntoView = function(anchor, lead, offset) { // first scroll anchor into view then scroll lead into view this.scrollCursorIntoView(anchor, offset); this.scrollCursorIntoView(lead, offset); }; /** * VirtualRenderer.scrollCursorIntoView(cursor, offset) -> Void * * Scrolls the cursor into the first visibile area of the editor **/ this.scrollCursorIntoView = function(cursor, offset) { // the editor is not visible if (this.$size.scrollerHeight === 0) return; var pos = this.$cursorLayer.getPixelPosition(cursor); var left = pos.left; var top = pos.top; if (this.scrollTop > top) { if (offset) top -= offset * this.$size.scrollerHeight; this.session.setScrollTop(top); } else if (this.scrollTop + this.$size.scrollerHeight < top + this.lineHeight) { if (offset) top += offset * this.$size.scrollerHeight; this.session.setScrollTop(top + this.lineHeight - this.$size.scrollerHeight); } var scrollLeft = this.scrollLeft; if (scrollLeft > left) { if (left < this.$padding + 2 * this.layerConfig.characterWidth) left = 0; this.session.setScrollLeft(left); } else if (scrollLeft + this.$size.scrollerWidth < left + this.characterWidth) { this.session.setScrollLeft(Math.round(left + this.characterWidth - this.$size.scrollerWidth)); } }; /** related to: EditSession.getScrollTop * VirtualRenderer.getScrollTop() -> Number * * {:EditSession.getScrollTop} **/ this.getScrollTop = function() { return this.session.getScrollTop(); }; /** related to: EditSession.getScrollLeft * VirtualRenderer.getScrollLeft() -> Number * * {:EditSession.getScrollLeft} **/ this.getScrollLeft = function() { return this.session.getScrollLeft(); }; /** * VirtualRenderer.getScrollTopRow() -> Number * * Returns the first visible row, regardless of whether it's fully visible or not. **/ this.getScrollTopRow = function() { return this.scrollTop / this.lineHeight; }; /** * VirtualRenderer.getScrollBottomRow() -> Number * * Returns the last visible row, regardless of whether it's fully visible or not. **/ this.getScrollBottomRow = function() { return Math.max(0, Math.floor((this.scrollTop + this.$size.scrollerHeight) / this.lineHeight) - 1); }; /** related to: EditSession.setScrollTop * VirtualRenderer.scrollToRow(row) -> Void * - row (Number): A row id * * Gracefully scrolls the top of the editor to the row indicated. **/ this.scrollToRow = function(row) { this.session.setScrollTop(row * this.lineHeight); }; this.alignCursor = function(cursor, alignment) { if (typeof cursor == "number") cursor = {row: cursor, column: 0}; var pos = this.$cursorLayer.getPixelPosition(cursor); var offset = pos.top - this.$size.scrollerHeight * (alignment || 0); this.session.setScrollTop(offset); }; this.STEPS = 8; this.$calcSteps = function(fromValue, toValue){ var i = 0; var l = this.STEPS; var steps = []; var func = function(t, x_min, dx) { return dx * (Math.pow(t - 1, 3) + 1) + x_min; }; for (i = 0; i < l; ++i) steps.push(func(i / this.STEPS, fromValue, toValue - fromValue)); return steps; }; /** * VirtualRenderer.scrollToLine(line, center, animate, callback) -> Void * - line (Number): A line number * - center (Boolean): If `true`, centers the editor the to indicated line * - animate (Boolean): If `true` animates scrolling * - callback (Function): Function to be called after the animation has finished * * Gracefully scrolls the editor to the row indicated. **/ this.scrollToLine = function(line, center, animate, callback) { var pos = this.$cursorLayer.getPixelPosition({row: line, column: 0}); var offset = pos.top; if (center) offset -= this.$size.scrollerHeight / 2; var initialScroll = this.scrollTop; this.session.setScrollTop(offset); if (animate !== false) this.animateScrolling(initialScroll, callback); }; this.animateScrolling = function(fromValue, callback) { var toValue = this.scrollTop; if (this.$animatedScroll && Math.abs(fromValue - toValue) < 100000) { var _self = this; var steps = _self.$calcSteps(fromValue, toValue); this.$inScrollAnimation = true; clearInterval(this.$timer); _self.session.setScrollTop(steps.shift()); this.$timer = setInterval(function() { if (steps.length) { _self.session.setScrollTop(steps.shift()); // trick session to think it's already scrolled to not loose toValue _self.session.$scrollTop = toValue; } else if (toValue != null) { _self.session.$scrollTop = -1; _self.session.setScrollTop(toValue); toValue = null; } else { // do this on separate step to not get spurious scroll event from scrollbar _self.$timer = clearInterval(_self.$timer); _self.$inScrollAnimation = false; callback && callback(); } }, 10); } }; /** * VirtualRenderer.scrollToY(scrollTop) -> Number * - scrollTop (Number): The position to scroll to * * Scrolls the editor to the y pixel indicated. * **/ this.scrollToY = function(scrollTop) { // after calling scrollBar.setScrollTop // scrollbar sends us event with same scrollTop. ignore it if (this.scrollTop !== scrollTop) { this.$loop.schedule(this.CHANGE_SCROLL); this.scrollTop = scrollTop; } }; /** * VirtualRenderer.scrollToX(scrollLeft) -> Number * - scrollLeft (Number): The position to scroll to * * Scrolls the editor to the x pixel indicated. * **/ this.scrollToX = function(scrollLeft) { if (scrollLeft < 0) scrollLeft = 0; if (this.scrollLeft !== scrollLeft) this.scrollLeft = scrollLeft; this.$loop.schedule(this.CHANGE_H_SCROLL); }; /** * VirtualRenderer.scrollBy(deltaX, deltaY) -> Void * - deltaX (Number): The x value to scroll by * - deltaY (Number): The y value to scroll by * * Scrolls the editor across both x- and y-axes. **/ this.scrollBy = function(deltaX, deltaY) { deltaY && this.session.setScrollTop(this.session.getScrollTop() + deltaY); deltaX && this.session.setScrollLeft(this.session.getScrollLeft() + deltaX); }; /** * VirtualRenderer.isScrollableBy(deltaX, deltaY) -> Boolean * - deltaX (Number): The x value to scroll by * - deltaY (Number): The y value to scroll by * * Returns `true` if you can still scroll by either parameter; in other words, you haven't reached the end of the file or line. **/ this.isScrollableBy = function(deltaX, deltaY) { if (deltaY < 0 && this.session.getScrollTop() > 0) return true; if (deltaY > 0 && this.session.getScrollTop() + this.$size.scrollerHeight < this.layerConfig.maxHeight) return true; // todo: handle horizontal scrolling }; this.pixelToScreenCoordinates = function(x, y) { var canvasPos = this.scroller.getBoundingClientRect(); var offset = (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth; var row = Math.floor((y + this.scrollTop - canvasPos.top) / this.lineHeight); var col = Math.round(offset); return {row: row, column: col, side: offset - col > 0 ? 1 : -1}; }; this.screenToTextCoordinates = function(x, y) { var canvasPos = this.scroller.getBoundingClientRect(); var col = Math.round( (x + this.scrollLeft - canvasPos.left - this.$padding) / this.characterWidth ); var row = Math.floor( (y + this.scrollTop - canvasPos.top) / this.lineHeight ); return this.session.screenToDocumentPosition(row, Math.max(col, 0)); }; /** * VirtualRenderer.textToScreenCoordinates(row, column) -> Object * - row (Number): The document row position * - column (Number): The document column position * * Returns an object containing the `pageX` and `pageY` coordinates of the document position. * * **/ this.textToScreenCoordinates = function(row, column) { var canvasPos = this.scroller.getBoundingClientRect(); var pos = this.session.documentToScreenPosition(row, column); var x = this.$padding + Math.round(pos.column * this.characterWidth); var y = pos.row * this.lineHeight; return { pageX: canvasPos.left + x - this.scrollLeft, pageY: canvasPos.top + y - this.scrollTop }; }; /** * VirtualRenderer.visualizeFocus() -> Void * * Focuses the current container. **/ this.visualizeFocus = function() { dom.addCssClass(this.container, "ace_focus"); }; /** * VirtualRenderer.visualizeBlur() -> Void * * Blurs the current container. **/ this.visualizeBlur = function() { dom.removeCssClass(this.container, "ace_focus"); }; /** internal, hide * VirtualRenderer.showComposition(position) -> Void * - position (Number): * **/ this.showComposition = function(position) { if (!this.$composition) this.$composition = { keepTextAreaAtCursor: this.$keepTextAreaAtCursor, cssText: this.textarea.style.cssText }; this.$keepTextAreaAtCursor = true; dom.addCssClass(this.textarea, "ace_composition"); this.textarea.style.cssText = ""; this.$moveTextAreaToCursor(); }; /** * VirtualRenderer.setCompositionText(text) -> Void * - text (String): A string of text to use * * Sets the inner text of the current composition to `text`. **/ this.setCompositionText = function(text) { this.$moveTextAreaToCursor(); }; /** * VirtualRenderer.hideComposition() -> Void * * Hides the current composition. **/ this.hideComposition = function() { if (!this.$composition) return; dom.removeCssClass(this.textarea, "ace_composition"); this.$keepTextAreaAtCursor = this.$composition.keepTextAreaAtCursor; this.textarea.style.cssText = this.$composition.cssText; this.$composition = null; }; this._loadTheme = function(name, callback) { if (!config.get("packaged")) return callback(); var base = name.split("/").pop(); var filename = config.get("themePath") + "/theme-" + base + config.get("suffix"); net.loadScript(filename, callback); }; /** * VirtualRenderer.setTheme(theme) -> Void * - theme (String): The path to a theme * * [Sets a new theme for the editor. `theme` should exist, and be a directory path, like `ace/theme/textmate`.]{: #VirtualRenderer.setTheme} **/ this.setTheme = function(theme) { var _self = this; this.$themeValue = theme; if (!theme || typeof theme == "string") { var moduleName = theme || "ace/theme/textmate"; var module; try { module = require(moduleName); } catch (e) {}; if (module) return afterLoad(module); _self._loadTheme(moduleName, function() { require([moduleName], function(module) { if (_self.$themeValue !== theme) return; afterLoad(module); }); }); } else { afterLoad(theme); } function afterLoad(theme) { dom.importCssString( theme.cssText, theme.cssClass, _self.container.ownerDocument ); if (_self.$theme) dom.removeCssClass(_self.container, _self.$theme); _self.$theme = theme ? theme.cssClass : null; if (_self.$theme) dom.addCssClass(_self.container, _self.$theme); if (theme && theme.isDark) dom.addCssClass(_self.container, "ace_dark"); else dom.removeCssClass(_self.container, "ace_dark"); // force re-measure of the gutter width if (_self.$size) { _self.$size.width = 0; _self.onResize(); } } }; /** * VirtualRenderer.getTheme() -> String * * [Returns the path of the current theme.]{: #VirtualRenderer.getTheme} **/ this.getTheme = function() { return this.$themeValue; }; // Methods allows to add / remove CSS classnames to the editor element. // This feature can be used by plug-ins to provide a visual indication of // a certain mode that editor is in. /** * VirtualRenderer.setStyle(style) -> Void * - style (String): A class name * * [Adds a new class, `style`, to the editor.]{: #VirtualRenderer.setStyle} **/ this.setStyle = function setStyle(style) { dom.addCssClass(this.container, style); }; /** * VirtualRenderer.unsetStyle(style) -> Void * - style (String): A class name * * [Removes the class `style` from the editor.]{: #VirtualRenderer.unsetStyle} **/ this.unsetStyle = function unsetStyle(style) { dom.removeCssClass(this.container, style); }; /** * VirtualRenderer.destroy() * * Destroys the text and cursor layers for this renderer. **/ this.destroy = function() { this.$textLayer.destroy(); this.$cursorLayer.destroy(); }; }).call(VirtualRenderer.prototype); exports.VirtualRenderer = VirtualRenderer; });