for loop efficiency
[conkeror.git] / modules / keymap.js
blobdf010a13c46f98fffb2c5e4811ab521649d53cc8
1 /**
2  * (C) Copyright 2004-2007 Shawn Betts
3  * (C) Copyright 2007-2009 John J. Foerch
4  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
5  *
6  * Use, modification, and distribution are subject to the terms specified in the
7  * COPYING file.
8 **/
10 /* Generate vk name table  */
11 var keycode_to_vk_name = [];
12 var vk_name_to_keycode = {};
13 let (KeyEvent = Ci.nsIDOMKeyEvent,
14      prefix = "DOM_VK_") {
15     for (var i in KeyEvent) {
16         /* Check if this is a key binding */
17         if (i.substr(0, prefix.length) == prefix) {
18             let name = i.substr(prefix.length).toLowerCase();
19             let code = KeyEvent[i];
20             keycode_to_vk_name[code] = name;
21             vk_name_to_keycode[name] = code;
22         }
23     }
28  * Modifiers
29  */
31 function modifier (in_event_p, set_in_event) {
32     this.in_event_p = in_event_p;
33     this.set_in_event = set_in_event;
36 var modifiers = {
37     A: new modifier(function (event) { return event.altKey; },
38                     function (event) { event.altKey = true; }),
39     C: new modifier(function (event) { return event.ctrlKey; },
40                     function (event) { event.ctrlKey = true; }),
41     M: new modifier(function (event) { return event.metaKey; },
42                     function (event) { event.metaKey = true; }),
43     S: new modifier(function (event) {
44                         if (event.shiftKey)
45                             return ((event.keyCode && event.keyCode < 41) ||
46                                     (event.charCode == 32) ||
47                                     (event.button != null));
48                         return false;
49                     },
50                     function (event) { event.shiftKey = true; })
52 var modifier_order = ['C', 'M', 'S'];
54 // check the platform and guess whether we should treat Alt as Meta
55 if (get_os() == 'Darwin') {
56     // In OS X, alt is a shift-like modifier, in that we
57     // only care about it for non-character events.
58     modifiers.A = new modifier(
59         function (event) {
60             if (event.altKey)
61                 return ((event.keyCode && event.keyCode < 41) ||
62                         (event.charCode == 32) ||
63                         (event.button != null));
64             return false;
65         },
66         function (event) { event.altKey = true; });
67     modifier_order = ['C', 'M', 'A', 'S'];
68 } else {
69     modifiers.M = modifiers.A;
75  * Combos
76  */
78 function format_key_combo (event) {
79     var combo = '';
80     for each (var M in modifier_order) {
81         if (modifiers[M].in_event_p(event) ||
82             (event.sticky_modifiers &&
83              event.sticky_modifiers.indexOf(M) != -1))
84         {
85             combo += (M + '-');
86         }
87     }
88     if (event.charCode) {
89         if (event.charCode == 32)
90             combo += 'space';
91         else
92             combo += String.fromCharCode(event.charCode);
93     } else if (event.keyCode) {
94         combo += keycode_to_vk_name[event.keyCode];
95     } else if (event.button != null) {
96         combo += "mouse" + (event.button + 1);
97     }
98     return combo;
101 function unformat_key_combo (combo) {
102     var event = {
103         keyCode: 0,
104         charCode: 0,
105         altKey: false,
106         ctrlKey: false,
107         metaKey: false,
108         shiftKey: false
109     };
110     var M;
111     var i = 0;
112     var len = combo.length - 2;
113     var res;
114     while (i < len && combo[i+1] == '-') {
115         M = combo[i];
116         modifiers[M].set_in_event(event);
117         i+=2;
118     }
119     var key = combo.substring(i);
120     if (key.length == 1)
121         event.charCode = key.charCodeAt(0);
122     else if (key == 'space')
123         event.charCode = 32;
124     else if (vk_name_to_keycode[key])
125         event.keyCode = vk_name_to_keycode[key];
126     else if (key.substring(0, 5) == 'mouse')
127         event.button = parseInt(key.substring(5));
128     return event;
134  * Keymap datatype
135  */
137 define_keywords("$parent", "$help", "$name", "$anonymous");
138 function keymap () {
139     keywords(arguments);
140     this.parent = arguments.$parent;
141     this.bindings = {};
142     this.predicate_bindings = [];
143     this.fallthrough = [];
144     this.help = arguments.$help;
145     this.name = arguments.$name;
146     this.anonymous = arguments.$anonymous;
149 function define_keymap (name) {
150     keywords(arguments);
151     this[name] = new keymap($name = name, forward_keywords(arguments));
157  * Key Match Predicates.
158  */
160 function define_key_match_predicate (name, description, predicate) {
161     conkeror[name] = predicate;
162     conkeror[name].name = name;
163     conkeror[name].description = description;
164     return conkeror[name];
167 define_key_match_predicate('match_any_key', 'any key',
168     function (event) true);
170 // should be renamed to match_any_unmodified_character
171 define_key_match_predicate('match_any_unmodified_character', 'any unmodified character',
172     function (event) {
173         // this predicate can be used for both keypress and keydown
174         // fallthroughs.
175         try {
176             return ((event.type == 'keypress' && event.charCode)
177                     || event.keyCode > 31)
178                 && !modifiers.A.in_event_p(event)
179                 && !event.metaKey
180                 && !event.ctrlKey
181                 && !event.sticky_modifiers;
182         } catch (e) { return false; }
183     });
185 define_key_match_predicate('match_checkbox_keys', 'checkbox keys',
186     function (event) {
187         return event.keyCode == 32
188             && !event.shiftKey
189             && !event.metaKey
190             && !event.altKey
191             && !event.ctrlKey;
192         //XXX: keycode fallthroughs don't support sticky modifiers
193     });
195 define_key_match_predicate('match_text_keys', 'text editing keys',
196     function (event) {
197         return ((event.type == 'keypress' && event.charCode)
198                 || event.keyCode == 13 || event.keyCode > 31)
199             && !event.ctrlKey
200             && !event.metaKey
201             && !modifiers.A.in_event_p(event);
202         //XXX: keycode fallthroughs don't support sticky modifiers
203     });
207  */
209 function format_key_spec (key) {
210     if (key instanceof Function) {
211         if (key.description)
212             return "<"+key.description+">";
213         if (key.name)
214             return "<"+key.name+">";
215         return "<anonymous match function>";
216     }
217     return key;
220 function format_binding_sequence (seq) {
221     return seq.map(function (x) {
222             return format_key_spec(x.key);
223         }).join(" ");
227 function keymap_lookup (keymaps, combo, event) {
228     var i = keymaps.length - 1;
229     var kmap = keymaps[i];
230     var new_kmaps;
231     while (true) {
232         var bindings = kmap.bindings;
233         var bind = bindings[combo];
234         if (new_kmaps) {
235             if (bind) {
236                 if (bind.keymap)
237                     new_kmaps.unshift(bind.keymap);
238                 else
239                     return new_kmaps;
240             }
241         } else if (bind) {
242             if (bind.keymap)
243                 new_kmaps = [bind.keymap];
244             else
245                 return bind;
246         } else {
247             var pred_binds = kmap.predicate_bindings;
248             for (var j = 0, pblen = pred_binds.length; j < pblen; ++j) {
249                 bind = pred_binds[j];
250                 if (bind.key(event))
251                     return bind;
252             }
253         }
254         if (kmap.parent)
255             kmap = kmap.parent;
256         else if (i > 0)
257             kmap = keymaps[--i];
258         else if (new_kmaps)
259             return new_kmaps
260         else
261             return null;
262     }
266 function keymap_lookup_fallthrough (keymap, event) {
267     var predicates = keymap.fallthrough;
268     for (var i = 0, plen = predicates.length; i < plen; ++i) {
269         if (predicates[i](event))
270             return true;
271     }
272     return false;
276 function for_each_key_binding (keymaps, callback) {
277     var binding_sequence = [];
278     function helper (keymap) {
279         var binding;
280         for each (binding in keymap.bindings) {
281             binding_sequence.push(binding);
282             callback(binding_sequence);
283             if (binding.keymap)
284                 helper(binding.keymap);
285             binding_sequence.pop();
286         }
287         for each (binding in keymap.predicate_bindings) {
288             binding_sequence.push(binding);
289             callback(binding_sequence);
290             if (binding.keymap)
291                 helper(binding.keymap);
292             binding_sequence.pop();
293         }
294     }
295     //outer loop is to go down the parent-chain of keymaps
296     var i = keymaps.length - 1;
297     var keymap = keymaps[i];
298     while (true) {
299         helper(keymap);
300         if (keymap.parent)
301             keymap = keymap.parent;
302         else if (i > 0)
303             keymap = keymaps[--i];
304         else
305             break;
306     }
310 function keymap_lookup_command (keymaps, command) {
311     var list = [];
312     for_each_key_binding(keymaps, function (bind_seq) {
313             var bind = bind_seq[bind_seq.length - 1];
314             if (bind.command == command)
315                 list.push(format_binding_sequence(bind_seq));
316         });
317     return list;
326  * $fallthrough, $repeat and $browser_object are as for define_key.
328  * ref is the source code reference of the call to define_key.
330  * kmap is the keymap in which the binding is to be defined.
332  * seq is the key sequence being bound.  it may be necessary
333  * to auto-generate new keymaps to accomodate the key sequence.
335  * only one of new_command and new_keymap will be given.
336  * the one that is given is the thing being bound to.
337  */
338 define_keywords("$fallthrough", "$repeat", "$browser_object");
339 function define_key_internal (ref, kmap, seq, new_command, new_keymap) {
340     keywords(arguments);
341     var args = arguments;
342     var last_in_sequence; // flag to indicate the final key combo in the sequence.
343     var key; // current key combo as we iterate through the sequence.
344     var undefine_key = (new_command == null) &&
345         (new_keymap == null) &&
346         (! args.$fallthrough);
348     /* Replace `bind' with the binding specified by (cmd, fallthrough) */
349     function replace_binding (bind) {
350         if (last_in_sequence) {
351             bind.command = new_command;
352             bind.keymap = new_keymap;
353             bind.fallthrough = args.$fallthrough;
354             bind.source_code_reference = ref;
355             bind.repeat = args.$repeat;
356             bind.browser_object = args.$browser_object;
357         } else {
358             if (!bind.keymap)
359                 throw new Error("Key sequence has a non-keymap in prefix");
360             kmap = bind.keymap;
361         }
362     }
364     function make_binding () {
365         if (last_in_sequence) {
366             return { key: key,
367                      fallthrough: args.$fallthrough,
368                      command: new_command,
369                      keymap: new_keymap,
370                      source_code_reference: ref,
371                      repeat: args.$repeat,
372                      browser_object: args.$browser_object,
373                      bound_in: kmap };
374         } else {
375             let old_kmap = kmap;
376             kmap = new keymap($anonymous,
377                               $name = old_kmap.name + " " + format_key_spec(key));
378             kmap.bound_in = old_kmap;
379             return { key: key,
380                      keymap: kmap,
381                      source_code_reference: ref,
382                      bound_in: old_kmap };
383         }
384     }
386 sequence:
387     for (var i = 0, slen = seq.length; i < slen; ++i) {
388         key = seq[i];
389         last_in_sequence = (i == slen - 1);
391         if (typeof key == "function") { // it's a match predicate
392             // Check if the binding is already present in the keymap
393             var pred_binds = kmap.predicate_bindings;
394             for (var j = 0, pblen = pred_binds.length; j < pblen; j++) {
395                 if (pred_binds[j].key == key) {
396                     if (last_in_sequence && undefine_key)
397                         delete pred_binds[j];
398                     else
399                         replace_binding(pred_binds[j]);
400                     continue sequence;
401                 }
402             }
403             pred_binds.push(make_binding());
404         } else {
405             // Check if the binding is already present in the keymap
406             var bindings = kmap.bindings;
407             var binding = bindings[key];
408             if (binding) {
409                 if (last_in_sequence && undefine_key)
410                     delete bindings[key];
411                 else
412                     replace_binding(binding);
413                 continue sequence;
414             }
415             if (! undefine_key)
416                 bindings[key] = make_binding();
417         }
418     }
422  * bind SEQ to a keymap or command CMD in keymap KMAP.
424  *   If CMD is the special value `fallthrough', it will be bound as a
425  * fallthrough key.
427  * keywords:
429  *  $fallthrough: specifies that the keypress event will fall through
430  *      to gecko.  Note, by this method, only keypress will fall through.
431  *      Keyup and keydown will still be blocked.
433  *  $repeat: (command name) shortcut command to call if a prefix
434  *      command key is pressed twice in a row.
436  *  $browser_object: (browser object) Override the default
437  *      browser-object for the command.
438  */
439 define_keywords("$fallthrough", "$repeat", "$browser_object");
440 function define_key (kmap, seq, cmd) {
441     keywords(arguments);
442     var orig_seq = seq;
443     try {
444         var ref = get_caller_source_code_reference();
446         if (typeof seq == "string" && seq.length > 1)
447             seq = seq.split(" ");
449         if (!(typeof seq == "object") || !(seq instanceof Array))
450             seq = [seq];
452         // normalize the order of modifiers in key combos
453         seq = seq.map(
454             function (k) {
455                 if (typeof(k) == "string")
456                     return format_key_combo(unformat_key_combo(k));
457                 else
458                     return k;
459             });
461         var new_command = null;
462         var new_keymap = null;
463         if (typeof cmd == "string" || typeof cmd == "function")
464             new_command = cmd;
465         else if (cmd instanceof keymap)
466             new_keymap = cmd;
467         else if (cmd != null)
468             throw new Error("Invalid `cmd' argument: " + cmd);
470         define_key_internal(ref, kmap, seq, new_command, new_keymap,
471                             forward_keywords(arguments));
473     } catch (e if (typeof e == "string")) {
474         dumpln("Warning: Error occurred while binding sequence: " + orig_seq);
475         dumpln(e);
476     }
480 function undefine_key (kmap, seq) {
481     define_key(kmap, seq);
486  * define_fallthrough
488  *   Takes a keymap and a predicate on an event.  Fallthroughs defined by
489  * these means will cause all three of keydown, keypress, and keyup to
490  * fall through to gecko, whereas those defined by the $fallthrough
491  * keyword to define_key only affect keypress events.
493  *   The limitations of this method are that only the keyCode is available
494  * to the predicate, not the charCode, and keymap inheritance is not
495  * available for these "bindings".
496  */
497 function define_fallthrough (keymap, predicate) {
498     keymap.fallthrough.push(predicate);
503  * Minibuffer Read Key Binding
504  */
506 define_keymap("key_binding_reader_keymap");
507 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
509 define_keywords("$buffer", "$keymap");
510 function key_binding_reader (window, continuation) {
511     keywords(arguments, $prompt = "Describe key:");
513     this.continuation = continuation;
515     if (arguments.$keymap)
516         this.target_keymap = arguments.$keymap;
517     else {
518         var buffer = arguments.$buffer;
519         this.target_keymap = get_current_keymaps(window);
520     }
522     this.key_sequence = [];
524     minibuffer_input_state.call(this, window, key_binding_reader_keymap, arguments.$prompt);
526 key_binding_reader.prototype = {
527     __proto__: minibuffer_input_state.prototype,
528     destroy: function (window) {
529         if (this.continuation)
530             this.continuation.throw(abort());
531         minibuffer_input_state.prototype.destroy.call(this, window);
532     }
535 function invalid_key_binding (seq) {
536     var e = new Error(seq.join(" ") + " is undefined");
537     e.key_sequence = seq;
538     e.__proto__ = invalid_key_binding.prototype;
539     return e;
541 invalid_key_binding.prototype = {
542     __proto__: interactive_error.prototype
545 function read_key_binding_key (window, state, event) {
546     var combo = format_key_combo(event);
547     var binding = keymap_lookup(state.target_keymap, combo, event);
549     state.key_sequence.push(combo);
551     if (binding == null) {
552         var c = state.continuation;
553         delete state.continuation;
554         window.minibuffer.pop_state();
555         c.throw(invalid_key_binding(state.key_sequence));
556         return;
557     }
559     if (binding.constructor == Array) { //keymaps stack
560         window.minibuffer._restore_normal_state();
561         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
562         state.target_keymap = binding;
563         return;
564     }
566     var c = state.continuation;
567     delete state.continuation;
569     window.minibuffer.pop_state();
571     if (c != null)
572         c([state.key_sequence, binding]);
574 interactive("read-key-binding-key",
575     "Handle a keystroke in the key binding reader minibuffer state.",
576      function (I) {
577          read_key_binding_key(I.window,
578                               I.minibuffer.check_state(key_binding_reader),
579                               I.event);
580      });
582 minibuffer.prototype.read_key_binding = function () {
583     keywords(arguments);
584     var s = new key_binding_reader(this.window, (yield CONTINUATION), forward_keywords(arguments));
585     this.push_state(s);
586     var result = yield SUSPEND;
587     yield co_return(result);