Permalink
Browse files

[util/formatting] Move over to new mode extension/introspection

This makes the formatter easier to adjust to new mode, and
cleans it up somewhat.
  • Loading branch information...
1 parent 23c8c3e commit ad7a8353270621ee9e13efdad0be453366deea21 @marijnh marijnh committed Sep 12, 2012
Showing with 165 additions and 263 deletions.
  1. +10 −3 doc/manual.html
  2. +8 −0 lib/codemirror.js
  3. +1 −6 lib/util/closetag.js
  4. +140 −253 lib/util/formatting.js
  5. +6 −1 mode/gfm/gfm.js
View
13 doc/manual.html
@@ -959,6 +959,11 @@ <h2 id="addons">Add-ons</h2>
Depends on
the <a href="#util_searchcursor"><code>searchcursor</code></a>
add-on. Demo <a href="../demo/matchhighlighter.html">here</a>.</dd>
+ <dt id="util_formatting"><a href="../lib/util/formatting.js"><code>formatting.js</code></a></dt>
+ <dd>Adds <code>commentRange</code>, <code>autoIndentRange</code>,
+ and <code>autoFormatRange</code> methods that, respectively,
+ comment (or uncomment), indent, or format (add line breaks) a
+ range of code. <a href="../demo/formatting.html">Demo here.</a></dd>
<dt id="util_closetag"><a href="../lib/util/closetag.js"><code>closetag.js</code></a></dt>
<dd>Provides utility functions for adding automatic tag closing
to XML modes. See
@@ -1163,9 +1168,11 @@ <h2 id="modeapi">Writing CodeMirror Modes</h2>
extra methods, <code>innerMode</code> which, given a state object,
returns a <code>{state, mode}</code> object with the inner mode
and its state for the current position. These are used by utility
- scripts such as the autoformatter and
- the <a href="#util_closetag">tag closer</a> to get context
- information.</p>
+ scripts such as the <a href="#util_formatting">autoformatter</a>
+ and the <a href="#util_closetag">tag closer</a> to get context
+ information. Use the <code>CodeMirror.innerMode</code> helper
+ function to, starting from a mode and a state, recursively walk
+ down to the innermost mode and state.</p>
<p>To make indentation work properly in a nested parser, it is
advisable to give the <code>startState</code> method of modes that
View
8 lib/codemirror.js
@@ -2273,6 +2273,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) {
View
7 lib/util/closetag.js
@@ -27,12 +27,7 @@
CodeMirror.defaults['closeTagVoid'] = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
function innerState(cm, state) {
- for (var mode = cm.getMode(); mode.innerMode;) {
- var info = mode.innerMode(state);
- mode = info.mode;
- state = info.state;
- }
- if (mode.name == "xml") return state;
+ return CodeMirror.innerMode(cm.getMode(), state).state;
}
View
393 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,149 @@ 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 reLinesSplitter = /(;|\{|\})([^\r\n;])/g;
+ var nonBreakableBlocks = jsNonBreakableBlocks(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;
+ }
}
+ if (curPos < text.length)
+ res += text.substr(curPos).replace(reLinesSplitter, "$1\n$2");
+ return res;
+ } else {
+ return text.replace(reLinesSplitter, "$1\n$2");
}
- if (curPos < text.length - 1) {
- res += text.substr(curPos).replace(reLinesSplitter, "$1\n$2");
- }
- return res;
}
- else {
- return text.replace(reLinesSplitter, "$1\n$2");
- }
- }
-};
-
-CodeMirror.modeExtensions["xml"] = {
- commentStart: "<!--",
- commentEnd: "-->",
- 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: "<!--",
+ 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;
+ 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: "<!--",
- commentEnd: "-->",
- 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("<style[^>]*>([\\s\\S]*?)(</style[^>]*>|$)", "i"),
- modeExt: CodeMirror.modeExtensions["css"],
- modeName: "css"
- };
- modeMatchers[1] =
- {
- regex: new RegExp("<script[^>]*>([\\s\\S]*?)(</script[^>]*>|$)", "i"),
- modeExt: CodeMirror.modeExtensions["javascript"],
- modeName: "javascript"
- };
+ function enumerateModesBetween(cm, line, start, end) {
+ var outer = cm.getMode();
+ if (!outer.innerMode) return [{from: start, to: end, mode: outer}];
+ var init = CodeMirror.innerMode(outer, cm.getTokenAt({line: line, ch: start}).state);
+ var state = init.state, mode = init.mode;
+ var found = [], stream = new CodeMirror.StringStream(cm.getLine(line));
+ stream.pos = stream.start = start;
+ for (;;) {
+ outer.token(stream, state);
+ var cur = CodeMirror.innerMode(outer, state).mode;
+ if (curMode != mode) {
+ found.push({from: start, to: stream.pos, mode: mode});
+ start = stream.pos;
+ 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);
+ this.operation(function() {
+ if (isComment) { // Comment range
+ this.replaceRange(curMode.commentEnd, to);
+ this.replaceRange(curMode.commentStart, from);
+ 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);
}
- }
- // 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 (i) 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;
- }
- 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";
- }
+ 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;
}
- 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");
+ });
+ });
+})();
View
7 mode/gfm/gfm.js
@@ -96,7 +96,7 @@ CodeMirror.defineMode("gfm", function(config, parserConfig) {
},
copyState: function(state) {
- return {token: state.token, mode: state.mode, mdState: CodeMirror.copyState(mdMode, state.mdState),
+ return {token: state.token, mdState: CodeMirror.copyState(mdMode, state.mdState),
localMode: state.localMode,
localState: state.localMode ? CodeMirror.copyState(state.localMode, state.localState) : null};
},
@@ -140,6 +140,11 @@ CodeMirror.defineMode("gfm", function(config, parserConfig) {
}
return state.token(stream, state);
+ },
+
+ innerMode: function(state) {
+ if (state.token == markdown) return {state: state.mdState, mode: mdMode};
+ else return {state: state.localState, mode: state.localMode};
}
};
}, "markdown");

0 comments on commit ad7a835

Please sign in to comment.