From da0cc83dcbbbe06cdb45613631b23ab7d8c5582b Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Tue, 14 Aug 2012 12:32:03 +0200 Subject: [PATCH] Implement a new gutter system - Multiple gutters are explicitly created with the gutters option - Markers go into a specific gutter, new setGutterMarker, clearGutter methods - The line number gutter is separate - The 'fixedGutter' feature is removed (painful to do now, was shaky anyway) --- demo/folding.html | 5 +- demo/marker.html | 47 ++++--- doc/manual.html | 101 +++++++------- lib/codemirror.css | 52 +++++--- lib/codemirror.js | 304 +++++++++++++++++++++++++------------------ lib/util/foldcode.js | 9 +- test/test.js | 19 ++- 7 files changed, 310 insertions(+), 227 deletions(-) diff --git a/demo/folding.html b/demo/folding.html index 39a6a60e15..994f5862fc 100644 --- a/demo/folding.html +++ b/demo/folding.html @@ -12,7 +12,8 @@ @@ -39,6 +40,7 @@

CodeMirror: Code Folding Demo

window.editor = CodeMirror.fromTextArea(te, { mode: "javascript", lineNumbers: true, + gutters: ["CodeMirror-linenumbers", "CodeMirror-folded"], lineWrapping: true, onGutterClick: foldFunc, extraKeys: {"Ctrl-Q": function(cm){foldFunc(cm, cm.getCursor().line);}} @@ -50,6 +52,7 @@

CodeMirror: Code Folding Demo

window.editor_html = CodeMirror.fromTextArea(te_html, { mode: "text/html", lineNumbers: true, + gutters: ["CodeMirror-linenumbers", "CodeMirror-folded"], lineWrapping: true, onGutterClick: foldFunc_html, extraKeys: {"Ctrl-Q": function(cm){foldFunc_html(cm, cm.getCursor().line);}} diff --git a/demo/marker.html b/demo/marker.html index 7bc6c6defe..c570be3546 100644 --- a/demo/marker.html +++ b/demo/marker.html @@ -9,13 +9,8 @@ @@ -24,29 +19,39 @@

CodeMirror: Breakpoint demo

Click the line-number gutter to add or remove 'breakpoints'.

diff --git a/doc/manual.html b/doc/manual.html index 6b4dc8c7b5..4d1a25957e 100644 --- a/doc/manual.html +++ b/doc/manual.html @@ -193,14 +193,17 @@

Configuration

lineNumberFormatter (function(integer))
A function used to format line numbers. The function is passed the current line number. Default prints the line number verbatim.
-
gutter (boolean)
-
Can be used to force a 'gutter' (empty space on the left of - the editor) to be shown even when no line numbers are active. - This is useful for setting markers.
- -
fixedGutter (boolean)
-
When enabled (off by default), this will make the gutter - stay visible when the document is scrolled horizontally.
+
gutters (array)
+
Can be used to add extra gutters (beyond or instead of the + line number gutter). Should be an array of CSS class names, each + of which defines a width (and optionally a + background), and which will be used to draw the background of + the gutters. May include + the CodeMirror-linenumbers class, in order to + explicitly set the position of the line number gutter (it will + default to be to the right of all other gutters). These class + names are the keys passed + to setGutterMarker.
readOnly (boolean)
This disables editing of the editor content by the user. If @@ -238,8 +241,9 @@

Configuration

When given, will be called whenever the editor gutter (the line-number area) is clicked. Will be given the editor instance as first argument, the (zero-based) number of the line that was - clicked as second argument, and the raw mousedown - event object as third argument.
+ clicked as second argument, the raw mousedown event + object as third argument, and the CSS class of the gutter that + was clicked as fourth argument.
onFocus, onBlur (function)
The given functions will be called whenever the editor is @@ -421,25 +425,25 @@

Customized Styling

class. This is used to hide the cursor and give the selection a different color when the editor is not focused.
-
CodeMirror-gutter
-
Use this for giving a background or a border to the editor - gutter. Don't set any padding here, - use CodeMirror-gutter-text for that. By default, - the gutter is 'fluid', meaning it will adjust its width to the - maximum line number or line marker width. You can also set a - fixed width if you want.
- -
CodeMirror-gutter-text
-
Used to style the actual line numbers. For the numbers to - line up, you must make sure that the font in the gutter is the - same as the one in the rest of the editor, so you should - probably only set font style and size in - the CodeMirror class.
+
CodeMirror-gutters
+
This is the backdrop for all gutters. Use it to set the + default gutter background color, and optionally add a border on + the right of the gutters.
+ +
CodeMirror-linenumbers
+
Use this for giving a background or width to the line number + gutter.
+ +
CodeMirror-linenumber
+
Used to style the actual individual line numbers. These + won't be children of the CodeMirror-linenumbers + (plural) element, but rather will be absolutely positioned to + overlay it. Use this to set alignment and text properties for + the line numbers.
CodeMirror-lines
-
The visible lines. If this has vertical - padding, CodeMirror-gutter should have the same - padding.
+
The visible lines. This is where you specify vertical + padding for the editor content.
CodeMirror-cursor
The cursor is a block element that is absolutely positioned. @@ -459,7 +463,9 @@

Customized Styling

the wrapper (class CodeMirror) element, and a height on the scroller - (class CodeMirror-scroll) element.

+ (class CodeMirror-scroll) element. + The setSize method is the best + way to dynamically change size at runtime.

The actual lines, as well as the cursor, are represented by pre elements. By default no text styling (such as @@ -613,25 +619,19 @@

Programming API

Returns an array of all the bookmarks and marked ranges present at the given position.
-
setMarker(line, text, className) → lineHandle
-
Add a gutter marker for the given line. Gutter markers are - shown in the line-number area (instead of the number for this - line). Both text and className are - optional. Setting text to a Unicode character like - ● tends to give a nice effect. To put a picture in the gutter, - set text to a space and className to - something that sets a background image. If you - specify text, the given text (which may contain - HTML) will, by default, replace the line number for that line. - If this is not what you want, you can include the - string %N% in the text, which will be replaced by - the line number.
-
clearMarker(line)
-
Clears a marker created - with setMarker. line can be either a - number or a handle returned by setMarker (since a - number may now refer to a different line if something was added - or deleted).
+
setGutterMarker(line, gutterID, value) → lineHandle
+
Sets the gutter marker for the given gutter (identified by + its CSS class, see + the gutters option) + to the given value. Value can be either null, to + clear the marker, or a DOM element, to set it. The DOM element + will be shown in the specified gutter next to the specified + line.
+ +
clearGutter(gutterID)
+
Remove all gutter markers in + the gutter with the given ID.
+
setLineClass(line, className, backgroundClassName) → lineHandle
Set a CSS class name for the given line. line can be a number or a line handle (as returned @@ -658,8 +658,9 @@

Programming API

Returns the line number, text content, and marker status of the given line, which can be either a number or a handle returned by setMarker. The returned object has the - structure {line, handle, text, markerText, markerClass, - lineClass, bgClass}.
+ structure {line, handle, text, gutterMarkers, lineClass, + bgClass}, where gutterMarkers is an object + mapping gutter IDs to marker elements.
getLineHandle(num) → lineHandle
Fetches the line handle for the given line number.
@@ -772,7 +773,7 @@

Programming API

the refresh method afterwards.)
getGutterElement() → node
-
Fetches the DOM node that represents the editor gutter.
+
Fetches the DOM node that contains the editor gutters.
getStateAfter(line) → state
Returns the mode's parser state, if any, at the end of the diff --git a/lib/codemirror.css b/lib/codemirror.css index f0e91b2d73..ddd3ac04ac 100644 --- a/lib/codemirror.css +++ b/lib/codemirror.css @@ -46,36 +46,47 @@ min-width: 18px; } -.CodeMirror-gutter { +.CodeMirror-gutters { position: absolute; left: 0; top: 0; - z-index: 10; + height: 100%; + border-right: 1px solid #ddd; background-color: #f7f7f7; - border-right: 1px solid #eee; - min-width: 2em; +} +.CodeMirror-gutter { height: 100%; + float: left; +} +.CodeMirror-gutter-elt { + position: absolute; + top: 0; + cursor: default; +} + +.CodeMirror-linenumbers { + padding: 0 .2em; } -.CodeMirror-gutter-text { - color: #aaa; +.CodeMirror-linenumber { + min-width: 1.8em; text-align: right; - padding: .4em .2em .4em .4em; + color: #999; + padding: 0 .2em; white-space: pre !important; - cursor: default; + font-size: 80%; } + .CodeMirror-lines { - padding: .4em; + padding: .4em 0; white-space: pre; cursor: text; } .CodeMirror pre { - -moz-border-radius: 0; - -webkit-border-radius: 0; - -o-border-radius: 0; - border-radius: 0; - border-width: 0; margin: 0; padding: 0; background: transparent; + -moz-border-radius: 0; -webkit-border-radius: 0; -o-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; font-family: inherit; font-size: inherit; - padding: 0; margin: 0; + padding: 0 .4em; margin: 0; white-space: pre; word-wrap: normal; line-height: inherit; @@ -91,8 +102,11 @@ overflow-x: hidden; } -.CodeMirror textarea { - outline: none !important; +.CodeMirror-measure { + position: absolute; + width: 100%; height: 0px; + overflow: hidden; + visibility: hidden; } .CodeMirror pre.CodeMirror-cursor { @@ -164,10 +178,8 @@ div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} @media print { - /* Hide the cursor when printing */ .CodeMirror pre.CodeMirror-cursor { visibility: hidden; } - -} \ No newline at end of file +} diff --git a/lib/codemirror.js b/lib/codemirror.js index 56ec4d3508..227b78b090 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -14,7 +14,7 @@ window.CodeMirror = (function() { if (defaults.hasOwnProperty(opt)) options[opt] = (givenOptions && givenOptions.hasOwnProperty(opt) ? givenOptions : defaults)[opt]; - var input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em"); + var input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none;"); input.setAttribute("wrap", "off"); input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); // Wraps and hides input textarea var inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); @@ -27,13 +27,13 @@ window.CodeMirror = (function() { // Blinky cursor, and element used to ensure cursor fits at the end of a line var cursor = elt("pre", "\u00a0", "CodeMirror-cursor"), widthForcer = elt("pre", "\u00a0", "CodeMirror-cursor", "visibility: hidden"); // Used to measure text size - var measure = elt("div", null, null, "position: absolute; width: 100%; height: 0px; overflow: hidden; visibility: hidden;"); + var measure = elt("div", null, "CodeMirror-measure"); var lineSpace = elt("div", [measure, cursor, widthForcer, selectionDiv, lineDiv], null, "position: relative; z-index: 0"); - var gutterText = elt("div", null, "CodeMirror-gutter-text"), gutter = elt("div", [gutterText], "CodeMirror-gutter"); // Moved around its parent to cover visible view - var mover = elt("div", [gutter, elt("div", [lineSpace], "CodeMirror-lines")], null, "position: relative"); + var mover = elt("div", [elt("div", [lineSpace], "CodeMirror-lines")], null, "position: relative"); + var gutters = elt("div", null, "CodeMirror-gutters"), lineGutter; // Set to the height of the text, causes scrolling - var sizer = elt("div", [mover], null, "position: relative"); + var sizer = elt("div", [gutters, mover], null, "position: relative;"); // Provides scrolling var scroller = elt("div", [sizer], "CodeMirror-scroll"); scroller.setAttribute("tabIndex", "-1"); @@ -48,7 +48,7 @@ window.CodeMirror = (function() { lineSpace.style.outline = "none"; if (options.tabindex != null) input.tabIndex = options.tabindex; if (options.autofocus) focusInput(); - if (!options.gutter && !options.lineNumbers) gutter.style.display = "none"; + setGuttersForLineNumbers(); updateGutters(); // Needed to handle Tab key in KHTML if (khtml) inputDiv.style.height = "1px", inputDiv.style.position = "absolute"; @@ -79,7 +79,7 @@ window.CodeMirror = (function() { // Variables used by startOperation/endOperation to track what // happened during the operation. var updateInput, userSelChange, changes, textChanged, selectionChanged, leaveInputAlone, - gutterDirty, callbacks; + callbacks; // Current visible range (may be bigger than the view window). var displayOffset = 0, showingFrom = 0, showingTo = 0, lastSizeC = 0; // bracketHighlighted is used to remember that a bracket has been @@ -98,6 +98,7 @@ window.CodeMirror = (function() { // Register our event handlers. connect(scroller, "mousedown", operation(onMouseDown)); + connect(gutters, "mousedown", clickInGutter); connect(scroller, "dblclick", operation(onDoubleClick)); connect(lineSpace, "selectstart", e_preventDefault); // Gecko browsers fire contextmenu *after* opening the menu, at @@ -168,11 +169,10 @@ window.CodeMirror = (function() { else if (option == "lineWrapping" && oldVal != value) operation(wrappingChanged)(); else if (option == "tabSize") updateDisplay(true); else if (option == "keyMap") keyMapChanged(); - if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" || - option == "theme" || option == "lineNumberFormatter") { - gutterChanged(); - updateDisplay(true); - } + else if (option == "gutters" || option == "lineNumbers") setGuttersForLineNumbers(); + if (option == "lineNumbers" || option == "gutters" || option == "firstLineNumber" || + option == "theme" || option == "lineNumberFormatter") + guttersChanged(); }, getOption: function(option) {return options[option];}, undo: operation(undo), @@ -222,8 +222,8 @@ window.CodeMirror = (function() { markText: operation(markText), setBookmark: setBookmark, findMarksAt: findMarksAt, - setMarker: operation(addGutterMarker), - clearMarker: operation(removeGutterMarker), + setGutterMarker: operation(setGutterMarker), + clearGutter: operation(clearGutter), setLineClass: operation(setLineClass), hideLine: operation(function(h) {return setLineHidden(h, true);}), showLine: operation(function(h) {return setLineHidden(h, false);}), @@ -245,7 +245,7 @@ window.CodeMirror = (function() { if (vert == "over") top = pos.y; else if (vert == "near") { var vspace = Math.max(scroller.offsetHeight, doc.height * textHeight()), - hspace = Math.max(sizer.clientWidth, lineSpace.clientWidth) - paddingLeft(); + hspace = Math.max(sizer.clientWidth, lineSpace.clientWidth); if (pos.yBot + node.offsetHeight > vspace && pos.y > node.offsetHeight) top = pos.y - node.offsetHeight; if (left + node.offsetWidth > hspace) @@ -259,7 +259,7 @@ window.CodeMirror = (function() { } else { if (horiz == "left") left = 0; else if (horiz == "middle") left = (sizer.clientWidth - node.offsetWidth) / 2; - node.style.left = (left + paddingLeft()) + "px"; + node.style.left = left + "px"; } if (scroll) scrollIntoView(left, top, left + node.offsetWidth, top + node.offsetHeight); @@ -353,12 +353,11 @@ window.CodeMirror = (function() { getInputField: function(){return input;}, getWrapperElement: function(){return wrapper;}, getScrollerElement: function(){return scroller;}, - getGutterElement: function(){return gutter;} + getGutterElement: function(){return gutters;} }; function getLine(n) { return getLineAt(doc, n); } function updateLineHeight(line, height) { - gutterDirty = true; var diff = height - line.height; for (var n = line; n; n = n.parent) n.height += diff; } @@ -383,8 +382,6 @@ window.CodeMirror = (function() { } function onScrollMain(e) { - if (options.fixedGutter && gutter.style.left != scroller.scrollLeft + "px") - gutter.style.left = scroller.scrollLeft + "px"; if (scroller.scrollTop != lastScrollTop) { lastScrollTop = scroller.scrollTop; if (scrollbar.scrollTop != lastScrollTop) @@ -400,14 +397,7 @@ window.CodeMirror = (function() { for (var n = e_target(e); n != wrapper; n = n.parentNode) if (n.parentNode == sizer && n != mover) return; - // See if this is a click in the gutter - for (var n = e_target(e); n != wrapper; n = n.parentNode) - if (n.parentNode == gutterText) { - if (options.onGutterClick) - options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom, e); - return e_preventDefault(e); - } - + if (clickInGutter(e)) return; var start = posFromMouse(e); switch (e_button(e)) { @@ -512,8 +502,6 @@ window.CodeMirror = (function() { var up = connect(document, "mouseup", operation(done), true); } function onDoubleClick(e) { - for (var n = e_target(e); n != wrapper; n = n.parentNode) - if (n.parentNode == gutterText) return e_preventDefault(e); e_preventDefault(e); } function onDrop(e) { @@ -556,6 +544,30 @@ window.CodeMirror = (function() { catch(e){} } } + + function clickInGutter(e) { + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + + if (mX >= Math.floor(gutters.getBoundingClientRect().right)) return false; + if (options.onGutterClick) { + mY -= wrapper.getBoundingClientRect().top; + for (var i = 0; i < options.gutters.length; ++i) { + var g = gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + if (mY < lineDiv.offsetHeight) { + var line = lineAtHeight(doc, mY); + var gutter = options.gutters[i]; + setTimeout(function() {options.onGutterClick(instance, line, e, gutter);}, 20); + } + break; + } + } + } + e_preventDefault(e); + return true; + } + function onDragStart(e) { var txt = getSelection(); e.dataTransfer.setData("Text", txt); @@ -731,7 +743,6 @@ window.CodeMirror = (function() { doc.iter(from.line, to.line + 1, function(line) { if (!line.hidden && line.text.length == maxLineLength) {recomputeMaxLength = true; return true;} }); - if (from.line != to.line || newText.length > 1) gutterDirty = true; var nlines = to.line - from.line, firstLine = getLine(from.line), lastLine = getLine(to.line); // First adjust the line structure, taking some care to leave highlighting intact. @@ -847,6 +858,7 @@ window.CodeMirror = (function() { } } else { sizer.style.minHeight = ""; + sizer.style.minHeight = scroller.clientHeight + "px"; } // Position the mover div to align with the current virtual scroll position mover.style.top = displayOffset * textHeight() + "px"; @@ -994,8 +1006,8 @@ window.CodeMirror = (function() { if (scrollPos.scrollTop != null) {scrollbar.scrollTop = scroller.scrollTop = scrollPos.scrollTop;} } function calculateScrollPos(x1, y1, x2, y2) { - var pl = paddingLeft(), pt = paddingTop(); - y1 += pt; y2 += pt; x1 += pl; x2 += pl; + var pt = paddingTop(); + y1 += pt; y2 += pt; var screen = scroller.clientHeight, screentop = scrollbar.scrollTop, result = {}; var docBottom = needsScrollbar() || Infinity; var atTop = y1 < pt + 10, atBottom = y2 + pt > docBottom - 10; @@ -1003,8 +1015,8 @@ window.CodeMirror = (function() { else if (y2 > screentop + screen) result.scrollTop = (atBottom ? docBottom : y2) - screen; var screenw = scroller.clientWidth, screenleft = scroller.scrollLeft; - var gutterw = options.fixedGutter ? gutter.clientWidth : 0; - var atLeft = x1 < gutterw + pl + 10; + var gutterw = gutters.offsetWidth; + var atLeft = x1 < gutterw + 10; if (x1 < screenleft + gutterw || atLeft) { if (atLeft) x1 = 0; result.scrollLeft = Math.max(0, x1 - 10 - gutterw); @@ -1038,6 +1050,15 @@ window.CodeMirror = (function() { updateVerticalScroll(scrollTop); return; } + if (changes && changes !== true && maybeUpdateLineNumberWidth()) + changes = true; + mover.style.marginLeft = gutters.offsetWidth + "px"; + // Used to determine which lines need their line numbers updated + var positionsChangedFrom = changes === true ? 0 : Infinity; + if (options.lineNumbers && changes && changes !== true) + for (var i = 0; i < changes.length; ++i) + if (changes[i].diff) { positionsChangedFrom = changes[i].from; break; } + var from = Math.max(visible.from - 100, 0), to = Math.min(doc.size, visible.to + 100); if (showingFrom < from && from - showingFrom < 20) from = showingFrom; if (showingTo > to && showingTo - to < 20) to = Math.min(doc.size, showingTo); @@ -1061,10 +1082,10 @@ window.CodeMirror = (function() { } intact.sort(function(a, b) {return a.domStart - b.domStart;}); - var th = textHeight(), gutterDisplay = gutter.style.display; + var th = textHeight(); lineDiv.style.display = "none"; - patchDisplay(from, to, intact); - lineDiv.style.display = gutter.style.display = ""; + patchDisplay(from, to, intact, positionsChangedFrom); + lineDiv.style.display = ""; var different = from != showingFrom || to != showingTo || lastSizeC != scroller.clientHeight + th; // This is just a bogus formula that detects when the editor is @@ -1094,7 +1115,7 @@ window.CodeMirror = (function() { var height = Math.round(curNode.offsetHeight / th) || 1; if (line.height != height) { updateLineHeight(line, height); - gutterDirty = heightChanged = true; + heightChanged = true; } } curNode = curNode.nextSibling; @@ -1104,11 +1125,6 @@ window.CodeMirror = (function() { if (options.lineWrapping) checkHeights(); - gutter.style.display = gutterDisplay; - if (different || gutterDirty) { - // If the gutter grew in size, re-check heights. If those changed, re-draw gutter. - updateGutter() && options.lineWrapping && checkHeights() && updateGutter(); - } updateVerticalScroll(scrollTop); updateSelection(); if (!suppressCallback && options.onUpdate) options.onUpdate(instance); @@ -1138,12 +1154,17 @@ window.CodeMirror = (function() { return intact; } - function patchDisplay(from, to, intact) { + function lineNumberFor(i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)); + } + + function patchDisplay(from, to, intact, updateNumbersFrom) { function killNode(node) { var tmp = node.nextSibling; node.parentNode.removeChild(node); return tmp; } + var lineNumbers = options.lineNumbers; // The first pass removes the DOM nodes that aren't intact. if (!intact.length) removeChildren(lineDiv); else { @@ -1151,23 +1172,47 @@ window.CodeMirror = (function() { for (var i = 0; i < intact.length; ++i) { var cur = intact[i]; while (cur.domStart > domPos) {curNode = killNode(curNode); domPos++;} - for (var j = 0, e = cur.to - cur.from; j < e; ++j) {curNode = curNode.nextSibling; domPos++;} + for (var j = cur.from, e = cur.to; j < e; ++j) { + if (lineNumbers && updateNumbersFrom <= j && curNode.firstChild) + setTextContent(curNode.firstChild, lineNumberFor(j)); + curNode = curNode.nextSibling; domPos++; + } } while (curNode) curNode = killNode(curNode); } // This pass fills in the lines that actually changed. - var nextIntact = intact.shift(), curNode = lineDiv.firstChild, j = from; + var nextIntact = intact.shift(), curNode = lineDiv.firstChild, j = from, gutterSpecs = options.gutters; doc.iter(from, to, function(line) { if (nextIntact && nextIntact.to == j) nextIntact = intact.shift(); if (!nextIntact || nextIntact.from > j) { - if (line.hidden) var lineElement = elt("pre"); + if (line.hidden) var lineElement = elt("div"); else { - var lineElement = line.getElement(makeTab); + var lineElement = line.getElement(makeTab), markers = line.gutterMarkers; if (line.className) lineElement.className = line.className; - // Kludge to make sure the styled element lies behind the selection (by z-index) - if (line.bgClassName) { - var pre = elt("pre", "\u00a0", line.bgClassName, "position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: -2"); - lineElement = elt("div", [pre, lineElement], null, "position: relative"); + // Lines with gutter elements or a background class need + // to be wrapped again, and have the extra elements added + // to the wrapper div + if (lineNumbers || markers || line.bgClassName) { + var inside = []; + if (lineNumbers) + inside.push(elt("div", lineNumberFor(j), + "CodeMirror-linenumber CodeMirror-gutter-elt", + "left: " + (lineGutter.offsetLeft - gutters.offsetWidth) + "px; width: " + + currentLineNumberWidth + "px")); + if (markers) + for (var k = 0; k < gutterSpecs.length; ++k) { + var id = gutterSpecs[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) { + var gutterElt = gutters.childNodes[k]; + inside.push(elt("div", [found], "CodeMirror-gutter-elt", "left: " + (gutterElt.offsetLeft - gutters.offsetWidth) + + "px; width: " + gutterElt.clientWidth + "px")); + } + } + // Kludge to make sure the styled element lies behind the selection (by z-index) + if (line.bgClassName) + inside.push(elt("pre", "\u00a0", line.bgClassName, "position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: -2")); + inside.push(lineElement); + lineElement = elt("div", inside, null, "position: relative"); } } lineDiv.insertBefore(lineElement, curNode); @@ -1178,46 +1223,48 @@ window.CodeMirror = (function() { }); } - function updateGutter() { - if (!options.gutter && !options.lineNumbers) return; - var hText = mover.offsetHeight, hEditor = scroller.clientHeight; - gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px"; - var fragment = document.createDocumentFragment(), i = showingFrom, normalNode; - doc.iter(showingFrom, Math.max(showingTo, showingFrom + 1), function(line) { - if (line.hidden) { - fragment.appendChild(elt("pre")); - } else { - var marker = line.gutterMarker; - var text = options.lineNumbers ? options.lineNumberFormatter(i + options.firstLineNumber) : null; - if (marker && marker.text) - text = marker.text.replace("%N%", text != null ? text : ""); - else if (text == null) - text = "\u00a0"; - var markerElement = fragment.appendChild(elt("pre", null, marker && marker.style)); - markerElement.innerHTML = text; - for (var j = 1; j < line.height; ++j) { - markerElement.appendChild(elt("br")); - markerElement.appendChild(document.createTextNode("\u00a0")); - } - if (!marker) normalNode = i; + var currentLineNumberWidth, currentLineNumberChars; + function maybeUpdateLineNumberWidth() { + if (!options.lineNumbers) return false; + var last = lineNumberFor(doc.size - 1); + if (last.length != currentLineNumberChars) { + var test = measure.appendChild(elt("div", last, "CodeMirror-linenumber CodeMirror-gutter-elt")); + currentLineNumberWidth = test.clientWidth; + currentLineNumberChars = currentLineNumberWidth ? last.length : -1; + lineGutter.style.width = currentLineNumberWidth + "px"; + return true; + } + return false; + } + + function updateGutters() { + removeChildren(gutters); + for (var i = 0; i < options.gutters.length; ++i) { + var gutterClass = options.gutters[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + lineGutter = gElt; + gElt.style.width = currentLineNumberWidth + "px"; } - ++i; - }); - gutter.style.display = "none"; - removeChildrenAndAdd(gutterText, fragment); - // Make sure scrolling doesn't cause number gutter size to pop - if (normalNode != null && options.lineNumbers) { - var node = gutterText.childNodes[normalNode - showingFrom]; - var minwidth = String(doc.size).length, val = eltText(node.firstChild), pad = ""; - while (val.length + pad.length < minwidth) pad += "\u00a0"; - if (pad) node.insertBefore(document.createTextNode(pad), node.firstChild); - } - gutter.style.display = ""; - var resized = Math.abs((parseInt(lineSpace.style.marginLeft) || 0) - gutter.offsetWidth) > 2; - lineSpace.style.marginLeft = gutter.offsetWidth + "px"; - gutterDirty = false; - return resized; + } + gutters.style.display = i ? "" : "none"; + } + function guttersChanged() { + updateGutters(); + updateDisplay(true); } + function setGuttersForLineNumbers() { + var found = false; + for (var i = 0; i < options.gutters.length; ++i) { + if (options.gutters[i] == "CodeMirror-linenumbers") { + if (options.lineNumbers) found = true; + else options.gutters.splice(i--, 1); + } + } + if (!found && options.lineNumbers) + options.gutters.push("CodeMirror-linenumbers"); + } + function updateSelection() { var collapsed = posEq(sel.from, sel.to); var fromPos = localCoords(sel.from, true); @@ -1248,9 +1295,9 @@ window.CodeMirror = (function() { var middleStart = Math.max(0, fromPos.y + (sel.from.ch ? th : 0)); var middleHeight = Math.min(toPos.y, clientHeight) - middleStart; if (middleHeight > 0.2 * th) - add(0, middleStart, 0, middleHeight); - if ((!sameLine || !sel.from.ch) && toPos.y < clientHeight - .5 * th) - add(0, toPos.y, clientWidth - toPos.x, th); + add(paddingLeft(), middleStart, 0, middleHeight); + if ((!sameLine || !sel.from.ch) && sel.to.ch && toPos.y < clientHeight - .5 * th) + add(paddingLeft(), toPos.y, clientWidth - toPos.x, th); removeChildrenAndAdd(selectionDiv, fragment); cursor.style.display = "none"; selectionDiv.style.display = ""; @@ -1458,12 +1505,6 @@ window.CodeMirror = (function() { work = [0]; startWorker(); } - function gutterChanged() { - var visible = options.gutter || options.lineNumbers; - gutter.style.display = visible ? "" : "none"; - if (visible) gutterDirty = true; - else lineDiv.parentNode.style.marginLeft = 0; - } function wrappingChanged(from, to) { if (options.lineWrapping) { wrapper.className += " CodeMirror-wrap"; @@ -1572,16 +1613,29 @@ window.CodeMirror = (function() { return markers; } - function addGutterMarker(line, text, className) { - if (typeof line == "number") line = getLine(clipLine(line)); - line.gutterMarker = {text: text, style: className}; - gutterDirty = true; - return line; + function isEmpty(obj) { + var c = 0; + for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) ++c; + return !c; } - function removeGutterMarker(line) { - if (typeof line == "number") line = getLine(clipLine(line)); - line.gutterMarker = null; - gutterDirty = true; + function setGutterMarker(line, gutterID, value) { + return changeLine(line, function(line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) line.gutterMarkers = null; + return true; + }); + } + function clearGutter(gutterID) { + var i = 0; + doc.iter(0, doc.size, function(line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + line.gutterMarkers[gutterID] = null; + changes.push({from: i, to: i + 1}); + if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; + } + ++i; + }); } function changeLine(handle, op) { @@ -1622,7 +1676,7 @@ window.CodeMirror = (function() { if (!to) return; setSelection(from, to); } - return (gutterDirty = true); + return true; } }); } @@ -1637,13 +1691,18 @@ window.CodeMirror = (function() { var n = lineNo(line); if (n == null) return null; } - var marker = line.gutterMarker; - return {line: n, handle: line, text: line.text, markerText: marker && marker.text, - markerClass: marker && marker.style, lineClass: line.className, bgClass: line.bgClassName}; + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + lineClass: line.className, bgClass: line.bgClassName}; + } + + function paddingLeft() { + var e = removeChildrenAndAdd(measure, elt("pre")).appendChild(elt("span", "x")); + return e.offsetLeft; } function measureLine(line, ch) { - if (ch == 0) return {top: 0, left: 0}; + if (ch == 0) return {top: 0, left: paddingLeft()}; + var wbr = options.lineWrapping && ch < line.text.length && spanAffectsWrapping.test(line.text.slice(ch - 1, ch + 1)); var pre = line.getElement(makeTab, ch, wbr); @@ -1660,12 +1719,9 @@ window.CodeMirror = (function() { } function localCoords(pos, inLineWrap) { var x, lh = textHeight(), y = lh * (heightAtLine(doc, pos.line) - (inLineWrap ? displayOffset : 0)); - if (pos.ch == 0) x = 0; - else { - var sp = measureLine(getLine(pos.line), pos.ch); - x = sp.left; - if (options.lineWrapping) y += Math.max(0, sp.top); - } + var sp = measureLine(getLine(pos.line), pos.ch); + x = sp.left; + if (options.lineWrapping) y += Math.max(0, sp.top); return {x: x, y: y, yBot: y + lh}; } // Coords must be lineSpace-local @@ -1743,7 +1799,6 @@ window.CodeMirror = (function() { return (cachedWidth = anchor.offsetWidth || 10); } function paddingTop() {return lineSpace.offsetTop;} - function paddingLeft() {return lineSpace.offsetLeft;} function posFromMouse(e, liberal) { var offW = eltOffset(scroller, true), x, y; @@ -1766,7 +1821,7 @@ window.CodeMirror = (function() { var oldCSS = input.style.cssText; inputDiv.style.position = "absolute"; input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + - "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; outline: none;" + "border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; leaveInputAlone = true; var val = input.value = getSelection(); @@ -1957,7 +2012,6 @@ window.CodeMirror = (function() { updated = updateDisplay(changes, true, newScrollPos && newScrollPos.scrollTop); if (!updated) { if (selectionChanged) updateSelection(); - if (gutterDirty) updateGutter(); } if (newScrollPos) scrollCursorIntoView(); if (selectionChanged) restartBlink(); @@ -2018,7 +2072,7 @@ window.CodeMirror = (function() { onDragEvent: null, lineWrapping: false, lineNumbers: false, - gutter: false, + gutters: [], fixedGutter: false, firstLineNumber: 1, readOnly: false, @@ -3107,7 +3161,7 @@ window.CodeMirror = (function() { return e; } function removeChildrenAndAdd(parent, e) { - removeChildren(parent).appendChild(e); + return removeChildren(parent).appendChild(e); } function setTextContent(e, str) { if (ie_lt9) { diff --git a/lib/util/foldcode.js b/lib/util/foldcode.js index 02cfb50ab7..cbe1b353b5 100644 --- a/lib/util/foldcode.js +++ b/lib/util/foldcode.js @@ -156,7 +156,7 @@ CodeMirror.indentRangeFinder = function(cm, line) { CodeMirror.newFoldFunction = function(rangeFinder, markText, hideEnd) { var folded = []; - if (markText == null) markText = '
%N%'; + if (markText == null) markText = "\u25bc"; function isFolded(cm, n) { for (var i = 0; i < folded.length; ++i) { @@ -167,7 +167,7 @@ CodeMirror.newFoldFunction = function(rangeFinder, markText, hideEnd) { } function expand(cm, region) { - cm.clearMarker(region.start); + cm.setGutterMarker(region.start, "CodeMirror-folded", null); for (var i = 0; i < region.hidden.length; ++i) cm.showLine(region.hidden[i]); } @@ -186,7 +186,10 @@ CodeMirror.newFoldFunction = function(rangeFinder, markText, hideEnd) { var handle = cm.hideLine(i); if (handle) hidden.push(handle); } - var first = cm.setMarker(line, markText); + var elt = document.createElement("div"); + elt.className = "CodeMirror-foldmarker"; + elt.innerHTML = markText; + var first = cm.setGutterMarker(line, "CodeMirror-folded", elt); var region = {start: first, hidden: hidden}; cm.onDeleteLine(first, function() { expand(cm, region); }); folded.push(region); diff --git a/test/test.js b/test/test.js index 2c8e398c83..4ba5d5de71 100644 --- a/test/test.js +++ b/test/test.js @@ -132,15 +132,20 @@ test("defaults", function() { testCM("lineInfo", function(cm) { eq(cm.lineInfo(-1), null); - var lh = cm.setMarker(1, "FOO", "bar"); + var mark = document.createElement("span"); + var lh = cm.setGutterMarker(1, "FOO", mark); var info = cm.lineInfo(1); eq(info.text, "222222"); - eq(info.markerText, "FOO"); - eq(info.markerClass, "bar"); + eq(info.gutterMarkers.FOO, mark); eq(info.line, 1); - eq(cm.lineInfo(2).markerText, null); - cm.clearMarker(lh); - eq(cm.lineInfo(1).markerText, null); + eq(cm.lineInfo(2).gutterMarkers, null); + cm.setGutterMarker(lh, "FOO", null); + eq(cm.lineInfo(1).gutterMarkers, null); + cm.setGutterMarker(1, "FOO", mark); + cm.setGutterMarker(0, "FOO", mark); + cm.clearGutter("FOO"); + eq(cm.lineInfo(0).gutterMarkers, null); + eq(cm.lineInfo(1).gutterMarkers, null); }, {value: "111111\n222222\n333333"}); testCM("coords", function(cm) { @@ -458,7 +463,7 @@ testCM("wrappingAndResizing", function(cm) { cm.setCursor({line: 0, ch: doc.length}); eq(wrap.offsetHeight, h0); cm.replaceSelection("x"); - is(wrap.offsetHeight > h0); + is(wrap.offsetHeight > h0, "wrapping happens"); // Now add a max-height and, in a document consisting of // almost-wrapped lines, go over it so that a scrollbar appears. cm.setValue(doc + "\n" + doc + "\n");