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");