Skip to content

Commit 11911a0

Browse files
committed
Add autoformatting utility script (and demo)
Courtesy of Alexey Kolosov
1 parent cf7aed9 commit 11911a0

File tree

3 files changed

+372
-0
lines changed

3 files changed

+372
-0
lines changed

demo/formatting.html

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>CodeMirror 2: Formatting Demo</title>
5+
<link rel="stylesheet" href="../lib/codemirror.css">
6+
<script src="../lib/codemirror.js"></script>
7+
<script src="../lib/util/formatting.js"></script>
8+
<script src="../mode/css/css.js"></script>
9+
<script src="../mode/xml/xml.js"></script>
10+
<script src="../mode/javascript/javascript.js"></script>
11+
<script src="../mode/htmlmixed/htmlmixed.js"></script>
12+
<link rel="stylesheet" href="../doc/docs.css">
13+
14+
<style type="text/css">
15+
.CodeMirror {
16+
border: 1px solid #eee;
17+
}
18+
td {
19+
padding-right: 20px;
20+
}
21+
</style>
22+
</head>
23+
<body>
24+
<h1>CodeMirror 2: Formatting demo</h1>
25+
26+
<form><textarea id="code" name="code"><script> function (s,e){ for(var i=0; i < 1; i++) test("test();a=1");} </script>
27+
<script>
28+
function test(c){ for (var i = 0; i < 10; i++){ process("a.b();c = null;", 300);}
29+
}
30+
</script>
31+
<table><tr><td>test 1</td></tr><tr><td>test 2</td></tr></table>
32+
<script> function test() { return 1;} </script>
33+
<style> .test { font-size: medium; font-family: monospace; }
34+
</style></textarea></form>
35+
36+
<p>Select a piece of code and click one of the links below to apply automatic formatting to the selected text or comment/uncomment the selected text. Note that the formatting behavior depends on the current block's mode.
37+
<table>
38+
<tr>
39+
<td>
40+
<a href="javascript:autoFormatSelection()">
41+
Autoformat Selected
42+
</a>
43+
</td>
44+
<td>
45+
<a href="javascript:commentSelection(true)">
46+
Comment Selected
47+
</a>
48+
</td>
49+
<td>
50+
<a href="javascript:commentSelection(false)">
51+
Uncomment Selected
52+
</a>
53+
</td>
54+
</tr>
55+
</table>
56+
</p>
57+
<script>
58+
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
59+
lineNumbers: true,
60+
mode: "htmlmixed"
61+
});
62+
CodeMirror.commands["selectAll"](editor);
63+
64+
function getSelectedRange() {
65+
return { from: editor.getCursor(true), to: editor.getCursor(false) };
66+
}
67+
68+
function autoFormatSelection() {
69+
var range = getSelectedRange();
70+
editor.autoFormatRange(range.from, range.to);
71+
}
72+
73+
function commentSelection(isComment) {
74+
var range = getSelectedRange();
75+
editor.commentRange(isComment, range.from, range.to);
76+
}
77+
</script>
78+
79+
</body>
80+
</html>

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ <h2 style="margin-top: 0">Usage demos:</h2>
8787
<li><a href="demo/fullscreen.html">Full-screen editing</a></li>
8888
<li><a href="demo/changemode.html">Mode auto-changing</a></li>
8989
<li><a href="demo/visibletabs.html">Visible tabs</a></li>
90+
<li><a href="demo/formatting.html">Autoformatting of code</a></li>
9091
<li><a href="demo/emacs.html">Emacs keybindings</a></li>
9192
<li><a href="demo/vim.html">Vim keybindings</a></li>
9293
</ul>

lib/util/formatting.js

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
// ============== Formatting extensions ============================
2+
// A common storage for all mode-specific formatting features
3+
if (!CodeMirror.modeExtensions) CodeMirror.modeExtensions = {};
4+
5+
// Returns the extension of the editor's current mode
6+
CodeMirror.defineExtension("getModeExt", function () {
7+
return CodeMirror.modeExtensions[this.getOption("mode")];
8+
});
9+
10+
// If the current mode is 'htmlmixed', returns the extension of a mode located at
11+
// the specified position (can be htmlmixed, css or javascript). Otherwise, simply
12+
// returns the extension of the editor's current mode.
13+
CodeMirror.defineExtension("getModeExtAtPos", function (pos) {
14+
var token = this.getTokenAt(pos);
15+
if (token && token.state && token.state.mode)
16+
return CodeMirror.modeExtensions[token.state.mode == "html" ? "htmlmixed" : token.state.mode];
17+
else
18+
return this.getModeExt();
19+
});
20+
21+
// Comment/uncomment the specified range
22+
CodeMirror.defineExtension("commentRange", function (isComment, from, to) {
23+
var curMode = this.getModeExtAtPos(this.getCursor());
24+
if (isComment) { // Comment range
25+
var commentedText = this.getRange(from, to);
26+
this.replaceRange(curMode.commentStart + this.getRange(from, to) + curMode.commentEnd
27+
, from, to);
28+
if (from.line == to.line && from.ch == to.ch) { // An empty comment inserted - put cursor inside
29+
this.setCursor(from.line, from.ch + curMode.commentStart.length);
30+
}
31+
}
32+
else { // Uncomment range
33+
var selText = this.getRange(from, to);
34+
var startIndex = selText.indexOf(curMode.commentStart);
35+
var endIndex = selText.lastIndexOf(curMode.commentEnd);
36+
if (startIndex > -1 && endIndex > -1 && endIndex > startIndex) {
37+
// Take string till comment start
38+
selText = selText.substr(0, startIndex)
39+
// From comment start till comment end
40+
+ selText.substring(startIndex + curMode.commentStart.length, endIndex)
41+
// From comment end till string end
42+
+ selText.substr(endIndex + curMode.commentEnd.length);
43+
}
44+
this.replaceRange(selText, from, to);
45+
}
46+
});
47+
48+
// Applies automatic mode-aware indentation to the specified range
49+
CodeMirror.defineExtension("autoIndentRange", function (from, to) {
50+
var cmInstance = this;
51+
this.operation(function () {
52+
for (var i = from.line; i <= to.line; i++) {
53+
cmInstance.indentLine(i);
54+
}
55+
});
56+
});
57+
58+
// Applies automatic formatting to the specified range
59+
CodeMirror.defineExtension("autoFormatRange", function (from, to) {
60+
var absStart = this.indexFromPos(from);
61+
var absEnd = this.indexFromPos(to);
62+
// Insert additional line breaks where necessary according to the
63+
// mode's syntax
64+
var res = this.getModeExt().autoFormatLineBreaks(this.getValue(), absStart, absEnd);
65+
var cmInstance = this;
66+
67+
// Replace and auto-indent the range
68+
this.operation(function () {
69+
cmInstance.replaceRange(res, from, to);
70+
var startLine = cmInstance.posFromIndex(absStart).line;
71+
var endLine = cmInstance.posFromIndex(absStart + res.length).line;
72+
for (var i = startLine; i <= endLine; i++) {
73+
cmInstance.indentLine(i);
74+
}
75+
});
76+
});
77+
78+
// Define extensions for a few modes
79+
80+
CodeMirror.modeExtensions["css"] = {
81+
commentStart: "/*",
82+
commentEnd: "*/",
83+
wordWrapChars: [";", "\\{", "\\}"],
84+
autoFormatLineBreaks: function (text) {
85+
return text.replace(new RegExp("(;|\\{|\\})([^\r\n])", "g"), "$1\n$2");
86+
}
87+
};
88+
89+
CodeMirror.modeExtensions["javascript"] = {
90+
commentStart: "/*",
91+
commentEnd: "*/",
92+
wordWrapChars: [";", "\\{", "\\}"],
93+
94+
getNonBreakableBlocks: function (text) {
95+
var nonBreakableRegexes = [
96+
new RegExp("for\\s*?\\(([\\s\\S]*?)\\)"),
97+
new RegExp("'([\\s\\S]*?)('|$)"),
98+
new RegExp("\"([\\s\\S]*?)(\"|$)"),
99+
new RegExp("//.*([\r\n]|$)")
100+
];
101+
var nonBreakableBlocks = new Array();
102+
for (var i = 0; i < nonBreakableRegexes.length; i++) {
103+
var curPos = 0;
104+
while (curPos < text.length) {
105+
var m = text.substr(curPos).match(nonBreakableRegexes[i]);
106+
if (m != null) {
107+
nonBreakableBlocks.push({
108+
start: curPos + m.index,
109+
end: curPos + m.index + m[0].length
110+
});
111+
curPos += m.index + Math.max(1, m[0].length);
112+
}
113+
else { // No more matches
114+
break;
115+
}
116+
}
117+
}
118+
nonBreakableBlocks.sort(function (a, b) {
119+
return a.start - b.start;
120+
});
121+
122+
return nonBreakableBlocks;
123+
},
124+
125+
autoFormatLineBreaks: function (text) {
126+
var curPos = 0;
127+
var reLinesSplitter = new RegExp("(;|\\{|\\})([^\r\n])", "g");
128+
var nonBreakableBlocks = this.getNonBreakableBlocks(text);
129+
if (nonBreakableBlocks != null) {
130+
var res = "";
131+
for (var i = 0; i < nonBreakableBlocks.length; i++) {
132+
if (nonBreakableBlocks[i].start > curPos) { // Break lines till the block
133+
res += text.substring(curPos, nonBreakableBlocks[i].start).replace(reLinesSplitter, "$1\n$2");
134+
curPos = nonBreakableBlocks[i].start;
135+
}
136+
if (nonBreakableBlocks[i].start <= curPos
137+
&& nonBreakableBlocks[i].end >= curPos) { // Skip non-breakable block
138+
res += text.substring(curPos, nonBreakableBlocks[i].end);
139+
curPos = nonBreakableBlocks[i].end;
140+
}
141+
}
142+
if (curPos < text.length - 1) {
143+
res += text.substr(curPos).replace(reLinesSplitter, "$1\n$2");
144+
}
145+
return res;
146+
}
147+
else {
148+
return text.replace(reLinesSplitter, "$1\n$2");
149+
}
150+
}
151+
};
152+
153+
CodeMirror.modeExtensions["xml"] = {
154+
commentStart: "<!--",
155+
commentEnd: "-->",
156+
wordWrapChars: [">"],
157+
158+
autoFormatLineBreaks: function (text) {
159+
var lines = text.split("\n");
160+
var reProcessedPortion = new RegExp("(^\\s*?<|^[^<]*?)(.+)(>\\s*?$|[^>]*?$)");
161+
var reOpenBrackets = new RegExp("<", "g");
162+
var reCloseBrackets = new RegExp("(>)([^\r\n])", "g");
163+
for (var i = 0; i < lines.length; i++) {
164+
var mToProcess = lines[i].match(reProcessedPortion);
165+
if (mToProcess != null && mToProcess.length > 3) { // The line starts with whitespaces and ends with whitespaces
166+
lines[i] = mToProcess[1]
167+
+ mToProcess[2].replace(reOpenBrackets, "\n$&").replace(reCloseBrackets, "$1\n$2")
168+
+ mToProcess[3];
169+
continue;
170+
}
171+
}
172+
173+
return lines.join("\n");
174+
}
175+
};
176+
177+
CodeMirror.modeExtensions["htmlmixed"] = {
178+
commentStart: "<!--",
179+
commentEnd: "-->",
180+
wordWrapChars: [">", ";", "\\{", "\\}"],
181+
182+
getModeInfos: function (text, absPos) {
183+
var modeInfos = new Array();
184+
modeInfos[0] =
185+
{
186+
pos: 0,
187+
modeExt: CodeMirror.modeExtensions["xml"],
188+
modeName: "xml"
189+
};
190+
191+
var modeMatchers = new Array();
192+
modeMatchers[0] =
193+
{
194+
regex: new RegExp("<style[^>]*>([\\s\\S]*?)(</style[^>]*>|$)", "i"),
195+
modeExt: CodeMirror.modeExtensions["css"],
196+
modeName: "css"
197+
};
198+
modeMatchers[1] =
199+
{
200+
regex: new RegExp("<script[^>]*>([\\s\\S]*?)(</script[^>]*>|$)", "i"),
201+
modeExt: CodeMirror.modeExtensions["javascript"],
202+
modeName: "javascript"
203+
};
204+
205+
var lastCharPos = (typeof (absPos) !== "undefined" ? absPos : text.length - 1);
206+
// Detect modes for the entire text
207+
for (var i = 0; i < modeMatchers.length; i++) {
208+
var curPos = 0;
209+
while (curPos <= lastCharPos) {
210+
var m = text.substr(curPos).match(modeMatchers[i].regex);
211+
if (m != null) {
212+
if (m.length > 1 && m[1].length > 0) {
213+
// Push block begin pos
214+
var blockBegin = curPos + m.index + m[0].indexOf(m[1]);
215+
modeInfos.push(
216+
{
217+
pos: blockBegin,
218+
modeExt: modeMatchers[i].modeExt,
219+
modeName: modeMatchers[i].modeName
220+
});
221+
// Push block end pos
222+
modeInfos.push(
223+
{
224+
pos: blockBegin + m[1].length,
225+
modeExt: modeInfos[0].modeExt,
226+
modeName: modeInfos[0].modeName
227+
});
228+
curPos += m.index + m[0].length;
229+
continue;
230+
}
231+
else {
232+
curPos += m.index + Math.max(m[0].length, 1);
233+
}
234+
}
235+
else { // No more matches
236+
break;
237+
}
238+
}
239+
}
240+
// Sort mode infos
241+
modeInfos.sort(function sortModeInfo(a, b) {
242+
return a.pos - b.pos;
243+
});
244+
245+
return modeInfos;
246+
},
247+
248+
autoFormatLineBreaks: function (text, startPos, endPos) {
249+
var modeInfos = this.getModeInfos(text);
250+
var reBlockStartsWithNewline = new RegExp("^\\s*?\n");
251+
var reBlockEndsWithNewline = new RegExp("\n\\s*?$");
252+
var res = "";
253+
// Use modes info to break lines correspondingly
254+
if (modeInfos.length > 1) { // Deal with multi-mode text
255+
for (var i = 1; i <= modeInfos.length; i++) {
256+
var selStart = modeInfos[i - 1].pos;
257+
var selEnd = (i < modeInfos.length ? modeInfos[i].pos : endPos);
258+
259+
if (selStart >= endPos) { // The block starts later than the needed fragment
260+
break;
261+
}
262+
if (selStart < startPos) {
263+
if (selEnd <= startPos) { // The block starts earlier than the needed fragment
264+
continue;
265+
}
266+
selStart = startPos;
267+
}
268+
if (selEnd > endPos) {
269+
selEnd = endPos;
270+
}
271+
var textPortion = text.substring(selStart, selEnd);
272+
if (modeInfos[i - 1].modeName != "xml") { // Starting a CSS or JavaScript block
273+
if (!reBlockStartsWithNewline.test(textPortion)
274+
&& selStart > 0) { // The block does not start with a line break
275+
textPortion = "\n" + textPortion;
276+
}
277+
if (!reBlockEndsWithNewline.test(textPortion)
278+
&& selEnd < text.length - 1) { // The block does not end with a line break
279+
textPortion += "\n";
280+
}
281+
}
282+
res += modeInfos[i - 1].modeExt.autoFormatLineBreaks(textPortion);
283+
}
284+
}
285+
else { // Single-mode text
286+
res = modeInfos[0].modeExt.autoFormatLineBreaks(text.substring(startPos, endPos));
287+
}
288+
289+
return res;
290+
}
291+
};

0 commit comments

Comments
 (0)