diff --git a/doc/manual.html b/doc/manual.html
index 6b4dc8c7b5..3851b80ff3 100644
--- a/doc/manual.html
+++ b/doc/manual.html
@@ -11,10 +11,13 @@
-
-
/* User manual and
+
+

+
+/* User manual and
reference guide */
+
@@ -249,11 +252,6 @@
Configuration
When given, will be called whenever the editor is
scrolled.
-
onHighlightComplete (function)
-
Whenever the editor's content has been fully highlighted,
- this function (if given) will be called. It'll be given a single
- argument, the editor instance.
-
onUpdate (function)
Will be called whenever CodeMirror updates its DOM display.
@@ -524,6 +522,10 @@
Programming API
Get an {x, y, width, height} object that
represents the current scroll position and scrollable area size
of the editor.
+
scrollIntoView(pos)
+
Scrolls the given {line, ch} position into
+ view. If no argument is given, this will scroll the cursor into
+ view.
setOption(option, value)
Change the configuration of the editor. option
@@ -533,6 +535,11 @@ Programming API
getOption(option) → value
Retrieves the current value of the given option for this
editor instance.
+
getMode() → object
+
Gets the mode object for the editor. Note that this is
+ distinct from getOption("mode"), which gives you
+ the mode specification, rather than the resolved, instantiated
+ mode object.
cursorCoords(start, mode) → object
Returns an {x, y, yBot} object containing the
@@ -551,6 +558,8 @@ Programming API
Given an {x, y} object (in page coordinates),
returns the {line, ch} position that corresponds to
it.
+ defaultTextHeight() → number
+
Returns the line height of the default font for the editor.
undo()
Undo one edit (if any undo events are stored).
@@ -592,14 +601,30 @@
Programming API
stateThe mode's state at the end of this token.
-
markText(from, to, className) → object
+
markText(from, to, className, options) → object
Can be used to mark a range of text with a specific CSS
class name. from and to should
- be {line, ch} objects. The method will return an
- object with two methods, clear(), which removes the
- mark, and find(), which returns a {from,
- to} (both document positions), indicating the current
- position of the marked range.
+ be
{line, ch} objects. The
options
+ parameter is optional. When given, it should be an object that
+ may contain the following configuration options:
+
+ inclusiveLeft- Determines whether
+ text inserted on the left of the marker will end up inside
+ or outside of it.
+ inclusiveRight- Like
inclusiveLeft,
+ but for the right side.
+ startStyle- Can be used to specify
+ an extra CSS class to be applied to the leftmost span that
+ is part of the marker.
+ endStyle- Equivalent
+ to
startStyle, but for the rightmost span.
+
+ The method will return an object with two methods,
+
clear(), which removes the mark,
+ and
find(), which returns a
{from, to}
+ (both document positions), indicating the current position of
+ the marked range, or
undefined if the marker is no
+ longer in the document.
setBookmark(pos) → object
Inserts a bookmark, a handle that follows the text around it
@@ -782,13 +807,23 @@ Programming API
contextual information for a line.
-
Finally, the CodeMirror object
- itself has a method fromTextArea. This takes a
+
The CodeMirror object itself provides
+ several useful properties. Firstly, its version
+ property contains a string that indicates the version of the
+ library. For releases, this simply
+ contains "major.minor" (for
+ example "2.33". For beta versions, " B"
+ (space, capital B) is added at the end of the string, for
+ development snapshots, " +" (space, plus) is
+ added.
+
+
The CodeMirror.fromTextArea
+ method provides another way to initialize an editor. It takes a
textarea DOM node as first argument and an optional configuration
object as second. It will replace the textarea with a CodeMirror
instance, and wire up the form of that textarea (if any) to make
sure the editor contents are put into the textarea when the form
- is submitted. A CodeMirror instance created this way has two
+ is submitted. A CodeMirror instance created this way has three
additional methods:
@@ -809,6 +844,13 @@ Programming API
will cause the given value (usually a method) to be added to all
CodeMirror instances created from then on.
+ If your extention just needs to run some
+ code whenever a CodeMirror instance is initialized,
+ use CodeMirror.defineInitHook. Give it a function as
+ its only argument, and from then on, that function will be called
+ (with the instance as argument) whenever a new CodeMirror instance
+ is initialized.
+
Add-ons
The lib/util directory in the distribution
@@ -930,6 +972,11 @@
Add-ons
Depends on
the searchcursor
add-on. Demo here.
+ formatting.js
+ - Adds
commentRange, autoIndentRange,
+ and autoFormatRange methods that, respectively,
+ comment (or uncomment), indent, or format (add line breaks) a
+ range of code. Demo here.
closetag.js
- Provides utility functions for adding automatic tag closing
to XML modes. See
@@ -1085,20 +1132,8 @@
Writing CodeMirror Modes
which is given a state and should return a safe copy of that
state.
- By default, CodeMirror will stop re-parsing
- a document as soon as it encounters a few lines that were
- highlighted the same in the old parse as in the new one. It is
- possible to provide an explicit way to test whether a state is
- equivalent to another one, which CodeMirror will use (instead of
- the unchanged-lines heuristic) to decide when to stop
- highlighting. You do this by providing
- a compareStates method on your mode object, which
- takes two state arguments and returns a boolean indicating whether
- they are equivalent. See the XML mode, which uses this to provide
- reliable highlighting of bad closing tags, as an example.
-
If you want your mode to provide smart indentation
- (though the indentLine
+ (through the indentLine
method and the indentAuto
and newlineAndIndent commands, which keys can be
bound to), you must define
@@ -1142,6 +1177,16 @@
Writing CodeMirror Modes
where mode is the mode that created the given
state.
+ In a nested mode, it is recommended to add an
+ extra methods, innerMode which, given a state object,
+ returns a {state, mode} object with the inner mode
+ and its state for the current position. These are used by utility
+ scripts such as the autoformatter
+ and the tag closer to get context
+ information. Use the CodeMirror.innerMode helper
+ function to, starting from a mode and a state, recursively walk
+ down to the innermost mode and state.
+
To make indentation work properly in a nested parser, it is
advisable to give the startState method of modes that
are intended to be nested an optional argument that provides the
@@ -1160,6 +1205,15 @@
Writing CodeMirror Modes
mode, as in the mode
option.
+ Sometimes, it is useful to add or override mode
+ object properties from external code.
+ The CodeMirror.extendMode can be used to add
+ properties to mode objects produced for a specific mode. Its first
+ argument is the name of the mode, its second an object that
+ specifies the properties that should be added. This is mostly
+ useful to add utilities that can later be looked
+ up getMode.
+
Contents
diff --git a/doc/oldrelease.html b/doc/oldrelease.html
index e873d7a7b1..27645f07ef 100644
--- a/doc/oldrelease.html
+++ b/doc/oldrelease.html
@@ -11,10 +11,87 @@
-
-
/* Old release history */
-
+
+

+
+/* Old release
+ history */
+
+
+ 27-02-2012: Version 2.22:
+
+
+
+
27-01-2012: Version 2.21:
+
+
+ - Added LESS, MySQL,
+ Go, and Verilog modes.
+ - Add
smartIndent
+ option.
+ - Support a cursor in
readOnly-mode.
+ - Support assigning multiple styles to a token.
+ - Use a new approach to drawing the selection.
+ - Add
scrollTo method.
+ - Allow undo/redo events to span non-adjacent lines.
+ - Lots and lots of bugfixes.
+
+
+
20-12-2011: Version 2.2:
+
+
+
+
21-11-2011: Version 2.18:
+
Fixes TextMarker.clear, which is broken in 2.17.
+
+
21-11-2011: Version 2.17:
+
+ - Add support for line
+ wrapping and code
+ folding.
+ - Add Github-style Markdown mode.
+ - Add Monokai
+ and Rubyblue themes.
+ - Add
setBookmark method.
+ - Move some of the demo code into reusable components
+ under
lib/util.
+ - Make screen-coord-finding code faster and more reliable.
+ - Fix drag-and-drop in Firefox.
+ - Improve support for IME.
+ - Speed up content rendering.
+ - Fix browser's built-in search in Webkit.
+ - Make double- and triple-click work in IE.
+ - Various fixes to modes.
+
27-10-2011: Version 2.16:
diff --git a/doc/realworld.html b/doc/realworld.html
new file mode 100644
index 0000000000..d58a4f73b3
--- /dev/null
+++ b/doc/realworld.html
@@ -0,0 +1,77 @@
+
+
+
+
+ CodeMirror: Real-world uses
+
+
+
+
+
+
+
+
+

+
+/* Real world uses,
+ full list */
+
+
+
+ Contact me if you'd like
+ your project to be added to this list.
+
+
+
+
+
diff --git a/doc/reporting.html b/doc/reporting.html
index 221e2629fb..a616512530 100644
--- a/doc/reporting.html
+++ b/doc/reporting.html
@@ -11,10 +11,13 @@
-
-
/* Reporting bugs
+
+

+
+/* Reporting bugs
effectively */
+
diff --git a/doc/upgrade_v2.2.html b/doc/upgrade_v2.2.html
index 7210355196..7e4d840043 100644
--- a/doc/upgrade_v2.2.html
+++ b/doc/upgrade_v2.2.html
@@ -10,10 +10,13 @@
-
-
/* Upgrading to v2.2
- */
+
+

+
+/* Upgrading to
+ v2.2 */
+
diff --git a/index.html b/index.html
index 6d074dd334..aa672a15f3 100644
--- a/index.html
+++ b/index.html
@@ -5,16 +5,19 @@
CodeMirror
-
+
-
-
/* In-browser code editing
+
+

+
+/* In-browser code editing
made bearable */
+
@@ -252,9 +257,10 @@ Support CodeMirror
@@ -276,10 +282,70 @@
Reading material
Releases
+
22-10-2012: Version 2.35:
+
+
+ - New (sub) mode: TypeScript.
+ - Don't overwrite (insert key) when pasting.
+ - Fix several bugs in
markText/undo interaction.
+ - Better indentation of JavaScript code without semicolons.
+ - Add
defineInitHook function.
+ - Full list of patches.
+
+
+
22-10-2012: Version 3.0, beta 2:
+
+
BETA release, new major version. Only partially
+ backwards-compatible. See
+ the upgrading
+ guide for more information. Changes since beta1:
+
+
+ - Fix page-based coordinate computation.
+ - Fix firing of
gutterClick event.
+ - Add
cursorHeight option.
+ - Fix bi-directional text regression.
+ - Add
viewportMargin option.
+ - Directly handle mousewheel events (again, hopefully better).
+ - Make vertical cursor movement more robust (through widgets, big line gaps).
+ - Add
flattenSpans option.
+ - Initialization in hidden state works again.
+ - Many optimizations. Poor responsiveness should be fixed.
+ - Full list of patches.
+
+
+
19-09-2012: Version 2.34:
+
+
+ - New mode: Common Lisp.
+ - Fix right-click select-all on most browsers.
+ - Change the way highlighting happens:
Saves memory and CPU cycles.
compareStates is no longer needed.
onHighlightComplete no longer works.
+ - Integrate mode (Markdown, XQuery, CSS, sTex) tests in central testsuite.
+ - Add a
CodeMirror.version property.
+ - More robust handling of nested modes in formatting and closetag plug-ins.
+ - Un/redo now preserves marked text and bookmarks.
+ - Full list of patches.
+
+
+
19-09-2012: Version 3.0, beta 1:
+
+
BETA release, new major version. Only partially
+ backwards-compatible. See
+ the upgrading
+ guide for more information. Major new features are:
+
+
+ - Bi-directional text support.
+ - More powerful gutter model.
+ - Support for arbitrary text/widget height.
+ - In-line widgets.
+ - Generalized event handling.
+
+
23-08-2012: Version 2.33:
-
27-02-2012: Version 2.22:
-
-
-
-
27-01-2012: Version 2.21:
-
-
- - Added LESS, MySQL,
- Go, and Verilog modes.
- - Add
smartIndent
- option.
- - Support a cursor in
readOnly-mode.
- - Support assigning multiple styles to a token.
- - Use a new approach to drawing the selection.
- - Add
scrollTo method.
- - Allow undo/redo events to span non-adjacent lines.
- - Lots and lots of bugfixes.
-
-
-
20-12-2011: Version 2.2:
-
-
-
-
21-11-2011: Version 2.18:
-
Fixes TextMarker.clear, which is broken in 2.17.
-
-
21-11-2011: Version 2.17:
-
- - Add support for line
- wrapping and code
- folding.
- - Add Github-style Markdown mode.
- - Add Monokai
- and Rubyblue themes.
- - Add
setBookmark method.
- - Move some of the demo code into reusable components
- under
lib/util.
- - Make screen-coord-finding code faster and more reliable.
- - Fix drag-and-drop in Firefox.
- - Improve support for IME.
- - Speed up content rendering.
- - Fix browser's built-in search in Webkit.
- - Make double- and triple-click work in IE.
- - Various fixes to modes.
-
-
Older releases...
diff --git a/keymap/emacs.js b/keymap/emacs.js
index 2a57e2ffe9..fab3ab9fe6 100644
--- a/keymap/emacs.js
+++ b/keymap/emacs.js
@@ -18,7 +18,8 @@
"Alt-Y": function(cm) {cm.replaceSelection(popFromRing());},
"Ctrl-/": "undo", "Shift-Ctrl--": "undo", "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd",
"Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": "clearSearch", "Shift-Alt-5": "replace",
- "Ctrl-Z": "undo", "Cmd-Z": "undo", "Alt-/": "autocomplete",
+ "Ctrl-Z": "undo", "Cmd-Z": "undo", "Alt-/": "autocomplete", "Alt-V": "goPageUp",
+ "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto",
fallthrough: ["basic", "emacsy"]
};
diff --git a/keymap/vim.js b/keymap/vim.js
index 73a4a0a771..10eb590b15 100644
--- a/keymap/vim.js
+++ b/keymap/vim.js
@@ -17,7 +17,7 @@
// Entering insert mode:
// i, I, a, A, o, O
// s
-// ce, cb (without support for number of actions like c3e - TODO)
+// ce, cb
// cc
// S, C TODO
// cf, cF, ct, cT
@@ -26,7 +26,7 @@
// x, X
// J
// dd, D
-// de, db (without support for number of actions like d3e - TODO)
+// de, db
// df, dF, dt, dT
//
// Yanking and pasting:
@@ -48,22 +48,28 @@
//
(function() {
- var count = "";
var sdir = "f";
var buf = "";
- var yank = 0;
- var mark = [];
- var reptTimes = 0;
+ var mark = {};
+ var repeatCount = 0;
+ function isLine(cm, line) { return line >= 0 && line < cm.lineCount(); }
function emptyBuffer() { buf = ""; }
function pushInBuffer(str) { buf += str; }
- function pushCountDigit(digit) { return function(cm) {count += digit;}; }
- function popCount() { var i = parseInt(count, 10); count = ""; return i || 1; }
+ function pushRepeatCountDigit(digit) {return function(cm) {repeatCount = (repeatCount * 10) + digit}; }
+ function getCountOrOne() {
+ var i = repeatCount;
+ return i || 1;
+ }
+ function clearCount() {
+ repeatCount = 0;
+ }
function iterTimes(func) {
- for (var i = 0, c = popCount(); i < c; ++i) func(i, i == c - 1);
+ for (var i = 0, c = getCountOrOne(); i < c; ++i) func(i, i == c - 1);
+ clearCount();
}
function countTimes(func) {
if (typeof func == "string") func = CodeMirror.commands[func];
- return function(cm) { iterTimes(function () { func(cm); }); };
+ return function(cm) { iterTimes(function (i, last) { func(cm, i, last); }); };
}
function iterObj(o, f) {
@@ -93,50 +99,110 @@
}
var word = [/\w/, /[^\w\s]/], bigWord = [/\S/];
- function findWord(line, pos, dir, regexps) {
- var stop = 0, next = -1;
- if (dir > 0) { stop = line.length; next = 0; }
- var start = stop, end = stop;
- // Find bounds of next one.
- outer: for (; pos != stop; pos += dir) {
- for (var i = 0; i < regexps.length; ++i) {
- if (regexps[i].test(line.charAt(pos + next))) {
- start = pos;
- for (; pos != stop; pos += dir) {
- if (!regexps[i].test(line.charAt(pos + next))) break;
+ // Finds a word on the given line, and continue searching the next line if it can't find one.
+ function findWord(cm, lineNum, pos, dir, regexps) {
+ var line = cm.getLine(lineNum);
+ while (true) {
+ var stop = (dir > 0) ? line.length : -1;
+ var wordStart = stop, wordEnd = stop;
+ // Find bounds of next word.
+ for (; pos != stop; pos += dir) {
+ for (var i = 0; i < regexps.length; ++i) {
+ if (regexps[i].test(line.charAt(pos))) {
+ wordStart = pos;
+ // Advance to end of word.
+ for (; pos != stop && regexps[i].test(line.charAt(pos)); pos += dir) {}
+ wordEnd = (dir > 0) ? pos : pos + 1;
+ return {
+ from: Math.min(wordStart, wordEnd),
+ to: Math.max(wordStart, wordEnd),
+ line: lineNum};
}
- end = pos;
- break outer;
}
}
+ // Advance to next/prev line.
+ lineNum += dir;
+ if (!isLine(cm, lineNum)) return null;
+ line = cm.getLine(lineNum);
+ pos = (dir > 0) ? 0 : line.length;
}
- return {from: Math.min(start, end), to: Math.max(start, end)};
}
- function moveToWord(cm, regexps, dir, times, where) {
+ /**
+ * @param {boolean} cm CodeMirror object.
+ * @param {regexp} regexps Regular expressions for word characters.
+ * @param {number} dir Direction, +/- 1.
+ * @param {number} times Number of times to advance word.
+ * @param {string} where Go to "start" or "end" of word, 'e' vs 'w'.
+ * @param {boolean} yank Whether we are finding words to yank. If true,
+ * do not go to the next line to look for the last word. This is to
+ * prevent deleting new line on 'dw' at the end of a line.
+ */
+ function moveToWord(cm, regexps, dir, times, where, yank) {
var cur = cm.getCursor();
-
+ if (yank) {
+ where = 'start';
+ }
for (var i = 0; i < times; i++) {
- var line = cm.getLine(cur.line), startCh = cur.ch, word;
+ var startCh = cur.ch, startLine = cur.line, word;
while (true) {
- // If we're at start/end of line, start on prev/next respectivly
- if (cur.ch == line.length && dir > 0) {
- cur.line++;
- cur.ch = 0;
- line = cm.getLine(cur.line);
- } else if (cur.ch == 0 && dir < 0) {
- cur.line--;
- cur.ch = line.length;
- line = cm.getLine(cur.line);
+ // Search and advance.
+ word = findWord(cm, cur.line, cur.ch, dir, regexps);
+ if (word) {
+ if (yank && times == 1 && dir == 1 && cur.line != word.line) {
+ // Stop at end of line of last word. Don't want to delete line return
+ // for dw if the last deleted word is at the end of a line.
+ cur.ch = cm.getLine(cur.line).length;
+ break;
+ } else {
+ // Move to the word we just found. If by moving to the word we end up
+ // in the same spot, then move an extra character and search again.
+ cur.line = word.line;
+ if (dir > 0 && where == 'end') {
+ // 'e'
+ if (startCh != word.to - 1 || startLine != word.line) {
+ cur.ch = word.to - 1;
+ break;
+ } else {
+ cur.ch = word.to;
+ }
+ } else if (dir > 0 && where == 'start') {
+ // 'w'
+ if (startCh != word.from || startLine != word.line) {
+ cur.ch = word.from;
+ break;
+ } else {
+ cur.ch = word.to;
+ }
+ } else if (dir < 0 && where == 'end') {
+ // 'ge'
+ if (startCh != word.to || startLine != word.line) {
+ cur.ch = word.to;
+ break;
+ } else {
+ cur.ch = word.from - 1;
+ }
+ } else if (dir < 0 && where == 'start') {
+ // 'b'
+ if (startCh != word.from || startLine != word.line) {
+ cur.ch = word.from;
+ break;
+ } else {
+ cur.ch = word.from - 1;
+ }
+ }
+ }
+ } else {
+ // No more words to be found. Move to end of document.
+ for (; isLine(cm, cur.line + dir); cur.line += dir) {}
+ cur.ch = (dir > 0) ? cm.getLine(cur.line).length : 0;
+ break;
}
- if (!line) break;
-
- // On to the actual searching
- word = findWord(line, cur.ch, dir, regexps);
- cur.ch = word[where == "end" ? "to" : "from"];
- if (startCh == cur.ch && word.from != word.to) cur.ch = word[dir < 0 ? "from" : "to"];
- else break;
}
}
+ if (where == 'end' && yank) {
+ // Include the last character of the word for actions.
+ cur.ch++;
+ }
return cur;
}
function joinLineNext(cm) {
@@ -157,7 +223,7 @@
var l = cm.getCursor().line, start = i > l ? l : i, end = i > l ? i : l;
cm.setCursor(start);
for (var c = start; c <= end; c++) {
- pushInBuffer("\n"+cm.getLine(start));
+ pushInBuffer("\n" + cm.getLine(start));
cm.removeLine(start);
}
}
@@ -169,7 +235,7 @@
}
var l = cm.getCursor().line, start = i > l ? l : i, end = i > l ? i : l;
for (var c = start; c <= end; c++) {
- pushInBuffer("\n"+cm.getLine(c));
+ pushInBuffer("\n" + cm.getLine(c));
}
cm.setCursor(start);
}
@@ -220,7 +286,7 @@
function enterInsertMode(cm) {
// enter insert mode: switch mode and cursor
- popCount();
+ clearCount();
cm.setOption("keyMap", "vim-insert");
}
@@ -238,7 +304,8 @@
var map = CodeMirror.keyMap.vim = {
// Pipe (|); TODO: should be *screen* chars, so need a util function to turn tabs into spaces?
"'|'": function(cm) {
- cm.setCursor(cm.getCursor().line, popCount() - 1, true);
+ cm.setCursor(cm.getCursor().line, getCountOrOne() - 1, true);
+ clearCount();
},
"A": function(cm) {
cm.setCursor(cm.getCursor().line, cm.getCursor().ch+1, true);
@@ -260,16 +327,26 @@
},
"G": function(cm) { cm.setOption("keyMap", "vim-prefix-g");},
"Shift-D": function(cm) {
- // commented out verions works, but I left original, cause maybe
- // I don't know vim enouth to see what it does
- /* var cur = cm.getCursor();
- var f = {line: cur.line, ch: cur.ch}, t = {line: cur.line};
- pushInBuffer(cm.getRange(f, t));
- */
+ var cursor = cm.getCursor();
+ var lineN = cursor.line;
+ var line = cm.getLine(lineN);
+ cm.setLine(lineN, line.slice(0, cursor.ch));
+
emptyBuffer();
- mark["Shift-D"] = cm.getCursor(false).line;
- cm.setCursor(cm.getCursor(true).line);
- delTillMark(cm,"Shift-D"); mark = [];
+ pushInBuffer(line.slice(cursor.ch));
+
+ if (repeatCount > 1) {
+ // we've already done it once
+ --repeatCount;
+ // the lines dissapear (ie, cursor stays on the same lineN),
+ // so only incremenet once
+ ++lineN;
+
+ iterTimes(function() {
+ pushInBuffer(cm.getLine(lineN));
+ cm.removeLine(lineN);
+ });
+ }
},
"S": function (cm) {
@@ -278,13 +355,11 @@
})(cm);
enterInsertMode(cm);
},
- "M": function(cm) {cm.setOption("keyMap", "vim-prefix-m"); mark = [];},
- "Y": function(cm) {cm.setOption("keyMap", "vim-prefix-y"); emptyBuffer(); yank = 0;},
+ "M": function(cm) {cm.setOption("keyMap", "vim-prefix-m"); mark = {};},
+ "Y": function(cm) {cm.setOption("keyMap", "vim-prefix-y"); emptyBuffer();},
"Shift-Y": function(cm) {
emptyBuffer();
- mark["Shift-D"] = cm.getCursor(false).line;
- cm.setCursor(cm.getCursor(true).line);
- yankTillMark(cm,"Shift-D"); mark = [];
+ iterTimes(function(i) { pushInBuffer("\n" + cm.getLine(cm.getCursor().line + i)); });
},
"/": function(cm) {var f = CodeMirror.commands.find; f && f(cm); sdir = "f";},
"'?'": function(cm) {
@@ -300,8 +375,8 @@
if (fn) sdir != "r" ? CodeMirror.commands.findPrev(cm) : fn.findNext(cm);
},
"Shift-G": function(cm) {
- count == "" ? cm.setCursor(cm.lineCount()) : cm.setCursor(parseInt(count, 10)-1);
- popCount();
+ (repeatCount == 0) ? cm.setCursor(cm.lineCount()) : cm.setCursor(repeatCount - 1);
+ clearCount();
CodeMirror.commands.goLineStart(cm);
},
"':'": function(cm) {
@@ -318,29 +393,16 @@
};
// standard mode switching
- iterList(["d", "t", "T", "f", "F", "c", "r"],
- function (ch) {
- CodeMirror.keyMap.vim[toCombo(ch)] = function (cm) {
- cm.setOption("keyMap", "vim-prefix-" + ch);
- emptyBuffer();
- };
- });
-
- function addCountBindings(keyMap) {
- // Add bindings for number keys
- keyMap["0"] = function(cm) {
- count.length > 0 ? pushCountDigit("0")(cm) : CodeMirror.commands.goLineStart(cm);
+ iterList(["d", "t", "T", "f", "F", "c", "r"], function (ch) {
+ CodeMirror.keyMap.vim[toCombo(ch)] = function (cm) {
+ cm.setOption("keyMap", "vim-prefix-" + ch);
+ emptyBuffer();
};
- for (var i = 1; i < 10; ++i) keyMap[i] = pushCountDigit(i);
- }
- addCountBindings(CodeMirror.keyMap.vim);
+ });
// main num keymap
// Add bindings that are influenced by number keys
iterObj({
- "Left": "goColumnLeft", "Right": "goColumnRight",
- "Down": "goLineDown", "Up": "goLineUp", "Backspace": "goCharLeft",
- "Space": "goCharRight",
"X": function(cm) {CodeMirror.commands.delCharRight(cm);},
"P": function(cm) {
var cur = cm.getCursor().line;
@@ -402,15 +464,18 @@
});
CodeMirror.keyMap["vim-prefix-g"] = {
- "E": countTimes(function(cm) { cm.setCursor(moveToWord(cm, word, -1, 1, "start"));}),
- "Shift-E": countTimes(function(cm) { cm.setCursor(moveToWord(cm, bigWord, -1, 1, "start"));}),
- "G": function (cm) { cm.setCursor({line: 0, ch: cm.getCursor().ch});},
+ "E": countTimes(function(cm) { cm.setCursor(moveToWord(cm, word, -1, 1, "end"));}),
+ "Shift-E": countTimes(function(cm) { cm.setCursor(moveToWord(cm, bigWord, -1, 1, "end"));}),
+ "G": function (cm) {
+ cm.setCursor({line: repeatCount - 1, ch: cm.getCursor().ch});
+ clearCount();
+ },
auto: "vim", nofallthrough: true, style: "fat-cursor"
};
CodeMirror.keyMap["vim-prefix-d"] = {
"D": countTimes(function(cm) {
- pushInBuffer("\n"+cm.getLine(cm.getCursor().line));
+ pushInBuffer("\n" + cm.getLine(cm.getCursor().line));
cm.removeLine(cm.getCursor().line);
cm.setOption("keyMap", "vim");
}),
@@ -429,8 +494,6 @@
},
nofallthrough: true, style: "fat-cursor"
};
- // FIXME - does not work for bindings like "d3e"
- addCountBindings(CodeMirror.keyMap["vim-prefix-d"]);
CodeMirror.keyMap["vim-prefix-c"] = {
"B": function (cm) {
@@ -472,10 +535,10 @@
mark[m] = cm.getCursor().line;
};
CodeMirror.keyMap["vim-prefix-d'"][m] = function(cm) {
- delTillMark(cm,m);
+ delTillMark(cm, m);
};
CodeMirror.keyMap["vim-prefix-y'"][m] = function(cm) {
- yankTillMark(cm,m);
+ yankTillMark(cm, m);
};
CodeMirror.keyMap["vim-prefix-r"][m] = function (cm) {
var cur = cm.getCursor();
@@ -509,7 +572,10 @@
setupPrefixBindingForKey("Space");
CodeMirror.keyMap["vim-prefix-y"] = {
- "Y": countTimes(function(cm) { pushInBuffer("\n"+cm.getLine(cm.getCursor().line+yank)); yank++; }),
+ "Y": countTimes(function(cm, i, last) {
+ pushInBuffer("\n" + cm.getLine(cm.getCursor().line + i));
+ cm.setOption("keyMap", "vim");
+ }),
"'": function(cm) {cm.setOption("keyMap", "vim-prefix-y'"); emptyBuffer();},
nofallthrough: true, style: "fat-cursor"
};
@@ -586,67 +652,107 @@
return {start: start, end: end};
}
- // These are our motion commands to be used for navigation and selection with
- // certian other commands. All should return a cursor object.
- var motionList = ['B', 'E', 'J', 'K', 'H', 'L', 'W', 'Shift-W', "'^'", "'$'", "'%'", 'Esc'];
+ // takes in a symbol and a cursor and tries to simulate text objects that have
+ // identical opening and closing symbols
+ // TODO support across multiple lines
+ function findBeginningAndEnd(cm, symb, inclusive) {
+ var cur = cm.getCursor();
+ var line = cm.getLine(cur.line);
+ var chars = line.split('');
+ var start = undefined;
+ var end = undefined;
+ var firstIndex = chars.indexOf(symb);
+
+ // the decision tree is to always look backwards for the beginning first,
+ // but if the cursor is in front of the first instance of the symb,
+ // then move the cursor forward
+ if (cur.ch < firstIndex) {
+ cur.ch = firstIndex;
+ cm.setCursor(cur.line, firstIndex+1);
+ }
+ // otherwise if the cursor is currently on the closing symbol
+ else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
+ end = cur.ch; // assign end to the current cursor
+ --cur.ch; // make sure to look backwards
+ }
- motions = {
- 'B': function(cm, times) { return moveToWord(cm, word, -1, times); },
- 'Shift-B': function(cm, times) { return moveToWord(cm, bigWord, -1, times); },
- 'E': function(cm, times) { return moveToWord(cm, word, 1, times, 'end'); },
- 'Shift-E': function(cm, times) { return moveToWord(cm, bigWord, 1, times, 'end'); },
- 'J': function(cm, times) {
- var cur = cm.getCursor();
- return {line: cur.line+times, ch : cur.ch};
- },
+ // if we're currently on the symbol, we've got a start
+ if (chars[cur.ch] == symb && end == null)
+ start = cur.ch + 1; // assign start to ahead of the cursor
+ else {
+ // go backwards to find the start
+ for (var i = cur.ch; i > -1 && start == null; i--)
+ if (chars[i] == symb) start = i + 1;
+ }
- 'K': function(cm, times) {
- var cur = cm.getCursor();
- return {line: cur.line-times, ch: cur.ch};
- },
+ // look forwards for the end symbol
+ if (start != null && end == null) {
+ for (var i = start, len = chars.length; i < len && end == null; i++) {
+ if (chars[i] == symb) end = i;
+ }
+ }
- 'H': function(cm, times) {
- var cur = cm.getCursor();
- return {line: cur.line, ch: cur.ch-times};
- },
+ // nothing found
+ // FIXME still enters insert mode
+ if (start == null || end == null) return {
+ start: cur, end: cur
+ };
- 'L': function(cm, times) {
- var cur = cm.getCursor();
- return {line: cur.line, ch: cur.ch+times};
- },
- 'W': function(cm, times) { return moveToWord(cm, word, 1, times); },
- 'Shift-W': function(cm, times) { return moveToWord(cm, bigWord, 1, times); },
- "'^'": function(cm, times) {
- var cur = cm.getCursor();
- var line = cm.getLine(cur.line).split('');
+ // include the symbols
+ if (inclusive) {
+ --start; ++end;
+ }
- // Empty line :o
- if (line.length == 0) return cur;
+ return {
+ start: {line: cur.line, ch: start},
+ end: {line: cur.line, ch: end}
+ };
+ }
+
+ function offsetCursor(cm, line, ch) {
+ var cur = cm.getCursor(); return {line: cur.line + line, ch: cur.ch + ch};
+ }
- for (var index = 0; index < line.length; index++) {
- if (line[index].match(/[^\s]/)) return {line: cur.line, ch: index};
+ // These are the motion commands we use for navigation and selection with
+ // certain other commands. All should return a cursor object.
+ var motions = {
+ "J": function(cm, times) { return offsetCursor(cm, times, 0); },
+ "Down": function(cm, times) { return offsetCursor(cm, times, 0); },
+ "K": function(cm, times) { return offsetCursor(cm, -times, 0); },
+ "Up": function(cm, times) { return offsetCursor(cm, -times, 0); },
+ "L": function(cm, times) { return offsetCursor(cm, 0, times); },
+ "Right": function(cm, times) { return offsetCursor(cm, 0, times); },
+ "Space": function(cm, times) { return offsetCursor(cm, 0, times); },
+ "H": function(cm, times) { return offsetCursor(cm, 0, -times); },
+ "Left": function(cm, times) { return offsetCursor(cm, 0, -times); },
+ "Backspace": function(cm, times) { return offsetCursor(cm, 0, -times); },
+ "B": function(cm, times, yank) { return moveToWord(cm, word, -1, times, 'start', yank); },
+ "Shift-B": function(cm, times, yank) { return moveToWord(cm, bigWord, -1, times, 'start', yank); },
+ "E": function(cm, times, yank) { return moveToWord(cm, word, 1, times, 'end', yank); },
+ "Shift-E": function(cm, times, yank) { return moveToWord(cm, bigWord, 1, times, 'end', yank); },
+ "W": function(cm, times, yank) { return moveToWord(cm, word, 1, times, 'start', yank); },
+ "Shift-W": function(cm, times, yank) { return moveToWord(cm, bigWord, 1, times, 'start', yank); },
+ "'^'": function(cm, times) {
+ var cur = cm.getCursor(), line = cm.getLine(cur.line).split('');
+ for (var i = 0; i < line.length; i++) {
+ if (line[i].match(/[^\s]/)) return {line: cur.line, ch: index};
}
+ return cur;
},
"'$'": function(cm) {
- var cur = cm.getCursor();
- var line = cm.getLine(cur.line);
- return {line: cur.line, ch: line.length};
+ var cur = cm.getCursor(), ch = cm.getLine(cur.line).length;
+ return {line: cur.line, ch: ch};
},
"'%'": function(cm) { return findMatchedSymbol(cm, cm.getCursor()); },
- "Esc" : function(cm) {
- cm.setOption('vim');
- reptTimes = 0;
-
- return cm.getCursor();
- }
+ "Esc" : function(cm) { cm.setOption("keyMap", "vim"); repeatCount = 0; return cm.getCursor(); }
};
// Map our movement actions each operator and non-operational movement
- motionList.forEach(function(key, index, array) {
+ iterObj(motions, function(key, motion) {
CodeMirror.keyMap['vim-prefix-d'][key] = function(cm) {
// Get our selected range
var start = cm.getCursor();
- var end = motions[key](cm, reptTimes ? reptTimes : 1);
+ var end = motion(cm, repeatCount ? repeatCount : 1, true);
// Set swap var if range is of negative length
if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true;
@@ -656,61 +762,64 @@
cm.replaceRange("", swap ? end : start, swap ? start : end);
// And clean up
- reptTimes = 0;
+ repeatCount = 0;
cm.setOption("keyMap", "vim");
};
CodeMirror.keyMap['vim-prefix-c'][key] = function(cm) {
var start = cm.getCursor();
- var end = motions[key](cm, reptTimes ? reptTimes : 1);
+ var end = motion(cm, repeatCount ? repeatCount : 1, true);
if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true;
pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end));
cm.replaceRange("", swap ? end : start, swap ? start : end);
- reptTimes = 0;
+ repeatCount = 0;
cm.setOption('keyMap', 'vim-insert');
};
CodeMirror.keyMap['vim-prefix-y'][key] = function(cm) {
var start = cm.getCursor();
- var end = motions[key](cm, reptTimes ? reptTimes : 1);
+ var end = motion(cm, repeatCount ? repeatCount : 1, true);
if ((start.line > end.line) || (start.line == end.line && start.ch > end.ch)) var swap = true;
pushInBuffer(cm.getRange(swap ? end : start, swap ? start : end));
- reptTimes = 0;
+ repeatCount = 0;
cm.setOption("keyMap", "vim");
};
CodeMirror.keyMap['vim'][key] = function(cm) {
- var cur = motions[key](cm, reptTimes ? reptTimes : 1);
+ var cur = motion(cm, repeatCount ? repeatCount : 1);
cm.setCursor(cur.line, cur.ch);
- reptTimes = 0;
+ repeatCount = 0;
};
});
- var nums = [1,2,3,4,5,6,7,8,9];
- nums.forEach(function(key, index, array) {
- CodeMirror.keyMap['vim'][key] = function (cm) {
- reptTimes = (reptTimes * 10) + key;
- };
- CodeMirror.keyMap['vim-prefix-d'][key] = function (cm) {
- reptTimes = (reptTimes * 10) + key;
- };
- CodeMirror.keyMap['vim-prefix-y'][key] = function (cm) {
- reptTimes = (reptTimes * 10) + key;
- };
- CodeMirror.keyMap['vim-prefix-c'][key] = function (cm) {
- reptTimes = (reptTimes * 10) + key;
+ function addCountBindings(keyMapName) {
+ // Add bindings for number keys
+ keyMap = CodeMirror.keyMap[keyMapName];
+ keyMap["0"] = function(cm) {
+ if (repeatCount > 0) {
+ pushRepeatCountDigit(0)(cm);
+ } else {
+ CodeMirror.commands.goLineStart(cm);
+ }
};
- });
+ for (var i = 1; i < 10; ++i) {
+ keyMap[i] = pushRepeatCountDigit(i);
+ }
+ }
+ addCountBindings('vim');
+ addCountBindings('vim-prefix-d');
+ addCountBindings('vim-prefix-y');
+ addCountBindings('vim-prefix-c');
// Create our keymaps for each operator and make xa and xi where x is an operator
// change to the corrosponding keymap
var operators = ['d', 'y', 'c'];
- operators.forEach(function(key, index, array) {
+ iterList(operators, function(key, index, array) {
CodeMirror.keyMap['vim-prefix-'+key+'a'] = {
auto: 'vim', nofallthrough: true, style: "fat-cursor"
};
@@ -719,12 +828,12 @@
};
CodeMirror.keyMap['vim-prefix-'+key]['A'] = function(cm) {
- reptTimes = 0;
+ repeatCount = 0;
cm.setOption('keyMap', 'vim-prefix-' + key + 'a');
};
CodeMirror.keyMap['vim-prefix-'+key]['I'] = function(cm) {
- reptTimes = 0;
+ repeatCount = 0;
cm.setOption('keyMap', 'vim-prefix-' + key + 'i');
};
});
@@ -737,7 +846,7 @@
// Create our text object functions. They work similar to motions but they
// return a start cursor as well
- var textObjectList = ['W', 'Shift-[', 'Shift-9', '['];
+ var textObjectList = ['W', 'Shift-[', 'Shift-9', '[', "'", "Shift-'"];
var textObjects = {
'W': function(cm, inclusive) {
var cur = cm.getCursor();
@@ -752,7 +861,9 @@
},
'Shift-[': function(cm, inclusive) { return selectCompanionObject(cm, '}', inclusive); },
'Shift-9': function(cm, inclusive) { return selectCompanionObject(cm, ')', inclusive); },
- '[': function(cm, inclusive) { return selectCompanionObject(cm, ']', inclusive); }
+ '[': function(cm, inclusive) { return selectCompanionObject(cm, ']', inclusive); },
+ "'": function(cm, inclusive) { return findBeginningAndEnd(cm, "'", inclusive); },
+ "Shift-'": function(cm, inclusive) { return findBeginningAndEnd(cm, '"', inclusive); }
};
// One function to handle all operation upon text objects. Kinda funky but it works
diff --git a/lib/codemirror.css b/lib/codemirror.css
index f0e91b2d73..41b8d09e13 100644
--- a/lib/codemirror.css
+++ b/lib/codemirror.css
@@ -80,6 +80,7 @@
word-wrap: normal;
line-height: inherit;
color: inherit;
+ overflow: visible;
}
.CodeMirror-wrap pre {
@@ -145,7 +146,7 @@ div.CodeMirror-selected { background: #d9d9d9; }
.cm-s-default span.cm-error {color: #f00;}
.cm-s-default span.cm-qualifier {color: #555;}
.cm-s-default span.cm-builtin {color: #30a;}
-.cm-s-default span.cm-bracket {color: #cc7;}
+.cm-s-default span.cm-bracket {color: #997;}
.cm-s-default span.cm-tag {color: #170;}
.cm-s-default span.cm-attribute {color: #00c;}
.cm-s-default span.cm-header {color: blue;}
@@ -170,4 +171,4 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
visibility: hidden;
}
-}
\ No newline at end of file
+}
diff --git a/lib/codemirror.js b/lib/codemirror.js
index fa950dca8c..571ea9c26c 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -64,9 +64,9 @@ window.CodeMirror = (function() {
var poll = new Delayed(), highlight = new Delayed(), blinker;
// mode holds a mode API object. doc is the tree of Line objects,
- // work an array of lines that should be parsed, and history the
- // undo history (instance of History constructor).
- var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), work, focused;
+ // frontier is the point up to which the content has been parsed,
+ // and history the undo history (instance of History constructor).
+ var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), frontier = 0, focused;
loadMode();
// The selection. These are always maintained to point at valid
// positions. Inverted is used to remember that the user is
@@ -75,10 +75,10 @@ window.CodeMirror = (function() {
// Selection-related flags. shiftSelecting obviously tracks
// whether the user is holding shift.
var shiftSelecting, lastClick, lastDoubleClick, lastScrollTop = 0, draggingText,
- overwrite = false, suppressEdits = false;
+ overwrite = false, suppressEdits = false, pasteIncoming = false;
// Variables used by startOperation/endOperation to track what
// happened during the operation.
- var updateInput, userSelChange, changes, textChanged, selectionChanged, leaveInputAlone,
+ var updateInput, userSelChange, changes, textChanged, selectionChanged,
gutterDirty, callbacks;
// Current visible range (may be bigger than the view window).
var displayOffset = 0, showingFrom = 0, showingTo = 0, lastSizeC = 0;
@@ -88,7 +88,6 @@ window.CodeMirror = (function() {
// Tracks the maximum line length so that the horizontal scrollbar
// can be kept static when scrolling.
var maxLine = getLine(0), updateMaxLine = false, maxLineChanged = true;
- var tabCache = {};
var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll
var goalColumn = null;
@@ -129,7 +128,7 @@ window.CodeMirror = (function() {
connect(scroller, "drop", operation(onDrop));
}
connect(scroller, "paste", function(){focusInput(); fastPoll();});
- connect(input, "paste", fastPoll);
+ connect(input, "paste", function(){pasteIncoming = true; fastPoll();});
connect(input, "cut", operation(function(){
if (!options.readOnly) replaceSelection("");
}));
@@ -168,6 +167,7 @@ window.CodeMirror = (function() {
else if (option == "lineWrapping" && oldVal != value) operation(wrappingChanged)();
else if (option == "tabSize") updateDisplay(true);
else if (option == "keyMap") keyMapChanged();
+ else if (option == "tabindex") input.tabIndex = value;
if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" ||
option == "theme" || option == "lineNumberFormatter") {
gutterChanged();
@@ -175,6 +175,7 @@ window.CodeMirror = (function() {
}
},
getOption: function(option) {return options[option];},
+ getMode: function() {return mode;},
undo: operation(undo),
redo: operation(redo),
indentLine: operation(function(n, dir) {
@@ -193,8 +194,18 @@ window.CodeMirror = (function() {
history.undone = histData.undone;
},
getHistory: function() {
- history.time = 0;
- return {done: history.done.concat([]), undone: history.undone.concat([])};
+ function cp(arr) {
+ for (var i = 0, nw = [], nwelt; i < arr.length; ++i) {
+ nw.push(nwelt = []);
+ for (var j = 0, elt = arr[i]; j < elt.length; ++j) {
+ var old = [], cur = elt[j];
+ nwelt.push({start: cur.start, added: cur.added, old: old});
+ for (var k = 0; k < cur.old.length; ++k) old.push(hlText(cur.old[k]));
+ }
+ }
+ return nw;
+ }
+ return {done: cp(history.done), undone: cp(history.undone)};
},
matchBrackets: operation(function(){matchBrackets(true);}),
getTokenAt: operation(function(pos) {
@@ -219,6 +230,7 @@ window.CodeMirror = (function() {
var off = eltOffset(lineSpace);
return coordsChar(coords.x - off.left, coords.y - off.top);
},
+ defaultTextHeight: function() { return textHeight(); },
markText: operation(markText),
setBookmark: setBookmark,
findMarksAt: findMarksAt,
@@ -333,6 +345,11 @@ window.CodeMirror = (function() {
return {x: scroller.scrollLeft, y: scrollbar.scrollTop,
height: scrollbar.scrollHeight, width: scroller.scrollWidth};
},
+ scrollIntoView: function(pos) {
+ var coords = localCoords(pos ? clipPos(pos) : sel.inverted ? sel.from : sel.to);
+ scrollIntoView(coords.x, coords.y, coords.x, coords.yBot);
+ },
+
setSize: function(width, height) {
function interpret(val) {
val = String(val);
@@ -363,6 +380,12 @@ window.CodeMirror = (function() {
for (var n = line; n; n = n.parent) n.height += diff;
}
+ function lineContent(line, wrapAt) {
+ if (!line.styles)
+ line.highlight(mode, line.stateAfter = getStateBefore(lineNo(line)), options.tabSize);
+ return line.getContent(options.tabSize, wrapAt, options.lineWrapping);
+ }
+
function setValue(code) {
var top = {line: 0, ch: 0};
updateLines(top, {line: doc.size - 1, ch: getLine(doc.size-1).text.length},
@@ -376,7 +399,7 @@ window.CodeMirror = (function() {
}
function onScrollBar(e) {
- if (scrollbar.scrollTop != lastScrollTop) {
+ if (Math.abs(scrollbar.scrollTop - lastScrollTop) > 1) {
lastScrollTop = scroller.scrollTop = scrollbar.scrollTop;
updateDisplay([]);
}
@@ -385,7 +408,7 @@ 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) {
+ if (Math.abs(scroller.scrollTop - lastScrollTop) > 1) {
lastScrollTop = scroller.scrollTop;
if (scrollbar.scrollTop != lastScrollTop)
scrollbar.scrollTop = lastScrollTop;
@@ -596,10 +619,11 @@ window.CodeMirror = (function() {
}, 50);
var name = keyNames[e_prop(e, "keyCode")], handled = false;
+ var flipCtrlCmd = opera && mac;
if (name == null || e.altGraphKey) return false;
if (e_prop(e, "altKey")) name = "Alt-" + name;
- if (e_prop(e, "ctrlKey")) name = "Ctrl-" + name;
- if (e_prop(e, "metaKey")) name = "Cmd-" + name;
+ if (e_prop(e, flipCtrlCmd ? "metaKey" : "ctrlKey")) name = "Ctrl-" + name;
+ if (e_prop(e, flipCtrlCmd ? "ctrlKey" : "metaKey")) name = "Cmd-" + name;
var stopped = false;
function stop() { stopped = true; }
@@ -617,7 +641,7 @@ window.CodeMirror = (function() {
if (handled) {
e_preventDefault(e);
restartBlink();
- if (ie) { e.oldKeyCode = e.keyCode; e.keyCode = 0; }
+ if (ie_lt9) { e.oldKeyCode = e.keyCode; e.keyCode = 0; }
}
return handled;
}
@@ -675,7 +699,6 @@ window.CodeMirror = (function() {
focused = true;
if (scroller.className.search(/\bCodeMirror-focused\b/) == -1)
scroller.className += " CodeMirror-focused";
- if (!leaveInputAlone) resetInput(true);
}
slowPoll();
restartBlink();
@@ -698,13 +721,16 @@ window.CodeMirror = (function() {
// Afterwards, set the selection to selFrom, selTo.
function updateLines(from, to, newText, selFrom, selTo) {
if (suppressEdits) return;
+ var old = [];
+ doc.iter(from.line, to.line + 1, function(line) {
+ old.push(newHL(line.text, line.markedSpans));
+ });
if (history) {
- var old = [];
- doc.iter(from.line, to.line + 1, function(line) { old.push(line.text); });
history.addChange(from.line, newText.length, old);
while (history.done.length > options.undoDepth) history.done.shift();
}
- updateLinesNoUndo(from, to, newText, selFrom, selTo);
+ var lines = updateMarkedSpans(hlSpans(old[0]), hlSpans(lst(old)), from.ch, to.ch, newText);
+ updateLinesNoUndo(from, to, lines, selFrom, selTo);
}
function unredoHelper(from, to) {
if (!from.length) return;
@@ -712,11 +738,12 @@ window.CodeMirror = (function() {
for (var i = set.length - 1; i >= 0; i -= 1) {
var change = set[i];
var replaced = [], end = change.start + change.added;
- doc.iter(change.start, end, function(line) { replaced.push(line.text); });
+ doc.iter(change.start, end, function(line) { replaced.push(newHL(line.text, line.markedSpans)); });
out.push({start: change.start, added: change.old.length, old: replaced});
var pos = {line: change.start + change.old.length - 1,
- ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])};
- updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length}, change.old, pos, pos);
+ ch: editEnd(hlText(lst(replaced)), hlText(lst(change.old)))};
+ updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length},
+ change.old, pos, pos);
}
updateInput = true;
to.push(out);
@@ -724,66 +751,59 @@ window.CodeMirror = (function() {
function undo() {unredoHelper(history.done, history.undone);}
function redo() {unredoHelper(history.undone, history.done);}
- function updateLinesNoUndo(from, to, newText, selFrom, selTo) {
+ function updateLinesNoUndo(from, to, lines, selFrom, selTo) {
if (suppressEdits) return;
var recomputeMaxLength = false, maxLineLength = maxLine.text.length;
if (!options.lineWrapping)
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;
+ if (from.line != to.line || lines.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.
- if (from.ch == 0 && to.ch == 0 && newText[newText.length - 1] == "") {
+ var lastHL = lst(lines);
+
+ // First adjust the line structure
+ if (from.ch == 0 && to.ch == 0 && hlText(lastHL) == "") {
// This is a whole-line replace. Treated specially to make
// sure line objects move the way they are supposed to.
var added = [], prevLine = null;
- if (from.line) {
- prevLine = getLine(from.line - 1);
- prevLine.fixMarkEnds(lastLine);
- } else lastLine.fixMarkStarts();
- for (var i = 0, e = newText.length - 1; i < e; ++i)
- added.push(Line.inheritMarks(newText[i], prevLine));
+ for (var i = 0, e = lines.length - 1; i < e; ++i)
+ added.push(new Line(hlText(lines[i]), hlSpans(lines[i])));
+ lastLine.update(lastLine.text, hlSpans(lastHL));
if (nlines) doc.remove(from.line, nlines, callbacks);
if (added.length) doc.insert(from.line, added);
} else if (firstLine == lastLine) {
- if (newText.length == 1)
- firstLine.replace(from.ch, to.ch, newText[0]);
- else {
- lastLine = firstLine.split(to.ch, newText[newText.length-1]);
- firstLine.replace(from.ch, null, newText[0]);
- firstLine.fixMarkEnds(lastLine);
- var added = [];
- for (var i = 1, e = newText.length - 1; i < e; ++i)
- added.push(Line.inheritMarks(newText[i], firstLine));
- added.push(lastLine);
+ if (lines.length == 1) {
+ firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]) + firstLine.text.slice(to.ch), hlSpans(lines[0]));
+ } else {
+ for (var added = [], i = 1, e = lines.length - 1; i < e; ++i)
+ added.push(new Line(hlText(lines[i]), hlSpans(lines[i])));
+ added.push(new Line(hlText(lastHL) + firstLine.text.slice(to.ch), hlSpans(lastHL)));
+ firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]), hlSpans(lines[0]));
doc.insert(from.line + 1, added);
}
- } else if (newText.length == 1) {
- firstLine.replace(from.ch, null, newText[0]);
- lastLine.replace(null, to.ch, "");
- firstLine.append(lastLine);
+ } else if (lines.length == 1) {
+ firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]) + lastLine.text.slice(to.ch), hlSpans(lines[0]));
doc.remove(from.line + 1, nlines, callbacks);
} else {
var added = [];
- firstLine.replace(from.ch, null, newText[0]);
- lastLine.replace(null, to.ch, newText[newText.length-1]);
- firstLine.fixMarkEnds(lastLine);
- for (var i = 1, e = newText.length - 1; i < e; ++i)
- added.push(Line.inheritMarks(newText[i], firstLine));
+ firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]), hlSpans(lines[0]));
+ lastLine.update(hlText(lastHL) + lastLine.text.slice(to.ch), hlSpans(lastHL));
+ for (var i = 1, e = lines.length - 1; i < e; ++i)
+ added.push(new Line(hlText(lines[i]), hlSpans(lines[i])));
if (nlines > 1) doc.remove(from.line + 1, nlines - 1, callbacks);
doc.insert(from.line + 1, added);
}
if (options.lineWrapping) {
var perLine = Math.max(5, scroller.clientWidth / charWidth() - 3);
- doc.iter(from.line, from.line + newText.length, function(line) {
+ doc.iter(from.line, from.line + lines.length, function(line) {
if (line.hidden) return;
var guess = Math.ceil(line.text.length / perLine) || 1;
if (guess != line.height) updateLineHeight(line, guess);
});
} else {
- doc.iter(from.line, from.line + newText.length, function(line) {
+ doc.iter(from.line, from.line + lines.length, function(line) {
var l = line.text;
if (!line.hidden && l.length > maxLineLength) {
maxLine = line; maxLineLength = l.length; maxLineChanged = true;
@@ -793,26 +813,24 @@ window.CodeMirror = (function() {
if (recomputeMaxLength) updateMaxLine = true;
}
- // Add these lines to the work array, so that they will be
- // highlighted. Adjust work lines if lines were added/removed.
- var newWork = [], lendiff = newText.length - nlines - 1;
- for (var i = 0, l = work.length; i < l; ++i) {
- var task = work[i];
- if (task < from.line) newWork.push(task);
- else if (task > to.line) newWork.push(task + lendiff);
- }
- var hlEnd = from.line + Math.min(newText.length, 500);
- highlightLines(from.line, hlEnd);
- newWork.push(hlEnd);
- work = newWork;
- startWorker(100);
+ // Adjust frontier, schedule worker
+ frontier = Math.min(frontier, from.line);
+ startWorker(400);
+
+ var lendiff = lines.length - nlines - 1;
// Remember that these lines changed, for updating the display
changes.push({from: from.line, to: to.line + 1, diff: lendiff});
- var changeObj = {from: from, to: to, text: newText};
- if (textChanged) {
- for (var cur = textChanged; cur.next; cur = cur.next) {}
- cur.next = changeObj;
- } else textChanged = changeObj;
+ if (options.onChange) {
+ // Normalize lines to contain only strings, since that's what
+ // the change event handler expects
+ for (var i = 0; i < lines.length; ++i)
+ if (typeof lines[i] != "string") lines[i] = lines[i].text;
+ var changeObj = {from: from, to: to, text: lines};
+ if (textChanged) {
+ for (var cur = textChanged; cur.next; cur = cur.next) {}
+ cur.next = changeObj;
+ } else textChanged = changeObj;
+ }
// Update the selection
function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;}
@@ -874,7 +892,7 @@ window.CodeMirror = (function() {
var line = pos.line + code.length - (to.line - from.line) - 1;
var ch = pos.ch;
if (pos.line == to.line)
- ch += code[code.length-1].length - (to.ch - (to.line == from.line ? from.ch : 0));
+ ch += lst(code).length - (to.ch - (to.line == from.line ? from.ch : 0));
return {line: line, ch: ch};
}
var end;
@@ -892,7 +910,7 @@ window.CodeMirror = (function() {
});
}
function replaceRange1(code, from, to, computeSel) {
- var endch = code.length == 1 ? code[0].length + from.ch : code[code.length-1].length;
+ var endch = code.length == 1 ? code[0].length + from.ch : lst(code).length;
var newSel = computeSel({line: from.line + code.length - 1, ch: endch});
updateLines(from, to, code, newSel.from, newSel.to);
}
@@ -912,21 +930,17 @@ window.CodeMirror = (function() {
function slowPoll() {
if (pollingFast) return;
poll.set(options.pollInterval, function() {
- startOperation();
readInput();
if (focused) slowPoll();
- endOperation();
});
}
function fastPoll() {
var missed = false;
pollingFast = true;
function p() {
- startOperation();
var changed = readInput();
if (!changed && !missed) {missed = true; poll.set(60, p);}
else {pollingFast = false; slowPoll();}
- endOperation();
}
poll.set(20, p);
}
@@ -938,19 +952,22 @@ window.CodeMirror = (function() {
// supported or compatible enough yet to rely on.)
var prevInput = "";
function readInput() {
- if (leaveInputAlone || !focused || hasSelection(input) || options.readOnly) return false;
+ if (!focused || hasSelection(input) || options.readOnly) return false;
var text = input.value;
if (text == prevInput) return false;
+ if (!nestedOperation) startOperation();
shiftSelecting = null;
var same = 0, l = Math.min(prevInput.length, text.length);
while (same < l && prevInput[same] == text[same]) ++same;
if (same < prevInput.length)
sel.from = {line: sel.from.line, ch: sel.from.ch - (prevInput.length - same)};
- else if (overwrite && posEq(sel.from, sel.to))
+ else if (overwrite && posEq(sel.from, sel.to) && !pasteIncoming)
sel.to = {line: sel.to.line, ch: Math.min(getLine(sel.to.line).text.length, sel.to.ch + (text.length - same))};
replaceSelection(text.slice(same), "end");
if (text.length > 1000) { input.value = prevInput = ""; }
else prevInput = text;
+ if (!nestedOperation) endOperation();
+ pasteIncoming = false;
return true;
}
function resetInput(user) {
@@ -1076,6 +1093,7 @@ window.CodeMirror = (function() {
});
showingFrom = from; showingTo = to;
displayOffset = heightAtLine(doc, from);
+ startWorker(100);
// Since this is all rather error prone, it is honoured with the
// only assertion in the whole file.
@@ -1162,7 +1180,7 @@ window.CodeMirror = (function() {
if (!nextIntact || nextIntact.from > j) {
if (line.hidden) var lineElement = elt("pre");
else {
- var lineElement = line.getElement(makeTab);
+ var lineElement = lineContent(line);
if (line.className) lineElement.className = line.className;
// Kludge to make sure the styled element lies behind the selection (by z-index)
if (line.bgClassName) {
@@ -1406,7 +1424,7 @@ window.CodeMirror = (function() {
var startChar = line.charAt(start);
var check = isWordChar(startChar) ? isWordChar :
/\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} :
- function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
+ function(ch) {return !/\s/.test(ch) && isWordChar(ch);};
while (start > 0 && check(line.charAt(start - 1))) --start;
while (end < line.length && check(line.charAt(end))) ++end;
}
@@ -1446,17 +1464,18 @@ window.CodeMirror = (function() {
var indentString = "", pos = 0;
if (options.indentWithTabs)
for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";}
- while (pos < indentation) {++pos; indentString += " ";}
+ if (pos < indentation) indentString += spaceStr(indentation - pos);
if (indentString != curSpaceString)
replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length});
+ line.stateAfter = null;
}
function loadMode() {
mode = CodeMirror.getMode(options, options.mode);
doc.iter(0, doc.size, function(line) { line.stateAfter = null; });
- work = [0];
- startWorker();
+ frontier = 0;
+ startWorker(100);
}
function gutterChanged() {
var visible = options.gutter || options.lineNumbers;
@@ -1483,13 +1502,6 @@ window.CodeMirror = (function() {
}
changes.push({from: 0, to: doc.size});
}
- function makeTab(col) {
- var w = options.tabSize - col % options.tabSize, cached = tabCache[w];
- if (cached) return cached;
- for (var str = "", i = 0; i < w; ++i) str += " ";
- var span = elt("span", str, "cm-tab");
- return (tabCache[w] = {element: span, width: w});
- }
function themeChanged() {
scroller.className = scroller.className.replace(/\s*cm-s-\S+/g, "") +
options.theme.replace(/(^|\s)\s*/g, " cm-s-");
@@ -1500,74 +1512,71 @@ window.CodeMirror = (function() {
(style ? " cm-keymap-" + style : "");
}
- function TextMarker() { this.set = []; }
+ function TextMarker(type, style) { this.lines = []; this.type = type; if (style) this.style = style; }
TextMarker.prototype.clear = operation(function() {
- var min = Infinity, max = -Infinity;
- for (var i = 0, e = this.set.length; i < e; ++i) {
- var line = this.set[i], mk = line.marked;
- if (!mk || !line.parent) continue;
- var lineN = lineNo(line);
- min = Math.min(min, lineN); max = Math.max(max, lineN);
- for (var j = 0; j < mk.length; ++j)
- if (mk[j].marker == this) mk.splice(j--, 1);
- }
- if (min != Infinity)
- changes.push({from: min, to: max + 1});
+ var min, max;
+ for (var i = 0; i < this.lines.length; ++i) {
+ var line = this.lines[i];
+ var span = getMarkedSpanFor(line.markedSpans, this);
+ if (span.from != null) min = lineNo(line);
+ if (span.to != null) max = lineNo(line);
+ line.markedSpans = removeMarkedSpan(line.markedSpans, span);
+ }
+ if (min != null) changes.push({from: min, to: max + 1});
+ this.lines.length = 0;
+ this.explicitlyCleared = true;
});
TextMarker.prototype.find = function() {
var from, to;
- for (var i = 0, e = this.set.length; i < e; ++i) {
- var line = this.set[i], mk = line.marked;
- for (var j = 0; j < mk.length; ++j) {
- var mark = mk[j];
- if (mark.marker == this) {
- if (mark.from != null || mark.to != null) {
- var found = lineNo(line);
- if (found != null) {
- if (mark.from != null) from = {line: found, ch: mark.from};
- if (mark.to != null) to = {line: found, ch: mark.to};
- }
- }
- }
+ for (var i = 0; i < this.lines.length; ++i) {
+ var line = this.lines[i];
+ var span = getMarkedSpanFor(line.markedSpans, this);
+ if (span.from != null || span.to != null) {
+ var found = lineNo(line);
+ if (span.from != null) from = {line: found, ch: span.from};
+ if (span.to != null) to = {line: found, ch: span.to};
}
}
- return {from: from, to: to};
+ if (this.type == "bookmark") return from;
+ return from && {from: from, to: to};
};
- function markText(from, to, className) {
+ function markText(from, to, className, options) {
from = clipPos(from); to = clipPos(to);
- var tm = new TextMarker();
- if (!posLess(from, to)) return tm;
- function add(line, from, to, className) {
- getLine(line).addMark(new MarkedText(from, to, className, tm));
- }
- if (from.line == to.line) add(from.line, from.ch, to.ch, className);
- else {
- add(from.line, from.ch, null, className);
- for (var i = from.line + 1, e = to.line; i < e; ++i)
- add(i, null, null, className);
- add(to.line, null, to.ch, className);
- }
+ var marker = new TextMarker("range", className);
+ if (options) for (var opt in options) if (options.hasOwnProperty(opt))
+ marker[opt] = options[opt];
+ var curLine = from.line;
+ doc.iter(curLine, to.line + 1, function(line) {
+ var span = {from: curLine == from.line ? from.ch : null,
+ to: curLine == to.line ? to.ch : null,
+ marker: marker};
+ line.markedSpans = (line.markedSpans || []).concat([span]);
+ marker.lines.push(line);
+ ++curLine;
+ });
changes.push({from: from.line, to: to.line + 1});
- return tm;
+ return marker;
}
function setBookmark(pos) {
pos = clipPos(pos);
- var bm = new Bookmark(pos.ch);
- getLine(pos.line).addMark(bm);
- return bm;
+ var marker = new TextMarker("bookmark"), line = getLine(pos.line);
+ history.addChange(pos.line, 1, [newHL(line.text, line.markedSpans)], true);
+ var span = {from: pos.ch, to: pos.ch, marker: marker};
+ line.markedSpans = (line.markedSpans || []).concat([span]);
+ marker.lines.push(line);
+ return marker;
}
function findMarksAt(pos) {
pos = clipPos(pos);
- var markers = [], marked = getLine(pos.line).marked;
- if (!marked) return markers;
- for (var i = 0, e = marked.length; i < e; ++i) {
- var m = marked[i];
- if ((m.from == null || m.from <= pos.ch) &&
- (m.to == null || m.to >= pos.ch))
- markers.push(m.marker || m);
+ var markers = [], spans = getLine(pos.line).markedSpans;
+ if (spans) for (var i = 0; i < spans.length; ++i) {
+ var span = spans[i];
+ if ((span.from == null || span.from <= pos.ch) &&
+ (span.to == null || span.to >= pos.ch))
+ markers.push(span.marker);
}
return markers;
}
@@ -1644,9 +1653,7 @@ window.CodeMirror = (function() {
function measureLine(line, ch) {
if (ch == 0) return {top: 0, left: 0};
- var wbr = options.lineWrapping && ch < line.text.length &&
- spanAffectsWrapping.test(line.text.slice(ch - 1, ch + 1));
- var pre = line.getElement(makeTab, ch, wbr);
+ var pre = lineContent(line, ch);
removeChildrenAndAdd(measure, pre);
var anchor = pre.anchor;
var top = anchor.offsetTop, left = anchor.offsetLeft;
@@ -1757,6 +1764,7 @@ window.CodeMirror = (function() {
var offL = eltOffset(lineSpace, true);
return coordsChar(x - offL.left, y - offL.top);
}
+ var detectingSelectAll;
function onContextMenu(e) {
var pos = posFromMouse(e), scrollPos = scrollbar.scrollTop;
if (!pos || opera) return; // Opera is difficult.
@@ -1768,19 +1776,30 @@ window.CodeMirror = (function() {
input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) +
"px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " +
"border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";
- leaveInputAlone = true;
- var val = input.value = getSelection();
focusInput();
- selectInput(input);
+ resetInput(true);
+ // Adds "Select all" to context menu in FF
+ if (posEq(sel.from, sel.to)) input.value = prevInput = " ";
+
function rehide() {
- var newVal = splitLines(input.value).join("\n");
- if (newVal != val && !options.readOnly) operation(replaceSelection)(newVal, "end");
inputDiv.style.position = "relative";
input.style.cssText = oldCSS;
if (ie_lt9) scrollbar.scrollTop = scrollPos;
- leaveInputAlone = false;
- resetInput(true);
slowPoll();
+
+ // Try to detect the user choosing select-all
+ if (input.selectionStart != null) {
+ clearTimeout(detectingSelectAll);
+ var extval = input.value = " " + (posEq(sel.from, sel.to) ? "" : input.value), i = 0;
+ prevInput = " ";
+ input.selectionStart = 1; input.selectionEnd = extval.length;
+ detectingSelectAll = setTimeout(function poll(){
+ if (prevInput == " " && input.selectionStart == 0)
+ operation(commands.selectAll)(instance);
+ else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500);
+ else resetInput();
+ }, 200);
+ }
}
if (gecko) {
@@ -1864,70 +1883,39 @@ window.CodeMirror = (function() {
return minline;
}
function getStateBefore(n) {
- var start = findStartLine(n), state = start && getLine(start-1).stateAfter;
+ var pos = findStartLine(n), state = pos && getLine(pos-1).stateAfter;
if (!state) state = startState(mode);
else state = copyState(mode, state);
- doc.iter(start, n, function(line) {
- line.highlight(mode, state, options.tabSize);
- line.stateAfter = copyState(mode, state);
+ doc.iter(pos, n, function(line) {
+ line.process(mode, state, options.tabSize);
+ line.stateAfter = (pos == n - 1 || pos % 5 == 0) ? copyState(mode, state) : null;
});
- if (start < n) changes.push({from: start, to: n});
- if (n < doc.size && !getLine(n).stateAfter) work.push(n);
return state;
}
- function highlightLines(start, end) {
- var state = getStateBefore(start);
- doc.iter(start, end, function(line) {
- line.highlight(mode, state, options.tabSize);
- line.stateAfter = copyState(mode, state);
- });
- }
function highlightWorker() {
- var end = +new Date + options.workTime;
- var foundWork = work.length;
- while (work.length) {
- if (!getLine(showingFrom).stateAfter) var task = showingFrom;
- else var task = work.pop();
- if (task >= doc.size) continue;
- var start = findStartLine(task), state = start && getLine(start-1).stateAfter;
- if (state) state = copyState(mode, state);
- else state = startState(mode);
-
- var unchanged = 0, compare = mode.compareStates, realChange = false,
- i = start, bail = false;
- doc.iter(i, doc.size, function(line) {
- var hadState = line.stateAfter;
- if (+new Date > end) {
- work.push(i);
- startWorker(options.workDelay);
- if (realChange) changes.push({from: task, to: i + 1});
- return (bail = true);
- }
- var changed = line.highlight(mode, state, options.tabSize);
- if (changed) realChange = true;
+ if (frontier >= showingTo) return;
+ var end = +new Date + options.workTime, state = copyState(mode, getStateBefore(frontier));
+ var startFrontier = frontier;
+ doc.iter(frontier, showingTo, function(line) {
+ if (frontier >= showingFrom) { // Visible
+ line.highlight(mode, state, options.tabSize);
line.stateAfter = copyState(mode, state);
- var done = null;
- if (compare) {
- var same = hadState && compare(hadState, state);
- if (same != Pass) done = !!same;
- }
- if (done == null) {
- if (changed !== false || !hadState) unchanged = 0;
- else if (++unchanged > 3 && (!mode.indent || mode.indent(hadState, "") == mode.indent(state, "")))
- done = true;
- }
- if (done) return true;
- ++i;
- });
- if (bail) return;
- if (realChange) changes.push({from: task, to: i + 1});
- }
- if (foundWork && options.onHighlightComplete)
- options.onHighlightComplete(instance);
+ } else {
+ line.process(mode, state, options.tabSize);
+ line.stateAfter = frontier % 5 == 0 ? copyState(mode, state) : null;
+ }
+ ++frontier;
+ if (+new Date > end) {
+ startWorker(options.workDelay);
+ return true;
+ }
+ });
+ if (showingTo > startFrontier && frontier >= showingFrom)
+ operation(function() {changes.push({from: startFrontier, to: frontier});})();
}
function startWorker(time) {
- if (!work.length) return;
- highlight.set(time, operation(highlightWorker));
+ if (frontier < showingTo)
+ highlight.set(time, highlightWorker);
}
// Operations are used to wrap changes in such a way that each
@@ -1962,8 +1950,7 @@ window.CodeMirror = (function() {
if (newScrollPos) scrollCursorIntoView();
if (selectionChanged) restartBlink();
- if (focused && !leaveInputAlone &&
- (updateInput === true || (updateInput !== false && selectionChanged)))
+ if (focused && (updateInput === true || (updateInput !== false && selectionChanged)))
resetInput(userSelChange);
if (selectionChanged && options.matchBrackets)
@@ -1998,6 +1985,7 @@ window.CodeMirror = (function() {
if (extensions.propertyIsEnumerable(ext) &&
!instance.propertyIsEnumerable(ext))
instance[ext] = extensions[ext];
+ for (var i = 0; i < initHooks.length; ++i) initHooks[i](instance);
return instance;
} // (end of function CodeMirror)
@@ -2027,7 +2015,6 @@ window.CodeMirror = (function() {
onCursorActivity: null,
onViewportChange: null,
onGutterClick: null,
- onHighlightComplete: null,
onUpdate: null,
onFocus: null, onBlur: null, onScroll: null,
matchBrackets: false,
@@ -2070,7 +2057,13 @@ window.CodeMirror = (function() {
var spec = CodeMirror.resolveMode(spec);
var mfactory = modes[spec.name];
if (!mfactory) return CodeMirror.getMode(options, "text/plain");
- return mfactory(options, spec);
+ var modeObj = mfactory(options, spec);
+ if (modeExtensions.hasOwnProperty(spec.name)) {
+ var exts = modeExtensions[spec.name];
+ for (var prop in exts) if (exts.hasOwnProperty(prop)) modeObj[prop] = exts[prop];
+ }
+ modeObj.name = spec.name;
+ return modeObj;
};
CodeMirror.listModes = function() {
var list = [];
@@ -2090,6 +2083,16 @@ window.CodeMirror = (function() {
extensions[name] = func;
};
+ var initHooks = [];
+ CodeMirror.defineInitHook = function(f) {initHooks.push(f);};
+
+ var modeExtensions = CodeMirror.modeExtensions = {};
+ CodeMirror.extendMode = function(mode, properties) {
+ var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {});
+ for (var prop in properties) if (properties.hasOwnProperty(prop))
+ exts[prop] = properties[prop];
+ };
+
var commands = CodeMirror.commands = {
selectAll: function(cm) {cm.setSelection({line: 0, ch: 0}, {line: cm.lineCount() - 1});},
killLine: function(cm) {
@@ -2175,7 +2178,7 @@ window.CodeMirror = (function() {
keyMap.emacsy = {
"Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
"Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
- "Ctrl-V": "goPageUp", "Shift-Ctrl-V": "goPageDown", "Ctrl-D": "delCharRight", "Ctrl-H": "delCharLeft",
+ "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharRight", "Ctrl-H": "delCharLeft",
"Alt-D": "delWordRight", "Alt-Backspace": "delWordLeft", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars"
};
@@ -2212,6 +2215,7 @@ window.CodeMirror = (function() {
var name = keyNames[e_prop(event, "keyCode")];
return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
}
+ CodeMirror.isModifierKey = isModifierKey;
CodeMirror.fromTextArea = function(textarea, options) {
if (!options) options = {};
@@ -2232,15 +2236,13 @@ window.CodeMirror = (function() {
if (textarea.form) {
// Deplorable hack to make the submit method do the right thing.
var rmSubmit = connect(textarea.form, "submit", save, true);
- if (typeof textarea.form.submit == "function") {
- var realSubmit = textarea.form.submit;
- textarea.form.submit = function wrappedSubmit() {
- save();
- textarea.form.submit = realSubmit;
- textarea.form.submit();
- textarea.form.submit = wrappedSubmit;
- };
- }
+ var realSubmit = textarea.form.submit;
+ textarea.form.submit = function wrappedSubmit() {
+ save();
+ textarea.form.submit = realSubmit;
+ textarea.form.submit();
+ textarea.form.submit = wrappedSubmit;
+ };
}
textarea.style.display = "none";
@@ -2292,6 +2294,14 @@ window.CodeMirror = (function() {
return mode.startState ? mode.startState(a1, a2) : true;
}
CodeMirror.startState = startState;
+ CodeMirror.innerMode = function(mode, state) {
+ while (mode.innerMode) {
+ var info = mode.innerMode(state);
+ state = info.state;
+ mode = info.mode;
+ }
+ return info || {mode: mode, state: state};
+ };
// The character stream used by a mode's parser.
function StringStream(string, tabSize) {
@@ -2340,6 +2350,7 @@ window.CodeMirror = (function() {
}
} else {
var match = this.string.slice(this.pos).match(pattern);
+ if (match && match.index > 0) return null;
if (match && consume !== false) this.pos += match[0].length;
return match;
}
@@ -2348,69 +2359,135 @@ window.CodeMirror = (function() {
};
CodeMirror.StringStream = StringStream;
- function MarkedText(from, to, className, marker) {
- this.from = from; this.to = to; this.style = className; this.marker = marker;
+ function MarkedSpan(from, to, marker) {
+ this.from = from; this.to = to; this.marker = marker;
}
- MarkedText.prototype = {
- attach: function(line) { this.marker.set.push(line); },
- detach: function(line) {
- var ix = indexOf(this.marker.set, line);
- if (ix > -1) this.marker.set.splice(ix, 1);
- },
- split: function(pos, lenBefore) {
- if (this.to <= pos && this.to != null) return null;
- var from = this.from < pos || this.from == null ? null : this.from - pos + lenBefore;
- var to = this.to == null ? null : this.to - pos + lenBefore;
- return new MarkedText(from, to, this.style, this.marker);
- },
- dup: function() { return new MarkedText(null, null, this.style, this.marker); },
- clipTo: function(fromOpen, from, toOpen, to, diff) {
- if (fromOpen && to > this.from && (to < this.to || this.to == null))
- this.from = null;
- else if (this.from != null && this.from >= from)
- this.from = Math.max(to, this.from) + diff;
- if (toOpen && (from < this.to || this.to == null) && (from > this.from || this.from == null))
- this.to = null;
- else if (this.to != null && this.to > from)
- this.to = to < this.to ? this.to + diff : from;
- },
- isDead: function() { return this.from != null && this.to != null && this.from >= this.to; },
- sameSet: function(x) { return this.marker == x.marker; }
- };
- function Bookmark(pos) {
- this.from = pos; this.to = pos; this.line = null;
+ function getMarkedSpanFor(spans, marker) {
+ if (spans) for (var i = 0; i < spans.length; ++i) {
+ var span = spans[i];
+ if (span.marker == marker) return span;
+ }
}
- Bookmark.prototype = {
- attach: function(line) { this.line = line; },
- detach: function(line) { if (this.line == line) this.line = null; },
- split: function(pos, lenBefore) {
- if (pos < this.from) {
- this.from = this.to = (this.from - pos) + lenBefore;
- return this;
+
+ function removeMarkedSpan(spans, span) {
+ var r;
+ for (var i = 0; i < spans.length; ++i)
+ if (spans[i] != span) (r || (r = [])).push(spans[i]);
+ return r;
+ }
+
+ function markedSpansBefore(old, startCh, endCh) {
+ if (old) for (var i = 0, nw; i < old.length; ++i) {
+ var span = old[i], marker = span.marker;
+ var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh);
+ if (startsBefore || marker.type == "bookmark" && span.from == startCh && span.from != endCh) {
+ var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);
+ (nw || (nw = [])).push({from: span.from,
+ to: endsAfter ? null : span.to,
+ marker: marker});
}
- },
- isDead: function() { return this.from > this.to; },
- clipTo: function(fromOpen, from, toOpen, to, diff) {
- if ((fromOpen || from < this.from) && (toOpen || to > this.to)) {
- this.from = 0; this.to = -1;
- } else if (this.from > from) {
- this.from = this.to = Math.max(to, this.from) + diff;
+ }
+ return nw;
+ }
+
+ function markedSpansAfter(old, endCh) {
+ if (old) for (var i = 0, nw; i < old.length; ++i) {
+ var span = old[i], marker = span.marker;
+ var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh);
+ if (endsAfter || marker.type == "bookmark" && span.from == endCh) {
+ var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);
+ (nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh,
+ to: span.to == null ? null : span.to - endCh,
+ marker: marker});
}
- },
- sameSet: function(x) { return false; },
- find: function() {
- if (!this.line || !this.line.parent) return null;
- return {line: lineNo(this.line), ch: this.from};
- },
- clear: function() {
- if (this.line) {
- var found = indexOf(this.line.marked, this);
- if (found != -1) this.line.marked.splice(found, 1);
- this.line = null;
+ }
+ return nw;
+ }
+
+ function updateMarkedSpans(oldFirst, oldLast, startCh, endCh, newText) {
+ if (!oldFirst && !oldLast) return newText;
+ // Get the spans that 'stick out' on both sides
+ var first = markedSpansBefore(oldFirst, startCh);
+ var last = markedSpansAfter(oldLast, endCh);
+
+ // Next, merge those two ends
+ var sameLine = newText.length == 1, offset = lst(newText).length + (sameLine ? startCh : 0);
+ if (first) {
+ // Fix up .to properties of first
+ for (var i = 0; i < first.length; ++i) {
+ var span = first[i];
+ if (span.to == null) {
+ var found = getMarkedSpanFor(last, span.marker);
+ if (!found) span.to = startCh;
+ else if (sameLine) span.to = found.to == null ? null : found.to + offset;
+ }
+ }
+ }
+ if (last) {
+ // Fix up .from in last (or move them into first in case of sameLine)
+ for (var i = 0; i < last.length; ++i) {
+ var span = last[i];
+ if (span.to != null) span.to += offset;
+ if (span.from == null) {
+ var found = getMarkedSpanFor(first, span.marker);
+ if (!found) {
+ span.from = offset;
+ if (sameLine) (first || (first = [])).push(span);
+ }
+ } else {
+ span.from += offset;
+ if (sameLine) (first || (first = [])).push(span);
+ }
}
}
- };
+
+ var newMarkers = [newHL(newText[0], first)];
+ if (!sameLine) {
+ // Fill gap with whole-line-spans
+ var gap = newText.length - 2, gapMarkers;
+ if (gap > 0 && first)
+ for (var i = 0; i < first.length; ++i)
+ if (first[i].to == null)
+ (gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker});
+ for (var i = 0; i < gap; ++i)
+ newMarkers.push(newHL(newText[i+1], gapMarkers));
+ newMarkers.push(newHL(lst(newText), last));
+ }
+ return newMarkers;
+ }
+
+ // hl stands for history-line, a data structure that can be either a
+ // string (line without markers) or a {text, markedSpans} object.
+ function hlText(val) { return typeof val == "string" ? val : val.text; }
+ function hlSpans(val) {
+ if (typeof val == "string") return null;
+ var spans = val.markedSpans, out = null;
+ for (var i = 0; i < spans.length; ++i) {
+ if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); }
+ else if (out) out.push(spans[i]);
+ }
+ return !out ? spans : out.length ? out : null;
+ }
+ function newHL(text, spans) { return spans ? {text: text, markedSpans: spans} : text; }
+
+ function detachMarkedSpans(line) {
+ var spans = line.markedSpans;
+ if (!spans) return;
+ for (var i = 0; i < spans.length; ++i) {
+ var lines = spans[i].marker.lines;
+ var ix = indexOf(lines, line);
+ lines.splice(ix, 1);
+ }
+ line.markedSpans = null;
+ }
+
+ function attachMarkedSpans(line, spans) {
+ if (!spans) return;
+ for (var i = 0; i < spans.length; ++i)
+ var marker = spans[i].marker.lines.push(line);
+ line.markedSpans = spans;
+ }
// When measuring the position of the end of a line, different
// browsers require different approaches. If an empty span is added,
@@ -2424,142 +2501,32 @@ window.CodeMirror = (function() {
// Line objects. These hold state related to a line, including
// highlighting info (the styles array).
- function Line(text, styles) {
- this.styles = styles || [text, null];
+ function Line(text, markedSpans) {
this.text = text;
this.height = 1;
+ attachMarkedSpans(this, markedSpans);
}
- Line.inheritMarks = function(text, orig) {
- var ln = new Line(text), mk = orig && orig.marked;
- if (mk) {
- for (var i = 0; i < mk.length; ++i) {
- if (mk[i].to == null && mk[i].style) {
- var newmk = ln.marked || (ln.marked = []), mark = mk[i];
- var nmark = mark.dup(); newmk.push(nmark); nmark.attach(ln);
- }
- }
- }
- return ln;
- };
Line.prototype = {
- // Replace a piece of a line, keeping the styles around it intact.
- replace: function(from, to_, text) {
- var st = [], mk = this.marked, to = to_ == null ? this.text.length : to_;
- copyStyles(0, from, this.styles, st);
- if (text) st.push(text, null);
- copyStyles(to, this.text.length, this.styles, st);
- this.styles = st;
- this.text = this.text.slice(0, from) + text + this.text.slice(to);
- this.stateAfter = null;
- if (mk) {
- var diff = text.length - (to - from);
- for (var i = 0; i < mk.length; ++i) {
- var mark = mk[i];
- mark.clipTo(from == null, from || 0, to_ == null, to, diff);
- if (mark.isDead()) {mark.detach(this); mk.splice(i--, 1);}
- }
- }
- },
- // Split a part off a line, keeping styles and markers intact.
- split: function(pos, textBefore) {
- var st = [textBefore, null], mk = this.marked;
- copyStyles(pos, this.text.length, this.styles, st);
- var taken = new Line(textBefore + this.text.slice(pos), st);
- if (mk) {
- for (var i = 0; i < mk.length; ++i) {
- var mark = mk[i];
- var newmark = mark.split(pos, textBefore.length);
- if (newmark) {
- if (!taken.marked) taken.marked = [];
- taken.marked.push(newmark); newmark.attach(taken);
- if (newmark == mark) mk.splice(i--, 1);
- }
- }
- }
- return taken;
- },
- append: function(line) {
- var mylen = this.text.length, mk = line.marked, mymk = this.marked;
- this.text += line.text;
- copyStyles(0, line.text.length, line.styles, this.styles);
- if (mymk) {
- for (var i = 0; i < mymk.length; ++i)
- if (mymk[i].to == null) mymk[i].to = mylen;
- }
- if (mk && mk.length) {
- if (!mymk) this.marked = mymk = [];
- outer: for (var i = 0; i < mk.length; ++i) {
- var mark = mk[i];
- if (!mark.from) {
- for (var j = 0; j < mymk.length; ++j) {
- var mymark = mymk[j];
- if (mymark.to == mylen && mymark.sameSet(mark)) {
- mymark.to = mark.to == null ? null : mark.to + mylen;
- if (mymark.isDead()) {
- mymark.detach(this);
- mk.splice(i--, 1);
- }
- continue outer;
- }
- }
- }
- mymk.push(mark);
- mark.attach(this);
- mark.from += mylen;
- if (mark.to != null) mark.to += mylen;
- }
- }
- },
- fixMarkEnds: function(other) {
- var mk = this.marked, omk = other.marked;
- if (!mk) return;
- outer: for (var i = 0; i < mk.length; ++i) {
- var mark = mk[i], close = mark.to == null;
- if (close && omk) {
- for (var j = 0; j < omk.length; ++j) {
- var om = omk[j];
- if (!om.sameSet(mark) || om.from != null) continue;
- if (mark.from == this.text.length && om.to == 0) {
- omk.splice(j, 1);
- mk.splice(i--, 1);
- continue outer;
- } else {
- close = false; break;
- }
- }
- }
- if (close) mark.to = this.text.length;
- }
- },
- fixMarkStarts: function() {
- var mk = this.marked;
- if (!mk) return;
- for (var i = 0; i < mk.length; ++i)
- if (mk[i].from == null) mk[i].from = 0;
- },
- addMark: function(mark) {
- mark.attach(this);
- if (this.marked == null) this.marked = [];
- this.marked.push(mark);
- this.marked.sort(function(a, b){return (a.from || 0) - (b.from || 0);});
+ update: function(text, markedSpans) {
+ this.text = text;
+ this.stateAfter = this.styles = null;
+ detachMarkedSpans(this);
+ attachMarkedSpans(this, markedSpans);
},
// Run the given mode's parser over a line, update the styles
// array, which contains alternating fragments of text and CSS
// classes.
highlight: function(mode, state, tabSize) {
- var stream = new StringStream(this.text, tabSize), st = this.styles, pos = 0;
- var changed = false, curWord = st[0], prevWord;
+ var stream = new StringStream(this.text, tabSize), st = this.styles || (this.styles = []);
+ var pos = st.length = 0;
if (this.text == "" && mode.blankLine) mode.blankLine(state);
while (!stream.eol()) {
- var style = mode.token(stream, state);
- var substr = this.text.slice(stream.start, stream.pos);
+ var style = mode.token(stream, state), substr = stream.current();
stream.start = stream.pos;
- if (pos && st[pos-1] == style)
+ if (pos && st[pos-1] == style) {
st[pos-2] += substr;
- else if (substr) {
- if (!changed && (st[pos+1] != style || (pos && st[pos-2] != prevWord))) changed = true;
+ } else if (substr) {
st[pos++] = substr; st[pos++] = style;
- prevWord = curWord; curWord = st[pos];
}
// Give up when line is ridiculously long
if (stream.pos > 5000) {
@@ -2567,12 +2534,14 @@ window.CodeMirror = (function() {
break;
}
}
- if (st.length != pos) {st.length = pos; changed = true;}
- if (pos && st[pos-2] != prevWord) changed = true;
- // Short lines with simple highlights return null, and are
- // counted as changed by the driver because they are likely to
- // highlight the same way in various contexts.
- return changed || (st.length < 5 && this.text.length < 10 ? null : false);
+ },
+ process: function(mode, state, tabSize) {
+ var stream = new StringStream(this.text, tabSize);
+ if (this.text == "" && mode.blankLine) mode.blankLine(state);
+ while (!stream.eol() && stream.pos <= 5000) {
+ mode.token(stream, state);
+ stream.start = stream.pos;
+ }
},
// Fetch the parser token for a given character. Useful for hacks
// that want to inspect the mode state (say, for completion).
@@ -2591,7 +2560,7 @@ window.CodeMirror = (function() {
indentation: function(tabSize) {return countColumn(this.text, null, tabSize);},
// Produces an HTML fragment for the line, taking selection,
// marking, and highlighting into account.
- getElement: function(makeTab, wrapAt, wrapWBR) {
+ getContent: function(tabSize, wrapAt, compensateForWrapping) {
var first = true, col = 0, specials = /[\t\u0000-\u0019\u200b\u2028\u2029\uFEFF]/g;
var pre = elt("pre");
function span_(html, text, style) {
@@ -2615,9 +2584,9 @@ window.CodeMirror = (function() {
if (!m) break;
pos += skipped + 1;
if (m[0] == "\t") {
- var tab = makeTab(col);
- content.appendChild(tab.element.cloneNode(true));
- col += tab.width;
+ var tabWidth = tabSize - col % tabSize;
+ content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"));
+ col += tabWidth;
} else {
var token = elt("span", "\u2022", "cm-invalidchar");
token.title = "\\u" + m[0].charCodeAt(0).toString(16);
@@ -2635,13 +2604,17 @@ window.CodeMirror = (function() {
span = function(html, text, style) {
var l = text.length;
if (wrapAt >= outPos && wrapAt < outPos + l) {
- if (wrapAt > outPos) {
- span_(html, text.slice(0, wrapAt - outPos), style);
+ var cut = wrapAt - outPos;
+ if (cut) {
+ span_(html, text.slice(0, cut), style);
// See comment at the definition of spanAffectsWrapping
- if (wrapWBR) html.appendChild(elt("wbr"));
+ if (compensateForWrapping) {
+ var view = text.slice(cut - 1, cut + 1);
+ if (spanAffectsWrapping.test(view)) html.appendChild(elt("wbr"));
+ else if (!ie_lt8 && /\w\w/.test(view)) html.appendChild(document.createTextNode("\u200d"));
+ }
}
html.appendChild(anchor);
- var cut = wrapAt - outPos;
span_(anchor, opera ? text.slice(cut, cut + 1) : text.slice(cut), style);
if (opera) span_(html, text.slice(cut + 1), style);
wrapAt--;
@@ -2659,7 +2632,7 @@ window.CodeMirror = (function() {
};
}
- var st = this.styles, allText = this.text, marked = this.marked;
+ var st = this.styles, allText = this.text, marked = this.markedSpans;
var len = allText.length;
function styleToClass(style) {
if (!style) return null;
@@ -2675,13 +2648,14 @@ window.CodeMirror = (function() {
span(pre, str, styleToClass(style));
}
} else {
+ marked.sort(function(a, b) { return a.from - b.from; });
var pos = 0, i = 0, text = "", style, sg = 0;
var nextChange = marked[0].from || 0, marks = [], markpos = 0;
var advanceMarks = function() {
var m;
while (markpos < marked.length &&
((m = marked[markpos]).from == pos || m.from == null)) {
- if (m.style != null) marks.push(m);
+ if (m.marker.type == "range") marks.push(m);
++markpos;
}
nextChange = markpos < marked.length ? marked[markpos].from : Infinity;
@@ -2700,8 +2674,12 @@ window.CodeMirror = (function() {
if (text) {
var end = pos + text.length;
var appliedStyle = style;
- for (var j = 0; j < marks.length; ++j)
- appliedStyle = (appliedStyle ? appliedStyle + " " : "") + marks[j].style;
+ for (var j = 0; j < marks.length; ++j) {
+ var mark = marks[j];
+ appliedStyle = (appliedStyle ? appliedStyle + " " : "") + mark.marker.style;
+ if (mark.marker.endStyle && mark.to === Math.min(end, upto)) appliedStyle += " " + mark.marker.endStyle;
+ if (mark.marker.startStyle && mark.from === pos) appliedStyle += " " + mark.marker.startStyle;
+ }
span(pre, end > upto ? text.slice(0, upto - pos) : text, appliedStyle);
if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
pos = end;
@@ -2714,24 +2692,9 @@ window.CodeMirror = (function() {
},
cleanUp: function() {
this.parent = null;
- if (this.marked)
- for (var i = 0, e = this.marked.length; i < e; ++i) this.marked[i].detach(this);
+ detachMarkedSpans(this);
}
};
- // Utility used by replace and split above
- function copyStyles(from, to, source, dest) {
- for (var i = 0, pos = 0, state = 0; pos < to; i+=2) {
- var part = source[i], end = pos + part.length;
- if (state == 0) {
- if (end > from) dest.push(part.slice(from - pos, Math.min(part.length, to - pos)), source[i+1]);
- if (end >= from) state = 1;
- } else if (state == 1) {
- if (end > to) dest.push(part.slice(0, to - pos), source[i+1]);
- else dest.push(part, source[i+1]);
- }
- pos = end;
- }
- }
// Data structure that holds the sequence of lines.
function LeafChunk(lines) {
@@ -2932,10 +2895,10 @@ window.CodeMirror = (function() {
History.prototype = {
addChange: function(start, added, old) {
this.undone.length = 0;
- var time = +new Date, cur = this.done[this.done.length - 1], last = cur && cur[cur.length - 1];
+ var time = +new Date, cur = lst(this.done), last = cur && lst(cur);
var dtime = time - this.time;
- if (this.compound && cur && !this.closed) {
+ if (cur && !this.closed && this.compound) {
cur.push({start: start, added: added, old: old});
} else if (dtime > 400 || !last || this.closed ||
last.start > start + old.length || last.start + last.added < start) {
@@ -3078,10 +3041,19 @@ window.CodeMirror = (function() {
return box;
}
- // Get a node's text content.
function eltText(node) {
return node.textContent || node.innerText || node.nodeValue || "";
}
+
+ var spaceStrs = [""];
+ function spaceStr(n) {
+ while (spaceStrs.length <= n)
+ spaceStrs.push(lst(spaceStrs) + " ");
+ return spaceStrs[n];
+ }
+
+ function lst(arr) { return arr[arr.length-1]; }
+
function selectInput(node) {
if (ios) { // Mobile Safari apparently has a bug where select() is broken.
node.selectionStart = 0;
@@ -3115,7 +3087,6 @@ window.CodeMirror = (function() {
e.appendChild(document.createTextNode(str));
} else e.textContent = str;
}
- CodeMirror.setTextContent = setTextContent;
// Used to position the cursor after an undo/redo by finding the
// last edited character.
@@ -3133,8 +3104,10 @@ window.CodeMirror = (function() {
if (collection[i] == elt) return i;
return -1;
}
+ var nonASCIISingleCaseWordChar = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc]/;
function isWordChar(ch) {
- return /\w/.test(ch) || ch.toUpperCase() != ch.toLowerCase();
+ return /\w/.test(ch) || ch > "\x80" &&
+ (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch));
}
// See if "".split is the broken IE version, if so, provide an
@@ -3190,5 +3163,7 @@ window.CodeMirror = (function() {
for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
})();
+ CodeMirror.version = "2.35 +";
+
return CodeMirror;
})();
diff --git a/lib/util/closetag.js b/lib/util/closetag.js
index 656e93c288..5096678473 100644
--- a/lib/util/closetag.js
+++ b/lib/util/closetag.js
@@ -26,6 +26,11 @@
/** Array of tag names where an end tag is forbidden. */
CodeMirror.defaults['closeTagVoid'] = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
+ function innerState(cm, state) {
+ return CodeMirror.innerMode(cm.getMode(), state).state;
+ }
+
+
/**
* Call during key processing to close tags. Handles the key event if the tag is closed, otherwise throws CodeMirror.Pass.
* - cm: The editor instance.
@@ -39,40 +44,34 @@
throw CodeMirror.Pass;
}
- var mode = cm.getOption('mode');
-
- if (mode == 'text/html' || mode == 'xml') {
+ /*
+ * Relevant structure of token:
+ *
+ * htmlmixed
+ * className
+ * state
+ * htmlState
+ * type
+ * tagName
+ * context
+ * tagName
+ * mode
+ *
+ * xml
+ * className
+ * state
+ * tagName
+ * type
+ */
- /*
- * Relevant structure of token:
- *
- * htmlmixed
- * className
- * state
- * htmlState
- * type
- * tagName
- * context
- * tagName
- * mode
- *
- * xml
- * className
- * state
- * tagName
- * type
- */
-
- var pos = cm.getCursor();
- var tok = cm.getTokenAt(pos);
- var state = tok.state;
-
- if (state.mode && state.mode != 'html') {
- throw CodeMirror.Pass; // With htmlmixed, we only care about the html sub-mode.
- }
+ var pos = cm.getCursor();
+ var tok = cm.getTokenAt(pos);
+ var state = innerState(cm, tok.state);
+
+ if (state) {
if (ch == '>') {
- var type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml
+ var type = state.type;
if (tok.className == 'tag' && type == 'closeTag') {
throw CodeMirror.Pass; // Don't process the '>' at the end of an end-tag.
@@ -83,11 +82,12 @@
cm.setCursor(pos);
tok = cm.getTokenAt(cm.getCursor());
- state = tok.state;
- type = state.htmlState ? state.htmlState.type : state.type; // htmlmixed : xml
+ state = innerState(cm, tok.state);
+ if (!state) throw CodeMirror.Pass;
+ var type = state.type;
if (tok.className == 'tag' && type != 'selfcloseTag') {
- var tagName = state.htmlState ? state.htmlState.tagName : state.tagName; // htmlmixed : xml
+ var tagName = state.tagName;
if (tagName.length > 0 && shouldClose(cm, vd, tagName)) {
insertEndTag(cm, indent, pos, tagName);
}
@@ -100,7 +100,7 @@
} else if (ch == '/') {
if (tok.className == 'tag' && tok.string == '<') {
- var tagName = state.htmlState ? (state.htmlState.context ? state.htmlState.context.tagName : '') : (state.context ? state.context.tagName : ''); // htmlmixed : xml
+ var ctx = state.context, tagName = ctx ? ctx.tagName : '';
if (tagName.length > 0) {
completeEndTag(cm, pos, tagName);
return;
diff --git a/lib/util/formatting.js b/lib/util/formatting.js
index 22c943fb40..00ffe78720 100644
--- a/lib/util/formatting.js
+++ b/lib/util/formatting.js
@@ -1,110 +1,22 @@
// ============== Formatting extensions ============================
-// A common storage for all mode-specific formatting features
-if (!CodeMirror.modeExtensions) CodeMirror.modeExtensions = {};
-
-// Returns the extension of the editor's current mode
-CodeMirror.defineExtension("getModeExt", function () {
- var mname = CodeMirror.resolveMode(this.getOption("mode")).name;
- var ext = CodeMirror.modeExtensions[mname];
- if (!ext) throw new Error("No extensions found for mode " + mname);
- return ext;
-});
-
-// If the current mode is 'htmlmixed', returns the extension of a mode located at
-// the specified position (can be htmlmixed, css or javascript). Otherwise, simply
-// returns the extension of the editor's current mode.
-CodeMirror.defineExtension("getModeExtAtPos", function (pos) {
- var token = this.getTokenAt(pos);
- if (token && token.state && token.state.mode)
- return CodeMirror.modeExtensions[token.state.mode == "html" ? "htmlmixed" : token.state.mode];
- else
- return this.getModeExt();
-});
-
-// Comment/uncomment the specified range
-CodeMirror.defineExtension("commentRange", function (isComment, from, to) {
- var curMode = this.getModeExtAtPos(this.getCursor());
- if (isComment) { // Comment range
- var commentedText = this.getRange(from, to);
- this.replaceRange(curMode.commentStart + this.getRange(from, to) + curMode.commentEnd
- , from, to);
- if (from.line == to.line && from.ch == to.ch) { // An empty comment inserted - put cursor inside
- this.setCursor(from.line, from.ch + curMode.commentStart.length);
- }
- }
- else { // Uncomment range
- var selText = this.getRange(from, to);
- var startIndex = selText.indexOf(curMode.commentStart);
- var endIndex = selText.lastIndexOf(curMode.commentEnd);
- if (startIndex > -1 && endIndex > -1 && endIndex > startIndex) {
- // Take string till comment start
- selText = selText.substr(0, startIndex)
- // From comment start till comment end
- + selText.substring(startIndex + curMode.commentStart.length, endIndex)
- // From comment end till string end
- + selText.substr(endIndex + curMode.commentEnd.length);
- }
- this.replaceRange(selText, from, to);
- }
-});
-
-// Applies automatic mode-aware indentation to the specified range
-CodeMirror.defineExtension("autoIndentRange", function (from, to) {
- var cmInstance = this;
- this.operation(function () {
- for (var i = from.line; i <= to.line; i++) {
- cmInstance.indentLine(i, "smart");
+(function() {
+ // Define extensions for a few modes
+ CodeMirror.extendMode("css", {
+ commentStart: "/*",
+ commentEnd: "*/",
+ wordWrapChars: [";", "\\{", "\\}"],
+ autoFormatLineBreaks: function (text) {
+ return text.replace(new RegExp("(;|\\{|\\})([^\r\n])", "g"), "$1\n$2");
}
});
-});
-
-// Applies automatic formatting to the specified range
-CodeMirror.defineExtension("autoFormatRange", function (from, to) {
- var absStart = this.indexFromPos(from);
- var absEnd = this.indexFromPos(to);
- // Insert additional line breaks where necessary according to the
- // mode's syntax
- var res = this.getModeExt().autoFormatLineBreaks(this.getValue(), absStart, absEnd);
- var cmInstance = this;
-
- // Replace and auto-indent the range
- this.operation(function () {
- cmInstance.replaceRange(res, from, to);
- var startLine = cmInstance.posFromIndex(absStart).line;
- var endLine = cmInstance.posFromIndex(absStart + res.length).line;
- for (var i = startLine; i <= endLine; i++) {
- cmInstance.indentLine(i, "smart");
- }
- });
-});
-
-// Define extensions for a few modes
-
-CodeMirror.modeExtensions["css"] = {
- commentStart: "/*",
- commentEnd: "*/",
- wordWrapChars: [";", "\\{", "\\}"],
- autoFormatLineBreaks: function (text, startPos, endPos) {
- text = text.substring(startPos, endPos);
- return text.replace(new RegExp("(;|\\{|\\})([^\r\n])", "g"), "$1\n$2");
- }
-};
-
-CodeMirror.modeExtensions["javascript"] = {
- commentStart: "/*",
- commentEnd: "*/",
- wordWrapChars: [";", "\\{", "\\}"],
- getNonBreakableBlocks: function (text) {
- var nonBreakableRegexes = [
- new RegExp("for\\s*?\\(([\\s\\S]*?)\\)"),
- new RegExp("\\\\\"([\\s\\S]*?)(\\\\\"|$)"),
- new RegExp("\\\\\'([\\s\\S]*?)(\\\\\'|$)"),
- new RegExp("'([\\s\\S]*?)('|$)"),
- new RegExp("\"([\\s\\S]*?)(\"|$)"),
- new RegExp("//.*([\r\n]|$)")
- ];
- var nonBreakableBlocks = new Array();
+ function jsNonBreakableBlocks(text) {
+ var nonBreakableRegexes = [/for\s*?\((.*?)\)/,
+ /\"(.*?)(\"|$)/,
+ /\'(.*?)(\'|$)/,
+ /\/\*(.*?)(\*\/|$)/,
+ /\/\/.*/];
+ var nonBreakableBlocks = [];
for (var i = 0; i < nonBreakableRegexes.length; i++) {
var curPos = 0;
while (curPos < text.length) {
@@ -126,174 +38,159 @@ CodeMirror.modeExtensions["javascript"] = {
});
return nonBreakableBlocks;
- },
+ }
- autoFormatLineBreaks: function (text, startPos, endPos) {
- text = text.substring(startPos, endPos);
- var curPos = 0;
- var reLinesSplitter = new RegExp("(;|\\{|\\})([^\r\n;])", "g");
- var nonBreakableBlocks = this.getNonBreakableBlocks(text);
- if (nonBreakableBlocks != null) {
- var res = "";
- for (var i = 0; i < nonBreakableBlocks.length; i++) {
- if (nonBreakableBlocks[i].start > curPos) { // Break lines till the block
- res += text.substring(curPos, nonBreakableBlocks[i].start).replace(reLinesSplitter, "$1\n$2");
- curPos = nonBreakableBlocks[i].start;
- }
- if (nonBreakableBlocks[i].start <= curPos
- && nonBreakableBlocks[i].end >= curPos) { // Skip non-breakable block
- res += text.substring(curPos, nonBreakableBlocks[i].end);
- curPos = nonBreakableBlocks[i].end;
+ CodeMirror.extendMode("javascript", {
+ commentStart: "/*",
+ commentEnd: "*/",
+ wordWrapChars: [";", "\\{", "\\}"],
+
+ autoFormatLineBreaks: function (text) {
+ var curPos = 0;
+ var split = this.jsonMode ? function(str) {
+ return str.replace(/([,{])/g, "$1\n").replace(/}/g, "\n}");
+ } : function(str) {
+ return str.replace(/(;|\{|\})([^\r\n;])/g, "$1\n$2");
+ };
+ var nonBreakableBlocks = jsNonBreakableBlocks(text), res = "";
+ if (nonBreakableBlocks != null) {
+ for (var i = 0; i < nonBreakableBlocks.length; i++) {
+ if (nonBreakableBlocks[i].start > curPos) { // Break lines till the block
+ res += split(text.substring(curPos, nonBreakableBlocks[i].start));
+ curPos = nonBreakableBlocks[i].start;
+ }
+ if (nonBreakableBlocks[i].start <= curPos
+ && nonBreakableBlocks[i].end >= curPos) { // Skip non-breakable block
+ res += text.substring(curPos, nonBreakableBlocks[i].end);
+ curPos = nonBreakableBlocks[i].end;
+ }
}
+ if (curPos < text.length)
+ res += split(text.substr(curPos));
+ } else {
+ res = split(text);
}
- if (curPos < text.length - 1) {
- res += text.substr(curPos).replace(reLinesSplitter, "$1\n$2");
- }
- return res;
+ return res.replace(/^\n*|\n*$/, "");
}
- else {
- return text.replace(reLinesSplitter, "$1\n$2");
- }
- }
-};
-
-CodeMirror.modeExtensions["xml"] = {
- commentStart: "",
- wordWrapChars: [">"],
+ });
- autoFormatLineBreaks: function (text, startPos, endPos) {
- text = text.substring(startPos, endPos);
- var lines = text.split("\n");
- var reProcessedPortion = new RegExp("(^\\s*?<|^[^<]*?)(.+)(>\\s*?$|[^>]*?$)");
- var reOpenBrackets = new RegExp("<", "g");
- var reCloseBrackets = new RegExp("(>)([^\r\n])", "g");
- for (var i = 0; i < lines.length; i++) {
- var mToProcess = lines[i].match(reProcessedPortion);
- if (mToProcess != null && mToProcess.length > 3) { // The line starts with whitespaces and ends with whitespaces
- lines[i] = mToProcess[1]
+ CodeMirror.extendMode("xml", {
+ commentStart: "",
+ wordWrapChars: [">"],
+
+ autoFormatLineBreaks: function (text) {
+ var lines = text.split("\n");
+ var reProcessedPortion = new RegExp("(^\\s*?<|^[^<]*?)(.+)(>\\s*?$|[^>]*?$)");
+ var reOpenBrackets = new RegExp("<", "g");
+ var reCloseBrackets = new RegExp("(>)([^\r\n])", "g");
+ for (var i = 0; i < lines.length; i++) {
+ var mToProcess = lines[i].match(reProcessedPortion);
+ if (mToProcess != null && mToProcess.length > 3) { // The line starts with whitespaces and ends with whitespaces
+ lines[i] = mToProcess[1]
+ mToProcess[2].replace(reOpenBrackets, "\n$&").replace(reCloseBrackets, "$1\n$2")
+ mToProcess[3];
- continue;
+ continue;
+ }
}
+ return lines.join("\n");
}
+ });
- return lines.join("\n");
+ function localModeAt(cm, pos) {
+ return CodeMirror.innerMode(cm.getMode(), cm.getTokenAt(pos).state).mode;
}
-};
-CodeMirror.modeExtensions["htmlmixed"] = {
- commentStart: "",
- wordWrapChars: [">", ";", "\\{", "\\}"],
-
- getModeInfos: function (text, absPos) {
- var modeInfos = new Array();
- modeInfos[0] =
- {
- pos: 0,
- modeExt: CodeMirror.modeExtensions["xml"],
- modeName: "xml"
- };
-
- var modeMatchers = new Array();
- modeMatchers[0] =
- {
- regex: new RegExp("]*>|$)", "i"),
- modeExt: CodeMirror.modeExtensions["css"],
- modeName: "css"
- };
- modeMatchers[1] =
- {
- regex: new RegExp("]*>|$)", "i"),
- modeExt: CodeMirror.modeExtensions["javascript"],
- modeName: "javascript"
- };
+ function enumerateModesBetween(cm, line, start, end) {
+ var outer = cm.getMode(), text = cm.getLine(line);
+ if (end == null) end = text.length;
+ if (!outer.innerMode) return [{from: start, to: end, mode: outer}];
+ var state = cm.getTokenAt({line: line, ch: start}).state;
+ var mode = CodeMirror.innerMode(outer, state).mode;
+ var found = [], stream = new CodeMirror.StringStream(text);
+ stream.pos = stream.start = start;
+ for (;;) {
+ outer.token(stream, state);
+ var curMode = CodeMirror.innerMode(outer, state).mode;
+ if (curMode != mode) {
+ var cut = stream.start;
+ // Crappy heuristic to deal with the fact that a change in
+ // mode can occur both at the end and the start of a token,
+ // and we don't know which it was.
+ if (mode.name == "xml" && text.charAt(stream.pos - 1) == ">") cut = stream.pos;
+ found.push({from: start, to: cut, mode: mode});
+ start = cut;
+ mode = curMode;
+ }
+ if (stream.pos >= end) break;
+ stream.start = stream.pos;
+ }
+ if (start < end) found.push({from: start, to: end, mode: mode});
+ return found;
+ }
- var lastCharPos = (typeof (absPos) !== "undefined" ? absPos : text.length - 1);
- // Detect modes for the entire text
- for (var i = 0; i < modeMatchers.length; i++) {
- var curPos = 0;
- while (curPos <= lastCharPos) {
- var m = text.substr(curPos).match(modeMatchers[i].regex);
- if (m != null) {
- if (m.length > 1 && m[1].length > 0) {
- // Push block begin pos
- var blockBegin = curPos + m.index + m[0].indexOf(m[1]);
- modeInfos.push(
- {
- pos: blockBegin,
- modeExt: modeMatchers[i].modeExt,
- modeName: modeMatchers[i].modeName
- });
- // Push block end pos
- modeInfos.push(
- {
- pos: blockBegin + m[1].length,
- modeExt: modeInfos[0].modeExt,
- modeName: modeInfos[0].modeName
- });
- curPos += m.index + m[0].length;
- continue;
- }
- else {
- curPos += m.index + Math.max(m[0].length, 1);
- }
- }
- else { // No more matches
- break;
+ // Comment/uncomment the specified range
+ CodeMirror.defineExtension("commentRange", function (isComment, from, to) {
+ var curMode = localModeAt(this, from), cm = this;
+ this.operation(function() {
+ if (isComment) { // Comment range
+ cm.replaceRange(curMode.commentEnd, to);
+ cm.replaceRange(curMode.commentStart, from);
+ if (from.line == to.line && from.ch == to.ch) // An empty comment inserted - put cursor inside
+ cm.setCursor(from.line, from.ch + curMode.commentStart.length);
+ } else { // Uncomment range
+ var selText = cm.getRange(from, to);
+ var startIndex = selText.indexOf(curMode.commentStart);
+ var endIndex = selText.lastIndexOf(curMode.commentEnd);
+ if (startIndex > -1 && endIndex > -1 && endIndex > startIndex) {
+ // Take string till comment start
+ selText = selText.substr(0, startIndex)
+ // From comment start till comment end
+ + selText.substring(startIndex + curMode.commentStart.length, endIndex)
+ // From comment end till string end
+ + selText.substr(endIndex + curMode.commentEnd.length);
}
+ cm.replaceRange(selText, from, to);
}
- }
- // Sort mode infos
- modeInfos.sort(function sortModeInfo(a, b) {
- return a.pos - b.pos;
});
+ });
- return modeInfos;
- },
-
- autoFormatLineBreaks: function (text, startPos, endPos) {
- var modeInfos = this.getModeInfos(text);
- var reBlockStartsWithNewline = new RegExp("^\\s*?\n");
- var reBlockEndsWithNewline = new RegExp("\n\\s*?$");
- var res = "";
- // Use modes info to break lines correspondingly
- if (modeInfos.length > 1) { // Deal with multi-mode text
- for (var i = 1; i <= modeInfos.length; i++) {
- var selStart = modeInfos[i - 1].pos;
- var selEnd = (i < modeInfos.length ? modeInfos[i].pos : endPos);
+ // Applies automatic mode-aware indentation to the specified range
+ CodeMirror.defineExtension("autoIndentRange", function (from, to) {
+ var cmInstance = this;
+ this.operation(function () {
+ for (var i = from.line; i <= to.line; i++) {
+ cmInstance.indentLine(i, "smart");
+ }
+ });
+ });
- if (selStart >= endPos) { // The block starts later than the needed fragment
- break;
+ // Applies automatic formatting to the specified range
+ CodeMirror.defineExtension("autoFormatRange", function (from, to) {
+ var cm = this;
+ cm.operation(function () {
+ for (var cur = from.line, end = to.line; cur <= end; ++cur) {
+ var f = {line: cur, ch: cur == from.line ? from.ch : 0};
+ var t = {line: cur, ch: cur == end ? to.ch : null};
+ var modes = enumerateModesBetween(cm, cur, f.ch, t.ch), mangled = "";
+ var text = cm.getRange(f, t);
+ for (var i = 0; i < modes.length; ++i) {
+ var part = modes.length > 1 ? text.slice(modes[i].from, modes[i].to) : text;
+ if (mangled) mangled += "\n";
+ if (modes[i].mode.autoFormatLineBreaks) {
+ mangled += modes[i].mode.autoFormatLineBreaks(part);
+ } else mangled += text;
}
- if (selStart < startPos) {
- if (selEnd <= startPos) { // The block starts earlier than the needed fragment
- continue;
- }
- selStart = startPos;
- }
- if (selEnd > endPos) {
- selEnd = endPos;
+ if (mangled != text) {
+ for (var count = 0, pos = mangled.indexOf("\n"); pos != -1; pos = mangled.indexOf("\n", pos + 1), ++count) {}
+ cm.replaceRange(mangled, f, t);
+ cur += count;
+ end += count;
}
- var textPortion = text.substring(selStart, selEnd);
- if (modeInfos[i - 1].modeName != "xml") { // Starting a CSS or JavaScript block
- if (!reBlockStartsWithNewline.test(textPortion)
- && selStart > 0) { // The block does not start with a line break
- textPortion = "\n" + textPortion;
- }
- if (!reBlockEndsWithNewline.test(textPortion)
- && selEnd < text.length - 1) { // The block does not end with a line break
- textPortion += "\n";
- }
- }
- res += modeInfos[i - 1].modeExt.autoFormatLineBreaks(textPortion);
}
- }
- else { // Single-mode text
- res = modeInfos[0].modeExt.autoFormatLineBreaks(text.substring(startPos, endPos));
- }
-
- return res;
- }
-};
+ for (var cur = from.line + 1; cur <= end; ++cur)
+ cm.indentLine(cur, "smart");
+ cm.setSelection(from, cm.getCursor(false));
+ });
+ });
+})();
diff --git a/lib/util/multiplex.js b/lib/util/multiplex.js
index 755588b9c4..214730839d 100644
--- a/lib/util/multiplex.js
+++ b/lib/util/multiplex.js
@@ -68,14 +68,10 @@ CodeMirror.multiplexingMode = function(outer /*, others */) {
return mode.indent(state.innerActive ? state.inner : state.outer, textAfter);
},
- compareStates: function(a, b) {
- if (a.innerActive != b.innerActive) return false;
- var mode = a.innerActive || outer;
- if (!mode.compareStates) return CodeMirror.Pass;
- return mode.compareStates(a.innerActive ? a.inner : a.outer,
- b.innerActive ? b.inner : b.outer);
- },
+ electricChars: outer.electricChars,
- electricChars: outer.electricChars
+ innerMode: function(state) {
+ return state.inner ? {state: state.inner, mode: state.innerActive.mode} : {state: state.outer, mode: outer};
+ }
};
};
diff --git a/lib/util/overlay.js b/lib/util/overlay.js
index 1d5df6c64c..fba38987bb 100644
--- a/lib/util/overlay.js
+++ b/lib/util/overlay.js
@@ -47,6 +47,13 @@ CodeMirror.overlayMode = CodeMirror.overlayParser = function(base, overlay, comb
indent: base.indent && function(state, textAfter) {
return base.indent(state.base, textAfter);
},
- electricChars: base.electricChars
+ electricChars: base.electricChars,
+
+ innerMode: function(state) { return {state: state.base, mode: base}; },
+
+ blankLine: function(state) {
+ if (base.blankLine) base.blankLine(state.base);
+ if (overlay.blankLine) overlay.blankLine(state.overlay);
+ }
};
};
diff --git a/lib/util/runmode-standalone.js b/lib/util/runmode-standalone.js
new file mode 100644
index 0000000000..afdf044d8d
--- /dev/null
+++ b/lib/util/runmode-standalone.js
@@ -0,0 +1,90 @@
+/* Just enough of CodeMirror to run runMode under node.js */
+
+function splitLines(string){ return string.split(/\r?\n|\r/); };
+
+function StringStream(string) {
+ this.pos = this.start = 0;
+ this.string = string;
+}
+StringStream.prototype = {
+ eol: function() {return this.pos >= this.string.length;},
+ sol: function() {return this.pos == 0;},
+ peek: function() {return this.string.charAt(this.pos) || null;},
+ next: function() {
+ if (this.pos < this.string.length)
+ return this.string.charAt(this.pos++);
+ },
+ eat: function(match) {
+ var ch = this.string.charAt(this.pos);
+ if (typeof match == "string") var ok = ch == match;
+ else var ok = ch && (match.test ? match.test(ch) : match(ch));
+ if (ok) {++this.pos; return ch;}
+ },
+ eatWhile: function(match) {
+ var start = this.pos;
+ while (this.eat(match)){}
+ return this.pos > start;
+ },
+ eatSpace: function() {
+ var start = this.pos;
+ while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
+ return this.pos > start;
+ },
+ skipToEnd: function() {this.pos = this.string.length;},
+ skipTo: function(ch) {
+ var found = this.string.indexOf(ch, this.pos);
+ if (found > -1) {this.pos = found; return true;}
+ },
+ backUp: function(n) {this.pos -= n;},
+ column: function() {return this.start;},
+ indentation: function() {return 0;},
+ match: function(pattern, consume, caseInsensitive) {
+ if (typeof pattern == "string") {
+ function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
+ if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) {
+ if (consume !== false) this.pos += pattern.length;
+ return true;
+ }
+ }
+ else {
+ var match = this.string.slice(this.pos).match(pattern);
+ if (match && consume !== false) this.pos += match[0].length;
+ return match;
+ }
+ },
+ current: function(){return this.string.slice(this.start, this.pos);}
+};
+exports.StringStream = StringStream;
+
+exports.startState = function(mode, a1, a2) {
+ return mode.startState ? mode.startState(a1, a2) : true;
+};
+
+var modes = exports.modes = {}, mimeModes = exports.mimeModes = {};
+exports.defineMode = function(name, mode) { modes[name] = mode; };
+exports.defineMIME = function(mime, spec) { mimeModes[mime] = spec; };
+exports.getMode = function(options, spec) {
+ if (typeof spec == "string" && mimeModes.hasOwnProperty(spec))
+ spec = mimeModes[spec];
+ if (typeof spec == "string")
+ var mname = spec, config = {};
+ else if (spec != null)
+ var mname = spec.name, config = spec;
+ var mfactory = modes[mname];
+ if (!mfactory) throw new Error("Unknown mode: " + spec);
+ return mfactory(options, config || {});
+};
+
+exports.runMode = function(string, modespec, callback) {
+ var mode = exports.getMode({indentUnit: 2}, modespec);
+ var lines = splitLines(string), state = exports.startState(mode);
+ for (var i = 0, e = lines.length; i < e; ++i) {
+ if (i) callback("\n");
+ var stream = new exports.StringStream(lines[i]);
+ while (!stream.eol()) {
+ var style = mode.token(stream, state);
+ callback(stream.current(), style, i, stream.start);
+ stream.start = stream.pos;
+ }
+ }
+};
diff --git a/lib/util/runmode.js b/lib/util/runmode.js
index 6723927f2d..327976badf 100644
--- a/lib/util/runmode.js
+++ b/lib/util/runmode.js
@@ -1,6 +1,6 @@
CodeMirror.runMode = function(string, modespec, callback, options) {
function esc(str) {
- return str.replace(/[<&]/, function(ch) { return ch == "<" ? "<" : "&"; });
+ return str.replace(/[<&]/g, function(ch) { return ch == "<" ? "<" : "&"; });
}
var mode = CodeMirror.getMode(CodeMirror.defaults, modespec);
diff --git a/lib/util/searchcursor.js b/lib/util/searchcursor.js
index 970af899d7..1750aadc11 100644
--- a/lib/util/searchcursor.js
+++ b/lib/util/searchcursor.js
@@ -17,14 +17,14 @@
query.lastIndex = 0;
var line = cm.getLine(pos.line).slice(0, pos.ch), match = query.exec(line), start = 0;
while (match) {
- start += match.index;
- line = line.slice(match.index);
+ start += match.index + 1;
+ line = line.slice(start);
query.lastIndex = 0;
var newmatch = query.exec(line);
if (newmatch) match = newmatch;
else break;
- start++;
}
+ start--;
} else {
query.lastIndex = pos.ch;
var line = cm.getLine(pos.line), match = query.exec(line),
diff --git a/lib/util/simple-hint.js b/lib/util/simple-hint.js
index 8e481c37c2..0ce25f9650 100644
--- a/lib/util/simple-hint.js
+++ b/lib/util/simple-hint.js
@@ -25,7 +25,10 @@
editor.replaceRange(str, result.from, result.to);
}
// When there is only one completion, use it directly.
- if (completions.length == 1) {insert(completions[0]); return true;}
+ if (options.completeSingle && completions.length == 1) {
+ insert(completions[0]);
+ return true;
+ }
// Build the select widget
var complete = document.createElement("div");
@@ -41,7 +44,7 @@
}
sel.firstChild.selected = true;
sel.size = Math.min(10, completions.length);
- var pos = editor.cursorCoords();
+ var pos = options.alignWithWord ? editor.charCoords(result.from) : editor.cursorCoords();
complete.style.left = pos.x + "px";
complete.style.top = pos.yBot + "px";
document.body.appendChild(complete);
@@ -71,7 +74,7 @@
if (code == 13) {CodeMirror.e_stop(event); pick();}
// Escape
else if (code == 27) {CodeMirror.e_stop(event); close(); editor.focus();}
- else if (code != 38 && code != 40) {
+ else if (code != 38 && code != 40 && code != 33 && code != 34 && !CodeMirror.isModifierKey(event)) {
close(); editor.focus();
// Pass the event to the CodeMirror instance so that it can handle things like backspace properly.
editor.triggerOnKeyDown(event);
@@ -92,6 +95,8 @@
};
CodeMirror.simpleHint.defaults = {
closeOnBackspace: true,
- closeOnTokenChange: false
+ closeOnTokenChange: false,
+ completeSingle: true,
+ alignWithWord: true
};
})();
diff --git a/lib/util/xml-hint.js b/lib/util/xml-hint.js
index 816e3b4a69..dd9d858d84 100644
--- a/lib/util/xml-hint.js
+++ b/lib/util/xml-hint.js
@@ -12,13 +12,7 @@
cm.setCursor(cursor);
}
- // dirty hack for simple-hint to receive getHint event on space
- var getTokenAt = editor.getTokenAt;
-
- editor.getTokenAt = function() { return 'disabled'; };
CodeMirror.simpleHint(cm, getHint);
-
- editor.getTokenAt = getTokenAt;
};
var getHint = function(cm) {
diff --git a/mode/clike/clike.js b/mode/clike/clike.js
index 59555e9432..69668a44d3 100644
--- a/mode/clike/clike.js
+++ b/mode/clike/clike.js
@@ -140,6 +140,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
},
indent: function(state, textAfter) {
+ if (state.tokenize == tokenComment) return CodeMirror.Pass;
if (state.tokenize != tokenBase && state.tokenize != null) return 0;
var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
diff --git a/mode/clojure/clojure.js b/mode/clojure/clojure.js
index d0268a78d6..84f6073fd5 100644
--- a/mode/clojure/clojure.js
+++ b/mode/clojure/clojure.js
@@ -40,9 +40,9 @@ CodeMirror.defineMode("clojure", function (config, mode) {
var tests = {
digit: /\d/,
digit_or_colon: /[\d:]/,
- hex: /[0-9a-fA-F]/,
+ hex: /[0-9a-f]/i,
sign: /[+-]/,
- exponent: /[eE]/,
+ exponent: /e/i,
keyword_char: /[^\s\(\[\;\)\]]/,
basic: /[\w\$_\-]/,
lang_keyword: /[\w*+!\-_?:\/]/
@@ -64,14 +64,13 @@ CodeMirror.defineMode("clojure", function (config, mode) {
function isNumber(ch, stream){
// hex
- if ( ch === '0' && 'x' == stream.peek().toLowerCase() ) {
- stream.eat('x');
+ if ( ch === '0' && stream.eat(/x/i) ) {
stream.eatWhile(tests.hex);
return true;
}
// leading sign
- if ( ch == '+' || ch == '-' ) {
+ if ( ( ch == '+' || ch == '-' ) && ( tests.digit.test(stream.peek()) ) ) {
stream.eat(tests.sign);
ch = stream.next();
}
@@ -85,8 +84,7 @@ CodeMirror.defineMode("clojure", function (config, mode) {
stream.eatWhile(tests.digit);
}
- if ( 'e' == stream.peek().toLowerCase() ) {
- stream.eat(tests.exponent);
+ if ( stream.eat(tests.exponent) ) {
stream.eat(tests.sign);
stream.eatWhile(tests.digit);
}
diff --git a/mode/commonlisp/commonlisp.js b/mode/commonlisp/commonlisp.js
new file mode 100644
index 0000000000..4fb4bdf9bd
--- /dev/null
+++ b/mode/commonlisp/commonlisp.js
@@ -0,0 +1,101 @@
+CodeMirror.defineMode("commonlisp", function (config) {
+ var assumeBody = /^with|^def|^do|^prog|case$|^cond$|bind$|when$|unless$/;
+ var numLiteral = /^(?:[+\-]?(?:\d+|\d*\.\d+)(?:[efd][+\-]?\d+)?|[+\-]?\d+(?:\/[+\-]?\d+)?|#b[+\-]?[01]+|#o[+\-]?[0-7]+|#x[+\-]?[\da-f]+)/;
+ var symbol = /[^\s'`,@()\[\]";]/;
+ var type;
+
+ function readSym(stream) {
+ var ch;
+ while (ch = stream.next()) {
+ if (ch == "\\") stream.next();
+ else if (!symbol.test(ch)) { stream.backUp(1); break; }
+ }
+ return stream.current();
+ }
+
+ function base(stream, state) {
+ if (stream.eatSpace()) {type = "ws"; return null;}
+ if (stream.match(numLiteral)) return "number";
+ var ch = stream.next();
+ if (ch == "\\") ch = stream.next();
+
+ if (ch == '"') return (state.tokenize = inString)(stream, state);
+ else if (ch == "(") { type = "open"; return "bracket"; }
+ else if (ch == ")" || ch == "]") { type = "close"; return "bracket"; }
+ else if (ch == ";") { stream.skipToEnd(); type = "ws"; return "comment"; }
+ else if (/['`,@]/.test(ch)) return null;
+ else if (ch == "|") {
+ if (stream.skipTo("|")) { stream.next(); return "symbol"; }
+ else { stream.skipToEnd(); return "error"; }
+ } else if (ch == "#") {
+ var ch = stream.next();
+ if (ch == "[") { type = "open"; return "bracket"; }
+ else if (/[+\-=\.']/.test(ch)) return null;
+ else if (/\d/.test(ch) && stream.match(/^\d*#/)) return null;
+ else if (ch == "|") return (state.tokenize = inComment)(stream, state);
+ else if (ch == ":") { readSym(stream); return "meta"; }
+ else return "error";
+ } else {
+ var name = readSym(stream);
+ if (name == ".") return null;
+ type = "symbol";
+ if (name == "nil" || name == "t") return "atom";
+ if (name.charAt(0) == ":") return "keyword";
+ if (name.charAt(0) == "&") return "variable-2";
+ return "variable";
+ }
+ }
+
+ function inString(stream, state) {
+ var escaped = false, next;
+ while (next = stream.next()) {
+ if (next == '"' && !escaped) { state.tokenize = base; break; }
+ escaped = !escaped && next == "\\";
+ }
+ return "string";
+ }
+
+ function inComment(stream, state) {
+ var next, last;
+ while (next = stream.next()) {
+ if (next == "#" && last == "|") { state.tokenize = base; break; }
+ last = next;
+ }
+ type = "ws";
+ return "comment";
+ }
+
+ return {
+ startState: function () {
+ return {ctx: {prev: null, start: 0, indentTo: 0}, tokenize: base};
+ },
+
+ token: function (stream, state) {
+ if (stream.sol() && typeof state.ctx.indentTo != "number")
+ state.ctx.indentTo = state.ctx.start + 1;
+
+ type = null;
+ var style = state.tokenize(stream, state);
+ if (type != "ws") {
+ if (state.ctx.indentTo == null) {
+ if (type == "symbol" && assumeBody.test(stream.current()))
+ state.ctx.indentTo = state.ctx.start + config.indentUnit;
+ else
+ state.ctx.indentTo = "next";
+ } else if (state.ctx.indentTo == "next") {
+ state.ctx.indentTo = stream.column();
+ }
+ }
+ if (type == "open") state.ctx = {prev: state.ctx, start: stream.column(), indentTo: null};
+ else if (type == "close") state.ctx = state.ctx.prev || state.ctx;
+ return style;
+ },
+
+ indent: function (state, textAfter) {
+ var i = state.ctx.indentTo;
+ return typeof i == "number" ? i : state.ctx.start + 1;
+ }
+ };
+});
+
+CodeMirror.defineMIME("text/x-common-lisp", "commonlisp");
diff --git a/mode/commonlisp/index.html b/mode/commonlisp/index.html
new file mode 100644
index 0000000000..f9766a844c
--- /dev/null
+++ b/mode/commonlisp/index.html
@@ -0,0 +1,165 @@
+
+
+
+
+ CodeMirror: Common Lisp mode
+
+
+
+
+
+
+
+ CodeMirror: Common Lisp mode
+
+
+
+ MIME types defined: text/x-common-lisp.
+
+
+
diff --git a/mode/css/css.js b/mode/css/css.js
index 9428c4e32e..87d5d7401e 100644
--- a/mode/css/css.js
+++ b/mode/css/css.js
@@ -1,60 +1,196 @@
CodeMirror.defineMode("css", function(config) {
var indentUnit = config.indentUnit, type;
+
+ var atMediaTypes = keySet([
+ "all", "aural", "braille", "handheld", "print", "projection", "screen",
+ "tty", "tv", "embossed"
+ ]);
+
+ var atMediaFeatures = keySet([
+ "width", "min-width", "max-width", "height", "min-height", "max-height",
+ "device-width", "min-device-width", "max-device-width", "device-height",
+ "min-device-height", "max-device-height", "aspect-ratio",
+ "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio",
+ "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color",
+ "max-color", "color-index", "min-color-index", "max-color-index",
+ "monochrome", "min-monochrome", "max-monochrome", "resolution",
+ "min-resolution", "max-resolution", "scan", "grid"
+ ]);
- var keywords = keySet(["above", "absolute", "activeborder", "activecaption", "afar", "after-white-space", "ahead", "alias", "all", "all-scroll",
- "alternate", "always", "amharic", "amharic-abegede", "antialiased", "appworkspace", "arabic-indic", "armenian", "asterisks",
- "auto", "avoid", "background", "backwards", "baseline", "below", "bidi-override", "binary", "bengali", "blink",
- "block", "block-axis", "bold", "bolder", "border", "border-box", "both", "bottom", "break-all", "break-word", "button",
- "button-bevel", "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "cambodian", "capitalize", "caps-lock-indicator",
- "caption", "captiontext", "caret", "cell", "center", "checkbox", "circle", "cjk-earthly-branch", "cjk-heavenly-stem", "cjk-ideographic",
- "clear", "clip", "close-quote", "col-resize", "collapse", "compact", "condensed", "contain", "content", "content-box", "context-menu",
- "continuous", "copy", "cover", "crop", "cross", "crosshair", "currentcolor", "cursive", "dashed", "decimal", "decimal-leading-zero", "default",
- "default-button", "destination-atop", "destination-in", "destination-out", "destination-over", "devanagari", "disc", "discard", "document",
- "dot-dash", "dot-dot-dash", "dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out", "element",
- "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede", "ethiopic-abegede-am-et", "ethiopic-abegede-gez",
- "ethiopic-abegede-ti-er", "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er", "ethiopic-halehame-aa-et",
- "ethiopic-halehame-am-et", "ethiopic-halehame-gez", "ethiopic-halehame-om-et", "ethiopic-halehame-sid-et",
- "ethiopic-halehame-so-et", "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig", "ew-resize", "expanded",
- "extra-condensed", "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "footnotes", "forwards", "from", "geometricPrecision",
- "georgian", "graytext", "groove", "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hebrew", "help",
- "hidden", "hide", "higher", "highlight", "highlighttext", "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "icon", "ignore",
- "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite", "infobackground", "infotext", "inherit", "initial", "inline",
- "inline-axis", "inline-block", "inline-table", "inset", "inside", "intrinsic", "invert", "italic", "justify", "kannada", "katakana",
- "katakana-iroha", "khmer", "landscape", "lao", "large", "larger", "left", "level", "lighter", "line-through", "linear", "lines",
- "list-item", "listbox", "listitem", "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", "lower-greek",
- "lower-hexadecimal", "lower-latin", "lower-norwegian", "lower-roman", "lowercase", "ltr", "malayalam", "match", "media-controls-background",
- "media-current-time-display", "media-fullscreen-button", "media-mute-button", "media-play-button", "media-return-to-realtime-button",
- "media-rewind-button", "media-seek-back-button", "media-seek-forward-button", "media-slider", "media-sliderthumb", "media-time-remaining-display",
- "media-volume-slider", "media-volume-slider-container", "media-volume-sliderthumb", "medium", "menu", "menulist", "menulist-button",
- "menulist-text", "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", "mix", "mongolian", "monospace", "move", "multiple",
- "myanmar", "n-resize", "narrower", "navy", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", "no-open-quote", "no-repeat", "none",
- "normal", "not-allowed", "nowrap", "ns-resize", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote", "optimizeLegibility",
- "optimizeSpeed", "oriya", "oromo", "outset", "outside", "overlay", "overline", "padding", "padding-box", "painted", "paused",
- "persian", "plus-darker", "plus-lighter", "pointer", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d", "progress",
- "push-button", "radio", "read-only", "read-write", "read-write-plaintext-only", "relative", "repeat", "repeat-x",
- "repeat-y", "reset", "reverse", "rgb", "rgba", "ridge", "right", "round", "row-resize", "rtl", "run-in", "running", "s-resize", "sans-serif",
- "scroll", "scrollbar", "se-resize", "searchfield", "searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button",
- "searchfield-results-decoration", "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama", "single",
- "skip-white-space", "slide", "slider-horizontal", "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow",
- "small", "small-caps", "small-caption", "smaller", "solid", "somali", "source-atop", "source-in", "source-out", "source-over",
- "space", "square", "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub", "subpixel-antialiased", "super",
- "sw-resize", "table", "table-caption", "table-cell", "table-column", "table-column-group", "table-footer-group", "table-header-group",
- "table-row", "table-row-group", "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai", "thick", "thin",
- "threeddarkshadow", "threedface", "threedhighlight", "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er", "tigrinya-er-abegede",
- "tigrinya-et", "tigrinya-et-abegede", "to", "top", "transparent", "ultra-condensed", "ultra-expanded", "underline", "up", "upper-alpha", "upper-armenian",
- "upper-greek", "upper-hexadecimal", "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", "vertical", "vertical-text", "visible",
- "visibleFill", "visiblePainted", "visibleStroke", "visual", "w-resize", "wait", "wave", "white", "wider", "window", "windowframe", "windowtext",
- "x-large", "x-small", "xor", "xx-large", "xx-small", "yellow", "-wap-marquee", "-webkit-activelink", "-webkit-auto", "-webkit-baseline-middle",
- "-webkit-body", "-webkit-box", "-webkit-center", "-webkit-control", "-webkit-focus-ring-color", "-webkit-grab", "-webkit-grabbing",
- "-webkit-gradient", "-webkit-inline-box", "-webkit-left", "-webkit-link", "-webkit-marquee", "-webkit-mini-control", "-webkit-nowrap", "-webkit-pictograph",
- "-webkit-right", "-webkit-small-control", "-webkit-text", "-webkit-xxx-large", "-webkit-zoom-in", "-webkit-zoom-out"]);
+ var propertyKeywords = keySet([
+ "align-content", "align-items", "align-self", "alignment-adjust",
+ "alignment-baseline", "anchor-point", "animation", "animation-delay",
+ "animation-direction", "animation-duration", "animation-iteration-count",
+ "animation-name", "animation-play-state", "animation-timing-function",
+ "appearance", "azimuth", "backface-visibility", "background",
+ "background-attachment", "background-clip", "background-color",
+ "background-image", "background-origin", "background-position",
+ "background-repeat", "background-size", "baseline-shift", "binding",
+ "bleed", "bookmark-label", "bookmark-level", "bookmark-state",
+ "bookmark-target", "border", "border-bottom", "border-bottom-color",
+ "border-bottom-left-radius", "border-bottom-right-radius",
+ "border-bottom-style", "border-bottom-width", "border-collapse",
+ "border-color", "border-image", "border-image-outset",
+ "border-image-repeat", "border-image-slice", "border-image-source",
+ "border-image-width", "border-left", "border-left-color",
+ "border-left-style", "border-left-width", "border-radius", "border-right",
+ "border-right-color", "border-right-style", "border-right-width",
+ "border-spacing", "border-style", "border-top", "border-top-color",
+ "border-top-left-radius", "border-top-right-radius", "border-top-style",
+ "border-top-width", "border-width", "bottom", "box-decoration-break",
+ "box-shadow", "box-sizing", "break-after", "break-before", "break-inside",
+ "caption-side", "clear", "clip", "color", "color-profile", "column-count",
+ "column-fill", "column-gap", "column-rule", "column-rule-color",
+ "column-rule-style", "column-rule-width", "column-span", "column-width",
+ "columns", "content", "counter-increment", "counter-reset", "crop", "cue",
+ "cue-after", "cue-before", "cursor", "direction", "display",
+ "dominant-baseline", "drop-initial-after-adjust",
+ "drop-initial-after-align", "drop-initial-before-adjust",
+ "drop-initial-before-align", "drop-initial-size", "drop-initial-value",
+ "elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis",
+ "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap",
+ "float", "float-offset", "font", "font-feature-settings", "font-family",
+ "font-kerning", "font-language-override", "font-size", "font-size-adjust",
+ "font-stretch", "font-style", "font-synthesis", "font-variant",
+ "font-variant-alternates", "font-variant-caps", "font-variant-east-asian",
+ "font-variant-ligatures", "font-variant-numeric", "font-variant-position",
+ "font-weight", "grid-cell", "grid-column", "grid-column-align",
+ "grid-column-sizing", "grid-column-span", "grid-columns", "grid-flow",
+ "grid-row", "grid-row-align", "grid-row-sizing", "grid-row-span",
+ "grid-rows", "grid-template", "hanging-punctuation", "height", "hyphens",
+ "icon", "image-orientation", "image-rendering", "image-resolution",
+ "inline-box-align", "justify-content", "left", "letter-spacing",
+ "line-break", "line-height", "line-stacking", "line-stacking-ruby",
+ "line-stacking-shift", "line-stacking-strategy", "list-style",
+ "list-style-image", "list-style-position", "list-style-type", "margin",
+ "margin-bottom", "margin-left", "margin-right", "margin-top",
+ "marker-offset", "marks", "marquee-direction", "marquee-loop",
+ "marquee-play-count", "marquee-speed", "marquee-style", "max-height",
+ "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index",
+ "nav-left", "nav-right", "nav-up", "opacity", "order", "orphans", "outline",
+ "outline-color", "outline-offset", "outline-style", "outline-width",
+ "overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y",
+ "padding", "padding-bottom", "padding-left", "padding-right", "padding-top",
+ "page", "page-break-after", "page-break-before", "page-break-inside",
+ "page-policy", "pause", "pause-after", "pause-before", "perspective",
+ "perspective-origin", "pitch", "pitch-range", "play-during", "position",
+ "presentation-level", "punctuation-trim", "quotes", "rendering-intent",
+ "resize", "rest", "rest-after", "rest-before", "richness", "right",
+ "rotation", "rotation-point", "ruby-align", "ruby-overhang",
+ "ruby-position", "ruby-span", "size", "speak", "speak-as", "speak-header",
+ "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set",
+ "tab-size", "table-layout", "target", "target-name", "target-new",
+ "target-position", "text-align", "text-align-last", "text-decoration",
+ "text-decoration-color", "text-decoration-line", "text-decoration-skip",
+ "text-decoration-style", "text-emphasis", "text-emphasis-color",
+ "text-emphasis-position", "text-emphasis-style", "text-height",
+ "text-indent", "text-justify", "text-outline", "text-shadow",
+ "text-space-collapse", "text-transform", "text-underline-position",
+ "text-wrap", "top", "transform", "transform-origin", "transform-style",
+ "transition", "transition-delay", "transition-duration",
+ "transition-property", "transition-timing-function", "unicode-bidi",
+ "vertical-align", "visibility", "voice-balance", "voice-duration",
+ "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress",
+ "voice-volume", "volume", "white-space", "widows", "width", "word-break",
+ "word-spacing", "word-wrap", "z-index"
+ ]);
+
+ var colorKeywords = keySet([
+ "black", "silver", "gray", "white", "maroon", "red", "purple", "fuchsia",
+ "green", "lime", "olive", "yellow", "navy", "blue", "teal", "aqua"
+ ]);
+
+ var valueKeywords = keySet([
+ "above", "absolute", "activeborder", "activecaption", "afar",
+ "after-white-space", "ahead", "alias", "all", "all-scroll", "alternate",
+ "always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
+ "arabic-indic", "armenian", "asterisks", "auto", "avoid", "background",
+ "backwards", "baseline", "below", "bidi-override", "binary", "bengali",
+ "blink", "block", "block-axis", "bold", "bolder", "border", "border-box",
+ "both", "bottom", "break-all", "break-word", "button", "button-bevel",
+ "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "cambodian",
+ "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret",
+ "cell", "center", "checkbox", "circle", "cjk-earthly-branch",
+ "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
+ "col-resize", "collapse", "compact", "condensed", "contain", "content",
+ "content-box", "context-menu", "continuous", "copy", "cover", "crop",
+ "cross", "crosshair", "currentcolor", "cursive", "dashed", "decimal",
+ "decimal-leading-zero", "default", "default-button", "destination-atop",
+ "destination-in", "destination-out", "destination-over", "devanagari",
+ "disc", "discard", "document", "dot-dash", "dot-dot-dash", "dotted",
+ "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out",
+ "element", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede",
+ "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er",
+ "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er",
+ "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et",
+ "ethiopic-halehame-gez", "ethiopic-halehame-om-et",
+ "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et",
+ "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et",
+ "ethiopic-halehame-tig", "ew-resize", "expanded", "extra-condensed",
+ "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "footnotes",
+ "forwards", "from", "geometricPrecision", "georgian", "graytext", "groove",
+ "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hebrew",
+ "help", "hidden", "hide", "higher", "highlight", "highlighttext",
+ "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "icon", "ignore",
+ "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite",
+ "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis",
+ "inline-block", "inline-table", "inset", "inside", "intrinsic", "invert",
+ "italic", "justify", "kannada", "katakana", "katakana-iroha", "khmer",
+ "landscape", "lao", "large", "larger", "left", "level", "lighter",
+ "line-through", "linear", "lines", "list-item", "listbox", "listitem",
+ "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian",
+ "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian",
+ "lower-roman", "lowercase", "ltr", "malayalam", "match",
+ "media-controls-background", "media-current-time-display",
+ "media-fullscreen-button", "media-mute-button", "media-play-button",
+ "media-return-to-realtime-button", "media-rewind-button",
+ "media-seek-back-button", "media-seek-forward-button", "media-slider",
+ "media-sliderthumb", "media-time-remaining-display", "media-volume-slider",
+ "media-volume-slider-container", "media-volume-sliderthumb", "medium",
+ "menu", "menulist", "menulist-button", "menulist-text",
+ "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic",
+ "mix", "mongolian", "monospace", "move", "multiple", "myanmar", "n-resize",
+ "narrower", "navy", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
+ "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
+ "ns-resize", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote",
+ "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
+ "outside", "overlay", "overline", "padding", "padding-box", "painted",
+ "paused", "persian", "plus-darker", "plus-lighter", "pointer", "portrait",
+ "pre", "pre-line", "pre-wrap", "preserve-3d", "progress", "push-button",
+ "radio", "read-only", "read-write", "read-write-plaintext-only", "relative",
+ "repeat", "repeat-x", "repeat-y", "reset", "reverse", "rgb", "rgba",
+ "ridge", "right", "round", "row-resize", "rtl", "run-in", "running",
+ "s-resize", "sans-serif", "scroll", "scrollbar", "se-resize", "searchfield",
+ "searchfield-cancel-button", "searchfield-decoration",
+ "searchfield-results-button", "searchfield-results-decoration",
+ "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama",
+ "single", "skip-white-space", "slide", "slider-horizontal",
+ "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow",
+ "small", "small-caps", "small-caption", "smaller", "solid", "somali",
+ "source-atop", "source-in", "source-out", "source-over", "space", "square",
+ "square-button", "start", "static", "status-bar", "stretch", "stroke",
+ "sub", "subpixel-antialiased", "super", "sw-resize", "table",
+ "table-caption", "table-cell", "table-column", "table-column-group",
+ "table-footer-group", "table-header-group", "table-row", "table-row-group",
+ "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai",
+ "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
+ "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
+ "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
+ "transparent", "ultra-condensed", "ultra-expanded", "underline", "up",
+ "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
+ "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
+ "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted",
+ "visibleStroke", "visual", "w-resize", "wait", "wave", "white", "wider",
+ "window", "windowframe", "windowtext", "x-large", "x-small", "xor",
+ "xx-large", "xx-small", "yellow"
+ ]);
function keySet(array) { var keys = {}; for (var i = 0; i < array.length; ++i) keys[array[i]] = true; return keys; }
function ret(style, tp) {type = tp; return style;}
function tokenBase(stream, state) {
var ch = stream.next();
- if (ch == "@") {stream.eatWhile(/[\w\\\-]/); return ret("meta", stream.current());}
+ if (ch == "@") {stream.eatWhile(/[\w\\\-]/); return ret("def", stream.current());}
else if (ch == "/" && stream.eat("*")) {
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
@@ -81,15 +217,29 @@ CodeMirror.defineMode("css", function(config) {
stream.eatWhile(/[\w.%]/);
return ret("number", "unit");
}
- else if (/[,.+>*\/]/.test(ch)) {
+ else if (ch === "-") {
+ if (/\d/.test(stream.peek())) {
+ stream.eatWhile(/[\w.%]/);
+ return ret("number", "unit");
+ } else if (stream.match(/^[^-]+-/)) {
+ return ret("meta", type);
+ }
+ }
+ else if (/[,+>*\/]/.test(ch)) {
return ret(null, "select-op");
}
- else if (/[;{}:\[\]\(\)]/.test(ch)) {
+ else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
+ return ret("qualifier", type);
+ }
+ else if (ch == ":") {
+ return ret("operator", ch);
+ }
+ else if (/[;{}\[\]\(\)]/.test(ch)) {
return ret(null, ch);
}
else {
stream.eatWhile(/[\w\\\-]/);
- return ret("variable", "variable");
+ return ret("property", "variable");
}
}
@@ -138,32 +288,156 @@ CodeMirror.defineMode("css", function(config) {
},
token: function(stream, state) {
+
+ // Use these terms when applicable (see http://www.xanthir.com/blog/b4E50)
+ //
+ // rule** or **ruleset:
+ // A selector + braces combo, or an at-rule.
+ //
+ // declaration block:
+ // A sequence of declarations.
+ //
+ // declaration:
+ // A property + colon + value combo.
+ //
+ // property value:
+ // The entire value of a property.
+ //
+ // component value:
+ // A single piece of a property value. Like the 5px in
+ // text-shadow: 0 0 5px blue;. Can also refer to things that are
+ // multiple terms, like the 1-4 terms that make up the background-size
+ // portion of the background shorthand.
+ //
+ // term:
+ // The basic unit of author-facing CSS, like a single number (5),
+ // dimension (5px), string ("foo"), or function. Officially defined
+ // by the CSS 2.1 grammar (look for the 'term' production)
+ //
+ //
+ // simple selector:
+ // A single atomic selector, like a type selector, an attr selector, a
+ // class selector, etc.
+ //
+ // compound selector:
+ // One or more simple selectors without a combinator. div.example is
+ // compound, div > .example is not.
+ //
+ // complex selector:
+ // One or more compound selectors chained with combinators.
+ //
+ // combinator:
+ // The parts of selectors that express relationships. There are four
+ // currently - the space (descendant combinator), the greater-than
+ // bracket (child combinator), the plus sign (next sibling combinator),
+ // and the tilda (following sibling combinator).
+ //
+ // sequence of selectors:
+ // One or more of the named type of selector chained with commas.
+
if (stream.eatSpace()) return null;
var style = state.tokenize(stream, state);
+ // Changing style returned based on context
var context = state.stack[state.stack.length-1];
- if (type == "hash" && context != "rule") style = "string-2";
- else if (style == "variable") {
- if (context == "rule") style = keywords[stream.current()] ? "keyword" : "number";
- else if (!context || context == "@media{") style = "tag";
+ if (style == "property") {
+ if (context == "propertyValue"){
+ if (valueKeywords[stream.current()]) {
+ style = "string-2";
+ } else if (colorKeywords[stream.current()]) {
+ style = "keyword";
+ } else {
+ style = "variable-2";
+ }
+ } else if (context == "rule") {
+ if (!propertyKeywords[stream.current()]) {
+ style += " error";
+ }
+ } else if (!context || context == "@media{") {
+ style = "tag";
+ } else if (context == "@media") {
+ if (atMediaTypes[stream.current()]) {
+ style = "attribute"; // Known attribute
+ } else if (/^(only|not)$/i.test(stream.current())) {
+ style = "keyword";
+ } else if (stream.current().toLowerCase() == "and") {
+ style = "error"; // "and" is only allowed in @mediaType
+ } else if (atMediaFeatures[stream.current()]) {
+ style = "error"; // Known property, should be in @mediaType(
+ } else {
+ // Unknown, expecting keyword or attribute, assuming attribute
+ style = "attribute error";
+ }
+ } else if (context == "@mediaType") {
+ if (atMediaTypes[stream.current()]) {
+ style = "attribute";
+ } else if (stream.current().toLowerCase() == "and") {
+ style = "operator";
+ } else if (/^(only|not)$/i.test(stream.current())) {
+ style = "error"; // Only allowed in @media
+ } else if (atMediaFeatures[stream.current()]) {
+ style = "error"; // Known property, should be in parentheses
+ } else {
+ // Unknown attribute or property, but expecting property (preceded
+ // by "and"). Should be in parentheses
+ style = "error";
+ }
+ } else if (context == "@mediaType(") {
+ if (propertyKeywords[stream.current()]) {
+ // do nothing, remains "property"
+ } else if (atMediaTypes[stream.current()]) {
+ style = "error"; // Known property, should be in parentheses
+ } else if (stream.current().toLowerCase() == "and") {
+ style = "operator";
+ } else if (/^(only|not)$/i.test(stream.current())) {
+ style = "error"; // Only allowed in @media
+ } else {
+ style += " error";
+ }
+ } else {
+ style = "error";
+ }
+ } else if (style == "atom") {
+ if(!context || context == "@media{") {
+ style = "builtin";
+ } else if (context == "propertyValue") {
+ if (!/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(stream.current())) {
+ style += " error";
+ }
+ } else {
+ style = "error";
+ }
+ } else if (context == "@media" && type == "{") {
+ style = "error";
}
- if (context == "rule" && /^[\{\};]$/.test(type))
- state.stack.pop();
+ // Push/pop context stack
if (type == "{") {
- if (context == "@media") state.stack[state.stack.length-1] = "@media{";
- else state.stack.push("{");
+ if (context == "@media" || context == "@mediaType") {
+ state.stack.pop();
+ state.stack[state.stack.length-1] = "@media{";
+ }
+ else state.stack.push("rule");
+ }
+ else if (type == "}") {
+ state.stack.pop();
+ if (context == "propertyValue") state.stack.pop();
}
- else if (type == "}") state.stack.pop();
else if (type == "@media") state.stack.push("@media");
- else if (context == "{" && type != "comment") state.stack.push("rule");
+ else if (context == "@media" && /\b(keyword|attribute)\b/.test(style))
+ state.stack.push("@mediaType");
+ else if (context == "@mediaType" && stream.current() == ",") state.stack.pop();
+ else if (context == "@mediaType" && type == "(") state.stack.push("@mediaType(");
+ else if (context == "@mediaType(" && type == ")") state.stack.pop();
+ else if (context == "rule" && type == ":") state.stack.push("propertyValue");
+ else if (context == "propertyValue" && type == ";") state.stack.pop();
return style;
},
indent: function(state, textAfter) {
var n = state.stack.length;
if (/^\}/.test(textAfter))
- n -= state.stack[state.stack.length-1] == "rule" ? 2 : 1;
+ n -= state.stack[state.stack.length-1] == "propertyValue" ? 2 : 1;
return state.baseIndent + n * indentUnit;
},
diff --git a/mode/css/index.html b/mode/css/index.html
index 1a591cbf3d..ae2c3bfcee 100644
--- a/mode/css/index.html
+++ b/mode/css/index.html
@@ -52,5 +52,7 @@ CodeMirror: CSS mode
MIME types defined: text/css.
+ Parsing/Highlighting Tests: normal, verbose.
+