2,350 changes: 1,515 additions & 835 deletions keymap/vim.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions lib/codemirror.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}

.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-emstrong {font-style: italic; font-weight: bold;}
Expand Down
399 changes: 240 additions & 159 deletions lib/codemirror.js

Large diffs are not rendered by default.

228 changes: 85 additions & 143 deletions lib/util/closetag.js
Original file line number Diff line number Diff line change
@@ -1,143 +1,85 @@
/**
* Tag-closer extension for CodeMirror.
*
* This extension adds a "closeTag" utility function that can be used with key bindings to
* insert a matching end tag after the ">" character of a start tag has been typed. It can
* also complete "</" if a matching start tag is found. It will correctly ignore signal
* characters for empty tags, comments, CDATA, etc.
*
* The function depends on internal parser state to identify tags. It is compatible with the
* following CodeMirror modes and will ignore all others:
* - htmlmixed
* - xml
*
* See demos/closetag.html for a usage example.
*
* @author Nathan Williams <nathan@nlwillia.net>
* Contributed under the same license terms as CodeMirror.
*/
(function() {
/** Option that allows tag closing behavior to be toggled. Default is true. */
CodeMirror.defaults['closeTagEnabled'] = true;

/** Array of tag names to add indentation after the start tag for. Default is the list of block-level html tags. */
CodeMirror.defaults['closeTagIndent'] = ['applet', 'blockquote', 'body', 'button', 'div', 'dl', 'fieldset', 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html', 'iframe', 'layer', 'legend', 'object', 'ol', 'p', 'select', 'table', 'ul'];

/** 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.
* - ch: The character being processed.
* - indent: Optional. An array of tag names to indent when closing. Omit or pass true to use the default indentation tag list defined in the 'closeTagIndent' option.
* Pass false to disable indentation. Pass an array to override the default list of tag names.
* - vd: Optional. An array of tag names that should not be closed. Omit to use the default void (end tag forbidden) tag list defined in the 'closeTagVoid' option. Ignored in xml mode.
*/
CodeMirror.defineExtension("closeTag", function(cm, ch, indent, vd) {
if (!cm.getOption('closeTagEnabled')) {
throw CodeMirror.Pass;
}
var pos = cm.getCursor();
var tok = cm.getTokenAt(pos);
var state = innerState(cm, tok.state);

if (state) {

if (ch == '>') {
var type = state.type;

if (tok.type == 'tag' && type == 'closeTag') {
throw CodeMirror.Pass; // Don't process the '>' at the end of an end-tag.
}

cm.replaceSelection('>'); // Mode state won't update until we finish the tag.
pos = {line: pos.line, ch: pos.ch + 1};
cm.setCursor(pos);

tok = cm.getTokenAt(cm.getCursor());
state = innerState(cm, tok.state);
if (!state) throw CodeMirror.Pass;
var type = state.type;

if (tok.type == 'tag' && type != 'selfcloseTag') {
var tagName = state.tagName;
if (tagName.length > 0 && shouldClose(cm, vd, tagName)) {
insertEndTag(cm, indent, pos, tagName);
}
return;
}

// Undo the '>' insert and allow cm to handle the key instead.
cm.setSelection({line: pos.line, ch: pos.ch - 1}, pos);
cm.replaceSelection("");

} else if (ch == '/') {
if (tok.type == 'tag' && tok.string == '<') {
var ctx = state.context, tagName = ctx ? ctx.tagName : '';
if (tagName.length > 0) {
completeEndTag(cm, pos, tagName);
return;
}
}
}

}

throw CodeMirror.Pass; // Bubble if not handled
});

function insertEndTag(cm, indent, pos, tagName) {
if (shouldIndent(cm, indent, tagName)) {
cm.replaceSelection('\n\n</' + tagName + '>', 'end');
cm.indentLine(pos.line + 1);
cm.indentLine(pos.line + 2);
cm.setCursor({line: pos.line + 1, ch: cm.getLine(pos.line + 1).length});
} else {
cm.replaceSelection('</' + tagName + '>');
cm.setCursor(pos);
}
}

function shouldIndent(cm, indent, tagName) {
if (typeof indent == 'undefined' || indent == null || indent == true) {
indent = cm.getOption('closeTagIndent');
}
if (!indent) {
indent = [];
}
return indexOf(indent, tagName.toLowerCase()) != -1;
}

function shouldClose(cm, vd, tagName) {
if (cm.getOption('mode') == 'xml') {
return true; // always close xml tags
}
if (typeof vd == 'undefined' || vd == null) {
vd = cm.getOption('closeTagVoid');
}
if (!vd) {
vd = [];
}
return indexOf(vd, tagName.toLowerCase()) == -1;
}

// C&P from codemirror.js...would be nice if this were visible to utilities.
function indexOf(collection, elt) {
if (collection.indexOf) return collection.indexOf(elt);
for (var i = 0, e = collection.length; i < e; ++i)
if (collection[i] == elt) return i;
return -1;
}

function completeEndTag(cm, pos, tagName) {
cm.replaceSelection('/' + tagName + '>');
cm.setCursor({line: pos.line, ch: pos.ch + tagName.length + 2 });
}

})();
/**
* Tag-closer extension for CodeMirror.
*
* This extension adds an "autoCloseTags" option that can be set to
* either true to get the default behavior, or an object to further
* configure its behavior.
*
* These are supported options:
*
* `whenClosing` (default true)
* Whether to autoclose when the '/' of a closing tag is typed.
* `whenOpening` (default true)
* Whether to autoclose the tag when the final '>' of an opening
* tag is typed.
* `dontCloseTags` (default is empty tags for HTML, none for XML)
* An array of tag names that should not be autoclosed.
* `indentTags` (default is block tags for HTML, none for XML)
* An array of tag names that should, when opened, cause a
* blank line to be added inside the tag, and the blank line and
* closing line to be indented.
*
* See demos/closetag.html for a usage example.
*/

(function() {
CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) {
if (val && (old == CodeMirror.Init || !old)) {
var map = {name: "autoCloseTags"};
if (typeof val != "object" || val.whenClosing)
map["'/'"] = function(cm) { autoCloseTag(cm, '/'); };
if (typeof val != "object" || val.whenOpening)
map["'>'"] = function(cm) { autoCloseTag(cm, '>'); };
cm.addKeyMap(map);
} else if (!val && (old != CodeMirror.Init && old)) {
cm.removeKeyMap("autoCloseTags");
}
});

var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
"source", "track", "wbr"];
var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4",
"h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"];

function autoCloseTag(cm, ch) {
var pos = cm.getCursor(), tok = cm.getTokenAt(pos);
var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
if (inner.mode.name != "xml") throw CodeMirror.Pass;

var opt = cm.getOption("autoCloseTags"), html = inner.mode.configuration == "html";
var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose);
var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent);

if (ch == ">" && state.tagName) {
var tagName = state.tagName;
if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch);
var lowerTagName = tagName.toLowerCase();
// Don't process the '>' at the end of an end-tag or self-closing tag
if (tok.type == "tag" && state.type == "closeTag" ||
/\/\s*$/.test(tok.string) ||
dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1)
throw CodeMirror.Pass;

var doIndent = indentTags && indexOf(indentTags, lowerTagName) > -1;
cm.replaceSelection(">" + (doIndent ? "\n\n" : "") + "</" + tagName + ">",
doIndent ? {line: pos.line + 1, ch: 0} : {line: pos.line, ch: pos.ch + 1});
if (doIndent) {
cm.indentLine(pos.line + 1);
cm.indentLine(pos.line + 2);
}
return;
} else if (ch == "/" && tok.type == "tag" && tok.string == "<") {
var tagName = state.context && state.context.tagName;
if (tagName) cm.replaceSelection("/" + tagName + ">", "end");
return;
}
throw CodeMirror.Pass;
}

function indexOf(collection, elt) {
if (collection.indexOf) return collection.indexOf(elt);
for (var i = 0, e = collection.length; i < e; ++i)
if (collection[i] == elt) return i;
return -1;
}
})();
174 changes: 43 additions & 131 deletions lib/util/formatting.js
Original file line number Diff line number Diff line change
@@ -1,138 +1,39 @@
// ============== Formatting extensions ============================
(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");
newlineAfterToken: function(_type, content) {
return /^[;{}]$/.test(content);
}
});

function jsNonBreakableBlocks(text) {
var nonBreakableRegexes = [/for\s*?\((.*?)\)/,
/\"(.*?)(\"|$)/,
/\'(.*?)(\'|$)/,
/\/\*(.*?)(\*\/|$)/,
/\/\/.*/];
var nonBreakableBlocks = [];
for (var i = 0; i < nonBreakableRegexes.length; i++) {
var curPos = 0;
while (curPos < text.length) {
var m = text.substr(curPos).match(nonBreakableRegexes[i]);
if (m != null) {
nonBreakableBlocks.push({
start: curPos + m.index,
end: curPos + m.index + m[0].length
});
curPos += m.index + Math.max(1, m[0].length);
}
else { // No more matches
break;
}
}
}
nonBreakableBlocks.sort(function (a, b) {
return a.start - b.start;
});

return nonBreakableBlocks;
}

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));
// FIXME semicolons inside of for
newlineAfterToken: function(_type, content, textAfter, state) {
if (this.jsonMode) {
return /^[\[,{]$/.test(content) || /^}/.test(textAfter);
} else {
res = split(text);
if (content == ";" && state.lexical && state.lexical.type == ")") return false;
return /^[;{}]$/.test(content) && !/^;/.test(textAfter);
}
return res.replace(/^\n*|\n*$/, "");
}
});

CodeMirror.extendMode("xml", {
commentStart: "<!--",
commentEnd: "-->",
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;
}
}
return lines.join("\n");
newlineAfterToken: function(type, content, textAfter) {
return type == "tag" && />$/.test(content) || /^</.test(textAfter);
}
});

function localModeAt(cm, pos) {
return CodeMirror.innerMode(cm.getMode(), cm.getTokenAt(pos).state).mode;
}

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;
}

// Comment/uncomment the specified range
CodeMirror.defineExtension("commentRange", function (isComment, from, to) {
var curMode = localModeAt(this, from), cm = this;
this.operation(function() {
var cm = this, curMode = CodeMirror.innerMode(cm.getMode(), cm.getTokenAt(from).state).mode;
cm.operation(function() {
if (isComment) { // Comment range
cm.replaceRange(curMode.commentEnd, to);
cm.replaceRange(curMode.commentStart, from);
Expand Down Expand Up @@ -168,27 +69,38 @@
// 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 (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 outer = cm.getMode(), text = cm.getRange(from, to).split("\n");
var state = CodeMirror.copyState(outer, cm.getTokenAt(from).state);
var tabSize = cm.getOption("tabSize");

var out = "", lines = 0, atSol = from.ch == 0;
function newline() {
out += "\n";
atSol = true;
++lines;
}

for (var i = 0; i < text.length; ++i) {
var stream = new CodeMirror.StringStream(text[i], tabSize);
while (!stream.eol()) {
var inner = CodeMirror.innerMode(outer, state);
var style = outer.token(stream, state), cur = stream.current();
stream.start = stream.pos;
if (!atSol || /\S/.test(cur)) {
out += cur;
atSol = false;
}
if (!atSol && inner.mode.newlineAfterToken &&
inner.mode.newlineAfterToken(style, cur, stream.string.slice(stream.pos) || text[i+1] || "", inner.state))
newline();
}
for (var cur = from.line + 1; cur <= end; ++cur)
if (!stream.pos && outer.blankLine) outer.blankLine(state);
if (!atSol) newline();
}

cm.operation(function () {
cm.replaceRange(out, from, to);
for (var cur = from.line + 1, end = from.line + lines; cur <= end; ++cur)
cm.indentLine(cur, "smart");
cm.setSelection(from, cm.getCursor(false));
});
Expand Down
2 changes: 1 addition & 1 deletion lib/util/searchcursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
var line = cm.getLine(pos.line).slice(0, pos.ch), match = query.exec(line), start = 0;
while (match) {
start += match.index + 1;
line = line.slice(start);
line = line.slice(start);
query.lastIndex = 0;
var newmatch = query.exec(line);
if (newmatch) match = newmatch;
Expand Down
16 changes: 14 additions & 2 deletions mode/clike/clike.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
if (style == "comment" || style == "meta") return style;
if (ctx.align == null) ctx.align = true;

if ((curPunc == ";" || curPunc == ":") && ctx.type == "statement") popContext(state);
if ((curPunc == ";" || curPunc == ":" || curPunc == ",") && ctx.type == "statement") popContext(state);
else if (curPunc == "{") pushContext(state, stream.column(), "}");
else if (curPunc == "[") pushContext(state, stream.column(), "]");
else if (curPunc == "(") pushContext(state, stream.column(), ")");
Expand Down Expand Up @@ -165,7 +165,19 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {

function cppHook(stream, state) {
if (!state.startOfLine) return false;
stream.skipToEnd();
for (;;) {
if (stream.skipTo("\\")) {
stream.next();
if (stream.eol()) {
state.tokenize = cppHook;
break;
}
} else {
stream.skipToEnd();
state.tokenize = null;
break;
}
}
return "meta";
}

Expand Down
4 changes: 2 additions & 2 deletions mode/diff/diff.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
CodeMirror.defineMode("diff", function() {

var TOKEN_NAMES = {
'+': 'tag',
'-': 'string',
'+': 'positive',
'-': 'negative',
'@': 'meta'
};

Expand Down
2 changes: 1 addition & 1 deletion mode/htmlmixed/htmlmixed.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ CodeMirror.defineMode("htmlmixed", function(config) {

function html(stream, state) {
var style = htmlMode.token(stream, state.htmlState);
if (style == "tag" && stream.current() == ">" && state.htmlState.context) {
if (/(?:^|\s)tag(?:\s|$)/.test(style) && stream.current() == ">" && state.htmlState.context) {
if (/^script$/i.test(state.htmlState.context.tagName)) {
state.token = javascript;
state.localState = jsMode.startState(htmlMode.indent(state.htmlState, ""));
Expand Down
98 changes: 98 additions & 0 deletions mode/http/http.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
CodeMirror.defineMode("http", function() {
function failFirstLine(stream, state) {
stream.skipToEnd();
state.cur = header;
return "error";
}

function start(stream, state) {
if (stream.match(/^HTTP\/\d\.\d/)) {
state.cur = responseStatusCode;
return "keyword";
} else if (stream.match(/^[A-Z]+/) && /[ \t]/.test(stream.peek())) {
state.cur = requestPath;
return "keyword";
} else {
return failFirstLine(stream, state);
}
}

function responseStatusCode(stream, state) {
var code = stream.match(/^\d+/);
if (!code) return failFirstLine(stream, state);

state.cur = responseStatusText;
var status = Number(code[0]);
if (status >= 100 && status < 200) {
return "positive informational";
} else if (status >= 200 && status < 300) {
return "positive success";
} else if (status >= 300 && status < 400) {
return "positive redirect";
} else if (status >= 400 && status < 500) {
return "negative client-error";
} else if (status >= 500 && status < 600) {
return "negative server-error";
} else {
return "error";
}
}

function responseStatusText(stream, state) {
stream.skipToEnd();
state.cur = header;
return null;
}

function requestPath(stream, state) {
stream.eatWhile(/\S/);
state.cur = requestProtocol;
return "string-2";
}

function requestProtocol(stream, state) {
if (stream.match(/^HTTP\/\d\.\d$/)) {
state.cur = header;
return "keyword";
} else {
return failFirstLine(stream, state);
}
}

function header(stream) {
if (stream.sol() && !stream.eat(/[ \t]/)) {
if (stream.match(/^.*?:/)) {
return "atom";
} else {
stream.skipToEnd();
return "error";
}
} else {
stream.skipToEnd();
return "string";
}
}

function body(stream) {
stream.skipToEnd();
return null;
}

return {
token: function(stream, state) {
var cur = state.cur;
if (cur != header && cur != body && stream.eatSpace()) return null;
return cur(stream, state);
},

blankLine: function(state) {
state.cur = body;
},

startState: function() {
return {cur: start};
}
};
});

CodeMirror.defineMIME("message/http", "http");
32 changes: 32 additions & 0 deletions mode/http/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: HTTP mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
<script src="http.js"></script>
<link rel="stylesheet" href="../../doc/docs.css">
<style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
</head>
<body>
<h1>CodeMirror: HTTP mode</h1>

<div><textarea id="code" name="code">
POST /somewhere HTTP/1.1
Host: example.com
If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT
Content-Type: application/x-www-form-urlencoded;
charset=utf-8
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.11 (KHTML, like Gecko) Ubuntu/12.04 Chromium/20.0.1132.47 Chrome/20.0.1132.47 Safari/536.11

This is the request body!
</textarea></div>

<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {});
</script>

<p><strong>MIME types defined:</strong> <code>message/http</code>.</p>
</body>
</html>
2 changes: 1 addition & 1 deletion mode/php/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ <h1>CodeMirror: PHP mode</h1>

<p>Simple HTML/PHP mode based on
the <a href="../clike/">C-like</a> mode. Depends on XML,
JavaScript, CSS, and C-like modes.</p>
JavaScript, CSS, HTMLMixed, and C-like modes.</p>

<p><strong>MIME types defined:</strong> <code>application/x-httpd-php</code> (HTML with PHP code), <code>text/x-php</code> (plain, non-wrapped PHP code).</p>
</body>
Expand Down
1 change: 1 addition & 0 deletions mode/php/php.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"print unset __halt_compiler self static parent"),
blockKeywords: keywords("catch do else elseif for foreach if switch try while"),
atoms: keywords("true false null TRUE FALSE NULL"),
builtin: keywords("func_num_args func_get_arg func_get_args strlen strcmp strncmp strcasecmp strncasecmp each error_reporting define defined trigger_error user_error set_error_handler restore_error_handler get_declared_classes get_loaded_extensions extension_loaded get_extension_funcs debug_backtrace constant bin2hex sleep usleep time mktime gmmktime strftime gmstrftime strtotime date gmdate getdate localtime checkdate flush wordwrap htmlspecialchars htmlentities html_entity_decode md5 md5_file crc32 getimagesize image_type_to_mime_type phpinfo phpversion phpcredits strnatcmp strnatcasecmp substr_count strspn strcspn strtok strtoupper strtolower strpos strrpos strrev hebrev hebrevc nl2br basename dirname pathinfo stripslashes stripcslashes strstr stristr strrchr str_shuffle str_word_count strcoll substr substr_replace quotemeta ucfirst ucwords strtr addslashes addcslashes rtrim str_replace str_repeat count_chars chunk_split trim ltrim strip_tags similar_text explode implode setlocale localeconv parse_str str_pad chop strchr sprintf printf vprintf vsprintf sscanf fscanf parse_url urlencode urldecode rawurlencode rawurldecode readlink linkinfo link unlink exec system escapeshellcmd escapeshellarg passthru shell_exec proc_open proc_close rand srand getrandmax mt_rand mt_srand mt_getrandmax base64_decode base64_encode abs ceil floor round is_finite is_nan is_infinite bindec hexdec octdec decbin decoct dechex base_convert number_format fmod ip2long long2ip getenv putenv getopt microtime gettimeofday getrusage uniqid quoted_printable_decode set_time_limit get_cfg_var magic_quotes_runtime set_magic_quotes_runtime get_magic_quotes_gpc get_magic_quotes_runtime import_request_variables error_log serialize unserialize memory_get_usage var_dump var_export debug_zval_dump print_r highlight_file show_source highlight_string ini_get ini_get_all ini_set ini_alter ini_restore get_include_path set_include_path restore_include_path setcookie header headers_sent connection_aborted connection_status ignore_user_abort parse_ini_file is_uploaded_file move_uploaded_file intval floatval doubleval strval gettype settype is_null is_resource is_bool is_long is_float is_int is_integer is_double is_real is_numeric is_string is_array is_object is_scalar ereg ereg_replace eregi eregi_replace split spliti join sql_regcase dl pclose popen readfile rewind rmdir umask fclose feof fgetc fgets fgetss fread fopen fpassthru ftruncate fstat fseek ftell fflush fwrite fputs mkdir rename copy tempnam tmpfile file file_get_contents stream_select stream_context_create stream_context_set_params stream_context_set_option stream_context_get_options stream_filter_prepend stream_filter_append fgetcsv flock get_meta_tags stream_set_write_buffer set_file_buffer set_socket_blocking stream_set_blocking socket_set_blocking stream_get_meta_data stream_register_wrapper stream_wrapper_register stream_set_timeout socket_set_timeout socket_get_status realpath fnmatch fsockopen pfsockopen pack unpack get_browser crypt opendir closedir chdir getcwd rewinddir readdir dir glob fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype file_exists is_writable is_writeable is_readable is_executable is_file is_dir is_link stat lstat chown touch clearstatcache mail ob_start ob_flush ob_clean ob_end_flush ob_end_clean ob_get_flush ob_get_clean ob_get_length ob_get_level ob_get_status ob_get_contents ob_implicit_flush ob_list_handlers ksort krsort natsort natcasesort asort arsort sort rsort usort uasort uksort shuffle array_walk count end prev next reset current key min max in_array array_search extract compact array_fill range array_multisort array_push array_pop array_shift array_unshift array_splice array_slice array_merge array_merge_recursive array_keys array_values array_count_values array_reverse array_reduce array_pad array_flip array_change_key_case array_rand array_unique array_intersect array_intersect_assoc array_diff array_diff_assoc array_sum array_filter array_map array_chunk array_key_exists pos sizeof key_exists assert assert_options version_compare ftok str_rot13 aggregate session_name session_module_name session_save_path session_id session_regenerate_id session_decode session_register session_unregister session_is_registered session_encode session_start session_destroy session_unset session_set_save_handler session_cache_limiter session_cache_expire session_set_cookie_params session_get_cookie_params session_write_close preg_match preg_match_all preg_replace preg_replace_callback preg_split preg_quote preg_grep overload ctype_alnum ctype_alpha ctype_cntrl ctype_digit ctype_lower ctype_graph ctype_print ctype_punct ctype_space ctype_upper ctype_xdigit virtual apache_request_headers apache_note apache_lookup_uri apache_child_terminate apache_setenv apache_response_headers apache_get_version getallheaders mysql_connect mysql_pconnect mysql_close mysql_select_db mysql_create_db mysql_drop_db mysql_query mysql_unbuffered_query mysql_db_query mysql_list_dbs mysql_list_tables mysql_list_fields mysql_list_processes mysql_error mysql_errno mysql_affected_rows mysql_insert_id mysql_result mysql_num_rows mysql_num_fields mysql_fetch_row mysql_fetch_array mysql_fetch_assoc mysql_fetch_object mysql_data_seek mysql_fetch_lengths mysql_fetch_field mysql_field_seek mysql_free_result mysql_field_name mysql_field_table mysql_field_len mysql_field_type mysql_field_flags mysql_escape_string mysql_real_escape_string mysql_stat mysql_thread_id mysql_client_encoding mysql_get_client_info mysql_get_host_info mysql_get_proto_info mysql_get_server_info mysql_info mysql mysql_fieldname mysql_fieldtable mysql_fieldlen mysql_fieldtype mysql_fieldflags mysql_selectdb mysql_createdb mysql_dropdb mysql_freeresult mysql_numfields mysql_numrows mysql_listdbs mysql_listtables mysql_listfields mysql_db_name mysql_dbname mysql_tablename mysql_table_name pg_connect pg_pconnect pg_close pg_connection_status pg_connection_busy pg_connection_reset pg_host pg_dbname pg_port pg_tty pg_options pg_ping pg_query pg_send_query pg_cancel_query pg_fetch_result pg_fetch_row pg_fetch_assoc pg_fetch_array pg_fetch_object pg_fetch_all pg_affected_rows pg_get_result pg_result_seek pg_result_status pg_free_result pg_last_oid pg_num_rows pg_num_fields pg_field_name pg_field_num pg_field_size pg_field_type pg_field_prtlen pg_field_is_null pg_get_notify pg_get_pid pg_result_error pg_last_error pg_last_notice pg_put_line pg_end_copy pg_copy_to pg_copy_from pg_trace pg_untrace pg_lo_create pg_lo_unlink pg_lo_open pg_lo_close pg_lo_read pg_lo_write pg_lo_read_all pg_lo_import pg_lo_export pg_lo_seek pg_lo_tell pg_escape_string pg_escape_bytea pg_unescape_bytea pg_client_encoding pg_set_client_encoding pg_meta_data pg_convert pg_insert pg_update pg_delete pg_select pg_exec pg_getlastoid pg_cmdtuples pg_errormessage pg_numrows pg_numfields pg_fieldname pg_fieldsize pg_fieldtype pg_fieldnum pg_fieldprtlen pg_fieldisnull pg_freeresult pg_result pg_loreadall pg_locreate pg_lounlink pg_loopen pg_loclose pg_loread pg_lowrite pg_loimport pg_loexport echo print global static exit array empty eval isset unset die include require include_once require_once"),
multiLineStrings: true,
hooks: {
"$": function(stream) {
Expand Down
14 changes: 9 additions & 5 deletions mode/xml/xml.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,14 +211,16 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
}
function endtag(startOfLine) {
return function(type) {
var tagName = curState.tagName;
curState.tagName = null;
if (type == "selfcloseTag" ||
(type == "endTag" && Kludges.autoSelfClosers.hasOwnProperty(curState.tagName.toLowerCase()))) {
maybePopContext(curState.tagName.toLowerCase());
(type == "endTag" && Kludges.autoSelfClosers.hasOwnProperty(tagName.toLowerCase()))) {
maybePopContext(tagName.toLowerCase());
return cont();
}
if (type == "endTag") {
maybePopContext(curState.tagName.toLowerCase());
pushContext(curState.tagName, startOfLine);
maybePopContext(tagName.toLowerCase());
pushContext(tagName, startOfLine);
return cont();
}
return cont();
Expand Down Expand Up @@ -310,7 +312,9 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
else return 0;
},

electricChars: "/"
electricChars: "/",

configuration: parserConfig.htmlMode ? "html" : "xml"
};
});

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codemirror",
"version":"3.0.11",
"version":"3.0.12",
"main": "codemirror.js",
"description": "In-browser code editing made bearable",
"licenses": [{"type": "MIT",
Expand Down
2 changes: 2 additions & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<script src="../lib/util/overlay.js"></script>
<script src="../mode/javascript/javascript.js"></script>
<script src="../mode/xml/xml.js"></script>
<script src="../keymap/vim.js"></script>

<style type="text/css">
.ok {color: #090;}
Expand Down Expand Up @@ -52,6 +53,7 @@ <h1>CodeMirror: Test Suite</h1>
<script src="../mode/stex/test.js"></script>
<script src="../mode/xquery/xquery.js"></script>
<script src="../mode/xquery/test.js"></script>
<script src="vim_test.js"></script>
<script>
window.onload = function() {
runHarness();
Expand Down
69 changes: 69 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,43 @@ testCM("selection", function(cm) {
eqPos(cm.getCursor(true), {line: 1, ch: 2});
}, {value: "111111\n222222\n333333"});

testCM("extendSelection", function(cm) {
cm.setExtending(true);
addDoc(cm, 10, 10);
cm.setSelection({line: 3, ch: 5});
eqPos(cm.getCursor("head"), {line: 3, ch: 5});
eqPos(cm.getCursor("anchor"), {line: 3, ch: 5});
cm.setSelection({line: 2, ch: 5}, {line: 5, ch: 5});
eqPos(cm.getCursor("head"), {line: 5, ch: 5});
eqPos(cm.getCursor("anchor"), {line: 2, ch: 5});
eqPos(cm.getCursor("start"), {line: 2, ch: 5});
eqPos(cm.getCursor("end"), {line: 5, ch: 5});
cm.setSelection({line: 5, ch: 5}, {line: 2, ch: 5});
eqPos(cm.getCursor("head"), {line: 2, ch: 5});
eqPos(cm.getCursor("anchor"), {line: 5, ch: 5});
eqPos(cm.getCursor("start"), {line: 2, ch: 5});
eqPos(cm.getCursor("end"), {line: 5, ch: 5});
cm.extendSelection({line: 3, ch: 2});
eqPos(cm.getCursor("head"), {line: 3, ch: 2});
eqPos(cm.getCursor("anchor"), {line: 5, ch: 5});
cm.extendSelection({line: 6, ch: 2});
eqPos(cm.getCursor("head"), {line: 6, ch: 2});
eqPos(cm.getCursor("anchor"), {line: 5, ch: 5});
cm.extendSelection({line: 6, ch: 3}, {line: 6, ch: 4});
eqPos(cm.getCursor("head"), {line: 6, ch: 4});
eqPos(cm.getCursor("anchor"), {line: 5, ch: 5});
cm.extendSelection({line: 0, ch: 3}, {line: 0, ch: 4});
eqPos(cm.getCursor("head"), {line: 0, ch: 3});
eqPos(cm.getCursor("anchor"), {line: 5, ch: 5});
cm.extendSelection({line: 4, ch: 5}, {line: 6, ch: 5});
eqPos(cm.getCursor("head"), {line: 6, ch: 5});
eqPos(cm.getCursor("anchor"), {line: 4, ch: 5});
cm.setExtending(false);
cm.extendSelection({line: 0, ch: 3}, {line: 0, ch: 4});
eqPos(cm.getCursor("head"), {line: 0, ch: 4});
eqPos(cm.getCursor("anchor"), {line: 0, ch: 3});
});

testCM("lines", function(cm) {
eq(cm.getLine(0), "111111");
eq(cm.getLine(1), "222222");
Expand Down Expand Up @@ -1096,3 +1133,35 @@ testCM("dirtyBit", function(cm) {
cm.redo();
eq(cm.isClean(), true);
});

testCM("addKeyMap", function(cm) {
function sendKey(code) {
cm.triggerOnKeyDown({type: "keydown", keyCode: code,
preventDefault: function(){}, stopPropagation: function(){}});
}

sendKey(39);
eqPos(cm.getCursor(), {line: 0, ch: 1});
var test = 0;
var map1 = {Right: function() { ++test; }}, map2 = {Right: function() { test += 10; }}
cm.addKeyMap(map1);
sendKey(39);
eqPos(cm.getCursor(), {line: 0, ch: 1});
eq(test, 1);
cm.addKeyMap(map2);
sendKey(39);
eq(test, 2);
cm.removeKeyMap(map1);
sendKey(39);
eq(test, 12);
cm.removeKeyMap(map2);
sendKey(39);
eq(test, 12);
eqPos(cm.getCursor(), {line: 0, ch: 2});
cm.addKeyMap({Right: function() { test = 55; }, name: "mymap"});
sendKey(39);
eq(test, 55);
cm.removeKeyMap("mymap");
sendKey(39);
eqPos(cm.getCursor(), {line: 0, ch: 3});
}, {value: "abc"});
433 changes: 433 additions & 0 deletions test/vim_test.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion theme/twilight.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@
.cm-s-twilight .cm-attribute { color: #d6bb6d; } /*?*/
.cm-s-twilight .cm-header { color: #FF6400; }
.cm-s-twilight .cm-hr { color: #AEAEAE; }
.cm-s-twilight .cm-link { color:#ad9361; font-style:italic; font-underline:none; } /**/
.cm-s-twilight .cm-link { color:#ad9361; font-style:italic; text-decoration:none; } /**/