4 var Pos = CodeMirror.Pos;
5 function posEq(a, b) { return a.line == b.line && a.ch == b.ch; }
10 function addToRing(str) {
12 if (killRing.length > 50) killRing.shift();
14 function growRingTop(str) {
15 if (!killRing.length) return addToRing(str);
16 killRing[killRing.length - 1] += str;
18 function getFromRing(n) { return killRing[killRing.length - (n ? Math.min(n, 1) : 1)] || ""; }
19 function popFromRing() { if (killRing.length > 1) killRing.pop(); return getFromRing(); }
23 function kill(cm, from, to, mayGrow, text) {
24 if (text == null) text = cm.getRange(from, to);
26 if (mayGrow && lastKill && lastKill.cm == cm && posEq(from, lastKill.pos) && cm.isClean(lastKill.gen))
30 cm.replaceRange("", from, to, "+delete");
32 if (mayGrow) lastKill = {cm: cm, pos: from, gen: cm.changeGeneration()};
36 // Boundaries of various units
38 function byChar(cm, pos, dir) {
39 return cm.findPosH(pos, dir, "char", true);
42 function byWord(cm, pos, dir) {
43 return cm.findPosH(pos, dir, "word", true);
46 function byLine(cm, pos, dir) {
47 return cm.findPosV(pos, dir, "line", cm.doc.sel.goalColumn);
50 function byPage(cm, pos, dir) {
51 return cm.findPosV(pos, dir, "page", cm.doc.sel.goalColumn);
54 function byParagraph(cm, pos, dir) {
55 var no = pos.line, line = cm.getLine(no);
56 var sawText = /\S/.test(dir < 0 ? line.slice(0, pos.ch) : line.slice(pos.ch));
57 var fst = cm.firstLine(), lst = cm.lastLine();
60 if (no < fst || no > lst)
61 return cm.clipPos(Pos(no - dir, dir < 0 ? 0 : null));
62 line = cm.getLine(no);
63 var hasText = /\S/.test(line);
64 if (hasText) sawText = true;
65 else if (sawText) return Pos(no, 0);
69 function bySentence(cm, pos, dir) {
70 var line = pos.line, ch = pos.ch;
71 var text = cm.getLine(pos.line), sawWord = false;
73 var next = text.charAt(ch + (dir < 0 ? -1 : 0));
74 if (!next) { // End/beginning of line reached
75 if (line == (dir < 0 ? cm.firstLine() : cm.lastLine())) return Pos(line, ch);
76 text = cm.getLine(line + dir);
77 if (!/\S/.test(text)) return Pos(line, ch);
79 ch = dir < 0 ? text.length : 0;
82 if (sawWord && /[!?.]/.test(next)) return Pos(line, ch + (dir > 0 ? 1 : 0));
83 if (!sawWord) sawWord = /\w/.test(next);
88 function byExpr(cm, pos, dir) {
90 if (cm.findMatchingBracket && (wrap = cm.findMatchingBracket(pos, true))
91 && wrap.match && (wrap.forward ? 1 : -1) == dir)
92 return dir > 0 ? Pos(wrap.to.line, wrap.to.ch + 1) : wrap.to;
94 for (var first = true;; first = false) {
95 var token = cm.getTokenAt(pos);
96 var after = Pos(pos.line, dir < 0 ? token.start : token.end);
97 if (first && dir > 0 && token.end == pos.ch || !/\w/.test(token.string)) {
98 var newPos = cm.findPosH(after, dir, "char");
99 if (posEq(after, newPos)) return pos;
107 // Prefixes (only crudely supported)
109 function getPrefix(cm, precise) {
110 var digits = cm.state.emacsPrefix;
111 if (!digits) return precise ? null : 1;
113 return digits == "-" ? -1 : Number(digits);
116 function repeated(cmd) {
117 var f = typeof cmd == "string" ? function(cm) { cm.execCommand(cmd); } : cmd;
118 return function(cm) {
119 var prefix = getPrefix(cm);
121 for (var i = 1; i < prefix; ++i) f(cm);
125 function findEnd(cm, by, dir) {
126 var pos = cm.getCursor(), prefix = getPrefix(cm);
127 if (prefix < 0) { dir = -dir; prefix = -prefix; }
128 for (var i = 0; i < prefix; ++i) {
129 var newPos = by(cm, pos, dir);
130 if (posEq(newPos, pos)) break;
136 function move(by, dir) {
137 var f = function(cm) {
138 cm.extendSelection(findEnd(cm, by, dir));
144 function killTo(cm, by, dir) {
145 kill(cm, cm.getCursor(), findEnd(cm, by, dir), true);
148 function addPrefix(cm, digit) {
149 if (cm.state.emacsPrefix) {
150 if (digit != "-") cm.state.emacsPrefix += digit;
154 cm.state.emacsPrefix = digit;
155 cm.on("keyHandled", maybeClearPrefix);
156 cm.on("inputRead", maybeDuplicateInput);
159 var prefixPreservingKeys = {"Alt-G": true, "Ctrl-X": true, "Ctrl-Q": true, "Ctrl-U": true};
161 function maybeClearPrefix(cm, arg) {
162 if (!cm.state.emacsPrefixMap && !prefixPreservingKeys.hasOwnProperty(arg))
166 function clearPrefix(cm) {
167 cm.state.emacsPrefix = null;
168 cm.off("keyHandled", maybeClearPrefix);
169 cm.off("inputRead", maybeDuplicateInput);
172 function maybeDuplicateInput(cm, event) {
173 var dup = getPrefix(cm);
174 if (dup > 1 && event.origin == "+input") {
175 var one = event.text.join("\n"), txt = "";
176 for (var i = 1; i < dup; ++i) txt += one;
177 cm.replaceSelection(txt, "end", "+input");
181 function addPrefixMap(cm) {
182 cm.state.emacsPrefixMap = true;
183 cm.addKeyMap(prefixMap);
184 cm.on("keyHandled", maybeRemovePrefixMap);
185 cm.on("inputRead", maybeRemovePrefixMap);
188 function maybeRemovePrefixMap(cm, arg) {
189 if (typeof arg == "string" && (/^\d$/.test(arg) || arg == "Ctrl-U")) return;
190 cm.removeKeyMap(prefixMap);
191 cm.state.emacsPrefixMap = false;
192 cm.off("keyHandled", maybeRemovePrefixMap);
193 cm.off("inputRead", maybeRemovePrefixMap);
198 function setMark(cm) {
199 cm.setCursor(cm.getCursor());
200 cm.setExtending(true);
201 cm.on("change", function() { cm.setExtending(false); });
204 function getInput(cm, msg, f) {
206 cm.openDialog(msg + ": <input type=\"text\" style=\"width: 10em\"/>", f, {bottom: true});
211 function operateOnWord(cm, op) {
212 var start = cm.getCursor(), end = cm.findPosH(start, 1, "word");
213 cm.replaceRange(op(cm.getRange(start, end)), start, end);
217 function toEnclosingExpr(cm) {
218 var pos = cm.getCursor(), line = pos.line, ch = pos.ch;
220 while (line >= cm.firstLine()) {
221 var text = cm.getLine(line);
222 for (var i = ch == null ? text.length : ch; i > 0;) {
223 var ch = text.charAt(--i);
230 else if (/[\(\{\[]/.test(ch) && (!stack.length || stack.pop() != ch))
231 return cm.extendSelection(Pos(line, i));
239 var keyMap = CodeMirror.keyMap.emacs = {
240 "Ctrl-W": function(cm) {kill(cm, cm.getCursor("start"), cm.getCursor("end"));},
241 "Ctrl-K": repeated(function(cm) {
242 var start = cm.getCursor(), end = cm.clipPos(Pos(start.line));
243 var text = cm.getRange(start, end);
244 if (!/\S/.test(text)) {
246 end = Pos(start.line + 1, 0);
248 kill(cm, start, end, true, text);
250 "Alt-W": function(cm) {
251 addToRing(cm.getSelection());
253 "Ctrl-Y": function(cm) {
254 var start = cm.getCursor();
255 cm.replaceRange(getFromRing(getPrefix(cm)), start, start, "paste");
256 cm.setSelection(start, cm.getCursor());
258 "Alt-Y": function(cm) {cm.replaceSelection(popFromRing());},
260 "Ctrl-Space": setMark, "Ctrl-Shift-2": setMark,
262 "Ctrl-F": move(byChar, 1), "Ctrl-B": move(byChar, -1),
263 "Right": move(byChar, 1), "Left": move(byChar, -1),
264 "Ctrl-D": function(cm) { killTo(cm, byChar, 1); },
265 "Delete": function(cm) { killTo(cm, byChar, 1); },
266 "Ctrl-H": function(cm) { killTo(cm, byChar, -1); },
267 "Backspace": function(cm) { killTo(cm, byChar, -1); },
269 "Alt-F": move(byWord, 1), "Alt-B": move(byWord, -1),
270 "Alt-D": function(cm) { killTo(cm, byWord, 1); },
271 "Alt-Backspace": function(cm) { killTo(cm, byWord, -1); },
273 "Ctrl-N": move(byLine, 1), "Ctrl-P": move(byLine, -1),
274 "Down": move(byLine, 1), "Up": move(byLine, -1),
275 "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
276 "End": "goLineEnd", "Home": "goLineStart",
278 "Alt-V": move(byPage, -1), "Ctrl-V": move(byPage, 1),
279 "PageUp": move(byPage, -1), "PageDown": move(byPage, 1),
281 "Ctrl-Up": move(byParagraph, -1), "Ctrl-Down": move(byParagraph, 1),
283 "Alt-A": move(bySentence, -1), "Alt-E": move(bySentence, 1),
284 "Alt-K": function(cm) { killTo(cm, bySentence, 1); },
286 "Ctrl-Alt-K": function(cm) { killTo(cm, byExpr, 1); },
287 "Ctrl-Alt-Backspace": function(cm) { killTo(cm, byExpr, -1); },
288 "Ctrl-Alt-F": move(byExpr, 1), "Ctrl-Alt-B": move(byExpr, -1),
290 "Shift-Ctrl-Alt-2": function(cm) {
291 cm.setSelection(findEnd(cm, byExpr, 1), cm.getCursor());
293 "Ctrl-Alt-T": function(cm) {
294 var leftStart = byExpr(cm, cm.getCursor(), -1), leftEnd = byExpr(cm, leftStart, 1);
295 var rightEnd = byExpr(cm, leftEnd, 1), rightStart = byExpr(cm, rightEnd, -1);
296 cm.replaceRange(cm.getRange(rightStart, rightEnd) + cm.getRange(leftEnd, rightStart) +
297 cm.getRange(leftStart, leftEnd), leftStart, rightEnd);
299 "Ctrl-Alt-U": repeated(toEnclosingExpr),
301 "Alt-Space": function(cm) {
302 var pos = cm.getCursor(), from = pos.ch, to = pos.ch, text = cm.getLine(pos.line);
303 while (from && /\s/.test(text.charAt(from - 1))) --from;
304 while (to < text.length && /\s/.test(text.charAt(to))) ++to;
305 cm.replaceRange(" ", Pos(pos.line, from), Pos(pos.line, to));
307 "Ctrl-O": repeated(function(cm) { cm.replaceSelection("\n", "start"); }),
308 "Ctrl-T": repeated(function(cm) {
309 var pos = cm.getCursor();
310 if (pos.ch < cm.getLine(pos.line).length) pos = Pos(pos.line, pos.ch + 1);
311 var from = cm.findPosH(pos, -2, "char");
312 var range = cm.getRange(from, pos);
313 if (range.length != 2) return;
314 cm.setSelection(from, pos);
315 cm.replaceSelection(range.charAt(1) + range.charAt(0), "end");
318 "Alt-C": repeated(function(cm) {
319 operateOnWord(cm, function(w) {
320 var letter = w.search(/\w/);
321 if (letter == -1) return w;
322 return w.slice(0, letter) + w.charAt(letter).toUpperCase() + w.slice(letter + 1).toLowerCase();
325 "Alt-U": repeated(function(cm) {
326 operateOnWord(cm, function(w) { return w.toUpperCase(); });
328 "Alt-L": repeated(function(cm) {
329 operateOnWord(cm, function(w) { return w.toLowerCase(); });
332 "Alt-;": "toggleComment",
334 "Ctrl-/": repeated("undo"), "Shift-Ctrl--": repeated("undo"),
335 "Ctrl-Z": repeated("undo"), "Cmd-Z": repeated("undo"),
336 "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd",
337 "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": "clearSearch", "Shift-Alt-5": "replace",
338 "Alt-/": "autocomplete",
339 "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto",
341 "Alt-G": function(cm) {cm.setOption("keyMap", "emacs-Alt-G");},
342 "Ctrl-X": function(cm) {cm.setOption("keyMap", "emacs-Ctrl-X");},
343 "Ctrl-Q": function(cm) {cm.setOption("keyMap", "emacs-Ctrl-Q");},
344 "Ctrl-U": addPrefixMap
347 CodeMirror.keyMap["emacs-Ctrl-X"] = {
348 "Tab": function(cm) {
349 cm.indentSelection(getPrefix(cm, true) || cm.getOption("indentUnit"));
351 "Ctrl-X": function(cm) {
352 cm.setSelection(cm.getCursor("head"), cm.getCursor("anchor"));
355 "Ctrl-S": "save", "Ctrl-W": "save", "S": "saveAll", "F": "open", "U": repeated("undo"), "K": "close",
356 "Delete": function(cm) { kill(cm, cm.getCursor(), sentenceEnd(cm, 1), true); },
357 auto: "emacs", nofallthrough: true, disableInput: true
360 CodeMirror.keyMap["emacs-Alt-G"] = {
362 var prefix = getPrefix(cm, true);
363 if (prefix != null && prefix > 0) return cm.setCursor(prefix - 1);
365 getInput(cm, "Goto line", function(str) {
367 if (str && !isNaN(num = Number(str)) && num == num|0 && num > 0)
368 cm.setCursor(num - 1);
371 auto: "emacs", nofallthrough: true, disableInput: true
374 CodeMirror.keyMap["emacs-Ctrl-Q"] = {
375 "Tab": repeated("insertTab"),
376 auto: "emacs", nofallthrough: true
379 var prefixMap = {"Ctrl-G": clearPrefix};
380 function regPrefix(d) {
381 prefixMap[d] = function(cm) { addPrefix(cm, d); };
382 keyMap["Ctrl-" + d] = function(cm) { addPrefix(cm, d); };
383 prefixPreservingKeys["Ctrl-" + d] = true;
385 for (var i = 0; i < 10; ++i) regPrefix(String(i));