Improved describe-bindings display
[conkeror.git] / modules / keyboard.js
blob4bfeff5b3f4f08f242bcb569aea2d63783d3cf7e
1 require("window.js");
3 var keycode_to_name = [];
4 var name_to_keycode = new Object();
5 var shifted_keycode_to_name = [];
6 var name_to_shifted_keycode = [];
8 /* Generate keyCode to string  and string to keyCode mapping tables.  */
9 function generate_key_tables()
11     var KeyEvent = Ci.nsIDOMKeyEvent;
12     function map(code, name) {
13         keycode_to_name[code] = name;
14         name_to_keycode[name] = code;
15     }
17     var prefix = "DOM_VK_";
18     for (i in KeyEvent)
19     {
20         /* Check if this is a key binding */
21         if (i.substr(0, prefix.length) == prefix)
22         {
23             var name = i.substr(prefix.length).toLowerCase();
24             var code = KeyEvent[i];
25             keycode_to_name[code] = name;
26             name_to_keycode[name] = code;
27         }
28     }
30     /* Add additional mappings for improved display */
31     map(KeyEvent.DOM_VK_BACK_SLASH, "\\");
32     map(KeyEvent.DOM_VK_OPEN_BRACKET, "[");
33     map(KeyEvent.DOM_VK_CLOSE_BRACKET, "]");
34     map(KeyEvent.DOM_VK_SEMICOLON, ";");
35     map(KeyEvent.DOM_VK_COMMA, ",");
36     map(KeyEvent.DOM_VK_PERIOD, ".");
37     map(KeyEvent.DOM_VK_SLASH, "/");
38     map(KeyEvent.DOM_VK_SUBTRACT, "-");
39     map(KeyEvent.DOM_VK_PAGE_DOWN, "pgdn");
40     map(KeyEvent.DOM_VK_PAGE_UP, "pgup");
41     map(KeyEvent.DOM_VK_EQUALS, "=");
44     /* Add additional shifted keycodes for improved display */
45     function smap(code, name) {
46         shifted_keycode_to_name[code] = name;
47         name_to_shifted_keycode[name] = code;
48     }
50     for (var i = 0; i < 26; ++i) {
51         var code = KeyEvent.DOM_VK_A + i;
52         var name = String.fromCharCode(i + "A".charCodeAt(0));
53         smap(code, name);
54     }
55     smap(KeyEvent.DOM_VK_SEMICOLON, ":");
56     smap(KeyEvent.DOM_VK_1, "!");
57     smap(KeyEvent.DOM_VK_2, "@");
58     smap(KeyEvent.DOM_VK_3, "#");
59     smap(KeyEvent.DOM_VK_4, "$");
60     smap(KeyEvent.DOM_VK_5, "%");
61     smap(KeyEvent.DOM_VK_6, "^");
62     smap(KeyEvent.DOM_VK_7, "&");
63     smap(KeyEvent.DOM_VK_8, "*");
64     smap(KeyEvent.DOM_VK_9, "(");
65     smap(KeyEvent.DOM_VK_0, ")");
66     smap(KeyEvent.DOM_VK_EQUALS, "+");
67     smap(KeyEvent.DOM_VK_SUBTRACT, "_");
68     smap(KeyEvent.DOM_VK_COMMA, "<");
69     smap(KeyEvent.DOM_VK_PERIOD, ">");
70     smap(KeyEvent.DOM_VK_SLASH, "?");
74 generate_key_tables();
76 var abort_key = null;
78 const MOD_CTRL = 0x1;
79 const MOD_META = 0x2;
80 const MOD_SHIFT = 0x4;
82 // Note: For elements of the modifier_names array, an element at index
83 // i should correspond to the modifier mask (1 << i).
84 var modifier_names = ["C", "M", "S"];
86 function format_key_press(code, modifiers)
88     if (code == 0)
89         return "<invalid>";
90     var name;
91     if ((modifiers & MOD_SHIFT) && (code in shifted_keycode_to_name))  {
92         name = shifted_keycode_to_name[code];
93         modifiers &= ~MOD_SHIFT;
94     } else
95         name = keycode_to_name[code];
96     if (!name)
97         name = String.fromCharCode(code);
98     var out = "";
99     if (modifiers)
100     {
101         for (var i = 0; i < modifier_names.length; ++i)
102         {
103             if (modifiers & (1 << i))
104                 out = out + modifier_names[i] + "-";
105         }
106     }
107     out = out + name;
108     return out;
111 function format_key_spec(key) {
112     if (key.match_function) {
113         if (key.match_function == match_any_key)
114             return "<any-key>";
115         if (key.match_function == match_any_unmodified_key)
116             return "<any-unmodified-key>";
117         return "<match-function>";
118     }
119     return format_key_press(key.keyCode, key.modifiers);
122 function format_key_event(event)
124     return format_key_press(event.keyCode, get_modifiers(event));
127 function format_binding_sequence(seq) {
128     return seq.map(function (x) {
129             return format_key_spec(x.key);
130         }).join(" ");
133 // Key Matching Functions.  These are functions that may be passed to kbd
134 // in place of key code or char code.  They take an event object as their
135 // argument and turn true if the event matches the class of keys that they
136 // represent.
138 function match_any_key (event)
140     return true;
143 function meta_pressed (event)
145     return event.altKey || event.metaKey;
148 function get_modifiers(event)
150     // Shift is always included in the modifiers, if it is included in
151     // the event.
152     return (event.ctrlKey ? MOD_CTRL:0) |
153         (meta_pressed(event) ? MOD_META:0) |
154         (event.shiftKey ? MOD_SHIFT: 0) |
155         event.sticky_modifiers;
158 /* This function is no longer used for normal keymap lookups.  It is
159  * only used to check if the current key matches the abort key. */
160 function match_binding(key, event)
162     return (key.keyCode
163             && event.keyCode == key.keyCode
164             && get_modifiers(event) == key.modifiers)
165         || (key.match_function && key.match_function (event));
168 function lookup_key_binding(kmap, event)
170     do {
171         // Check if the key matches the keycode table
172         var mods = get_modifiers(event);
173         var keycode_binds = kmap.keycode_bindings;
174         var arr;;
175         var bind;
176         if ((arr = keycode_binds[event.keyCode]) != null &&
177             (bind = arr[mods]) != null)
178             return bind;
180         // Check if the key matches a predicate
181         var pred_binds = kmap.predicate_bindings;
182         for (var i = 0; i < pred_binds.length; ++i)
183         {
184             var bind = pred_binds[i];
185             if (bind.key.match_function(event))
186                 return bind;
187         }
188         kmap = kmap.parent;
189     } while (kmap);
190     return null;
193 function match_any_unmodified_key (event)
195     try {
196         return event.charCode
197             && !meta_pressed(event)
198             && !event.ctrlKey;
199     } catch (e) {return false; }
202 function kbd (spec, mods)
204     if (typeof(spec) == "object")
205         return spec;
206     var key = {};
207     if (typeof spec == "function")
208         key.match_function = spec;
209     else if (typeof spec == "string")
210     {
211         /* Attempt to parse a key specification.  In order to allow
212          * the user to specify the "-" key literally, special case the
213          * parsing of that. */
214         var parts;
215         if (spec.substr(spec.length - 1) == "-")
216         {
217             parts = spec.substr(0, spec.length - 1).split("-");
218             parts.push("-");
219         } else
220             parts = spec.split("-");
221         var parsed_modifiers = 0;
222         if (parts.length > 1)
223         {
224             // Attempt to parse modifiers
225             for (var i = 0; i < parts.length - 1; ++i)
226             {
227                 var k = modifier_names.indexOf(parts[i]);
228                 if (k < 0)
229                     continue;
230                 var mod = 1 << k;
231                 parsed_modifiers |= mod;
232             }
233         }
234         // Attempt to lookup keycode
235         var name = parts[parts.length - 1];
236         var code = null;
237         if (name in name_to_keycode)
238             code = name_to_keycode[name];
239         else if (name in name_to_shifted_keycode) {
240             code = name_to_shifted_keycode[name];
241             parsed_modifiers |= MOD_SHIFT;
242         } else
243             throw "Invalid key specification: " + spec;
244         key.keyCode = code;
245         if (mods)
246             parsed_modifiers |= mods;
247         key.modifiers = parsed_modifiers;
248     }
249     else {
250         key.keyCode = spec;
251         if (mods)
252             key.modifiers = mods;
253         else
254             key.modifiers = 0;
255     }
257     return key;
260 // bind key to either the keymap or command in the keymap, kmap
261 define_keywords("$fallthrough", "$hook", "$category");
262 function define_key(kmap, keys, cmd)
264     keywords(arguments);
266     var ref = get_caller_source_code_reference();
268     var args = arguments;
269     
270     if (typeof(keys) == "string" && keys.length > 1)
271         keys = keys.split(" ");
272     if (!(keys instanceof Array))
273         keys = [keys];
275     var new_command = null, new_keymap = null;
276     if (typeof(cmd) == "string" || typeof(cmd) == "function")
277         new_command = cmd;
278     else if (cmd instanceof keymap)
279         new_keymap = cmd;
280     else if (cmd != null)
281         throw new Error("Invalid `cmd' argument: " + cmd);
283     var parent_kmap = kmap.parent;
285 outer:
286     for (var i = 0; i < keys.length; ++i) {
287         var key = keys[i];
288         var final_binding = (i == keys.length - 1);
290         if (typeof key == "string")
291             key = kbd(key);
292         else if (typeof key == "function")
293             key = kbd(key);
295         /* Replace `bind' with the binding specified by (cmd, fallthrough) */
296         function replace_binding(bind)
297         {
298             if (final_binding) {
299                 bind.command = new_command;
300                 bind.keymap = new_keymap;
301                 bind.fallthrough = args.$fallthrough;
302                 bind.hook = args.$hook;
303                 bind.source_code_reference = ref;
304                 bind.category = args.$category;
305             } else {
306                 if (!bind.keymap)
307                     throw new Error("Key sequence has a non-keymap in prefix");
308                 kmap = bind.keymap;
309             }
310         }
312         function make_binding()
313         {
314             if (final_binding) {
315                 return {key: key, fallthrough: args.$fallthrough, hook: args.$hook,
316                         command: new_command, keymap: new_keymap,
317                         source_code_reference: ref,
318                         category: args.$category,
319                         bound_in: kmap};
320             }
321             else
322             {
323                 let old_kmap = kmap;
324                 // Check for a corresponding binding a parent
325                 kmap = new keymap($parent = parent_kmap);
326                 kmap.bound_in = old_kmap;
327                 return {key: key, keymap: kmap,
328                         source_code_reference: ref,
329                         bound_in: old_kmap};
330             }
331         }
333         // Check if the specified binding is already present in the kmap
334         if (key.match_function)
335         {
336             var pred_binds = kmap.predicate_bindings;
337             for (var i = 0; i < pred_binds.length; i++)
338             {
339                 var cur_bind = pred_binds[i];
340                 if (cur_bind.key.match_function == key.match_function)
341                 {
342                     replace_binding(cur_bind);
343                     continue outer;
344                 }
345             }
347             if (!final_binding && parent_kmap) {
348                 var parent_pred_binds = parent_kmap.predicate_bindings;
349                 parent_kmap = null;
350                 for (var i = 0; i < parent_pred_binds.length; i++)
351                 {
352                     var cur_bind = parent_pred_binds[i];
353                     if (cur_bind.key.match_function == key.match_function && cur_bind.keymap)
354                     {
355                         parent_kmap = cur_bind.keymap;
356                         break;
357                     }
358                 }
359             }
360             // Not already present, must be added
361             pred_binds.push(make_binding());
362         } else
363         {
364             // This is a binding by keycode: look it up in the table
365             var keycode_binds = kmap.keycode_bindings;
366             var arr = keycode_binds[key.keyCode];
368             if (arr && arr[key.modifiers])
369             {
370                 replace_binding(arr[key.modifiers]);
371                 continue outer;
372             }
374             if (!final_binding && parent_kmap) {
375                 var p_keycode_binds = parent_kmap.keycode_bindings;
376                 parent_kmap = null;
377                 var p_arr = p_keycode_binds[key.keyCode];
378                 var p_bind;
379                 if (p_arr && (p_bind = p_arr[key.modifiers]) != null && p_bind.keymap)
380                     parent_kmap = p_bind.keymap;
381             }
383             if (!arr)
384                 arr = (keycode_binds[key.keyCode] = []);
386             arr[key.modifiers] = make_binding();
387         }
388     }
391 define_keywords("$parent", "$help", "$name");
392 function keymap ()
394     keywords(arguments);
395     /* For efficiency, a table indexed by the key code, and then by
396      * the modifiers is used to lookup key bindings, rather than
397      * looping through all bindings in the key map to find one.  The
398      * array keycode_bindings is indexed by the keyCode; if the
399      * corresponding element for a keyCode is non-null, it is itself
400      * an array indexed by the result of get_modifiers (i.e. from 0 to 7).
401      * As before, match_function-based bindings are stored as a simple
402      * list, predicate_bindings. */
403     this.parent = arguments.$parent;
404     this.keycode_bindings = [];
405     this.predicate_bindings = [];
406     this.help = arguments.$help;
407     this.name = arguments.$name;
410 function define_keymap(name) {
411     this[name] = new keymap($name = name, forward_keywords(arguments));
414 function copy_event(event)
416     var ev = {};
417     ev.keyCode = event.keyCode;
418     ev.charCode = event.charCode;
419     ev.ctrlKey = event.ctrlKey;
420     ev.metaKey = event.metaKey;
421     ev.altKey = event.altKey;
422     ev.shiftKey = event.shiftKey;
423     ev.sticky_modifiers = event.sticky_modifiers;
424     return ev;
427 function key_down_handler(event)
429     var window = this;
430     //window.dumpln("key down: " + conkeror.format_key_press(event.keyCode, conkeror.get_modifiers(event)));
432     var state = window.keyboard;
433     state.last_key_down_event = copy_event(event);
434     state.last_char_code = null;
435     state.last_key_code = null;
438 define_variable("keyboard_key_sequence_help_timeout", 0,
439                 "Delay (in millseconds) before the current key sequence prefix is displayed in the minibuffer.");
441 define_window_local_hook("key_press_hook", RUN_HOOK_UNTIL_SUCCESS);
443 function key_press_handler(true_event)
445     try{
446         var window = this;
447         var state = window.keyboard;
449         /* ASSERT(state.last_key_down_event != null); */
451         var event = state.last_key_down_event;
452         event.charCode = true_event.charCode;
454         // If the true_event includes a keyCode, we can just use that
455         if (true_event.keyCode)
456             event.keyCode = true_event.keyCode;
458         /* Filter out events from keys like the Windows/Super/Hyper key */
459         if (event.keyCode == 0)
460             return;
462         /* Clear minibuffer message */
463         window.minibuffer.clear();    
465         var binding = null;
466         var done = true;
468         var ctx;
469         if (!state.current_context)
470             ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
471         else
472             ctx = state.current_context;
474         event.sticky_modifiers = ctx.sticky_modifiers;
475         ctx.sticky_modifiers = 0;
477         ctx.event = event;
479         if (key_press_hook.run(window, ctx, true_event))
480             return;
482         var active_keymap =
483             state.active_keymap ||
484             state.override_keymap ||
485             window.buffers.current.keymap;
486         var overlay_keymap = ctx.overlay_keymap;
488         binding =
489             lookup_key_binding(active_keymap, event) ||
490             (overlay_keymap && lookup_key_binding(overlay_keymap, event));
492         ctx.overlay_keymap = null;
494         // Should we stop this event from being processed by the gui?
495         //
496         // 1) we have a binding, and the binding's fallthrough property is not
497         //    true.
498         //
499         // 2) we are in the middle of a key sequence, and we need to say that
500         //    the key sequence given has no command.
501         //
502         if (!binding || !binding.fallthrough)
503         {
504             true_event.preventDefault();
505             true_event.stopPropagation();
506         }
508         // Finally, process the binding.
509         ctx.key_sequence.push(format_key_event(event));
510         if (binding) {
511             if (binding.keymap) {
512                 if (binding.hook)
513                     binding.hook.call(null, ctx, active_keymap, overlay_keymap);
514                 state.active_keymap = binding.keymap;
515                 if (!state.help_displayed)
516                 {
517                     state.help_timer_ID = window.setTimeout(function () {
518                             window.minibuffer.show(ctx.key_sequence.join(" "));
519                             state.help_displayed = true;
520                             state.help_timer_ID = null;
521                         }, keyboard_key_sequence_help_timeout);
522                 }
523                 else
524                     window.minibuffer.show(ctx.key_sequence.join(" "));
526                 // We're going for another round
527                 done = false;
528             } else if (binding.command) {
529                 call_interactively(ctx, binding.command);
530             }
531         } else {
532             window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
533         }
535         // Clean up if we're done
536         if (done)
537         {
538             if (state.help_timer_ID != null)
539             {
540                 window.clearTimeout(state.help_timer_ID);
541                 state.help_timer_ID = null;
542             }
543             state.help_displayed = false;
544             state.active_keymap = null;
545             state.current_context = null;
546         }
547     } catch(e) { dump_error(e);}
550 function keyboard(window)
552     this.window = window;
555 keyboard.prototype = {
556     last_key_down_event : null,
557     current_context : null,
558     active_keymap : null,
559     help_timer_ID : null,
560     help_displayed : false,
562     /* If this is non-null, it is used instead of the current buffer's
563      * keymap. */
564     override_keymap : null,
566     set_override_keymap : function (keymap) {
567         /* Clear out any in-progress key sequence. */
568         this.active_keymap = null;
569         this.current_context = null;
570         if (this.help_timer_ID != null)
571         {
572             this.window.clearTimeout(this.help_timer_ID);
573             this.help_timer_ID = null;
574         }
575         this.override_keymap = keymap;
576     }
580 function keyboard_initialize_window(window)
582     window.keyboard = new keyboard(window);
584     window.addEventListener ("keydown", key_down_handler, true /* capture */,
585                             false /* ignore untrusted events */);
586     window.addEventListener ("keypress", key_press_handler, true /* capture */,
587                             false /* ignore untrusted events */);
590 add_hook("window_initialize_hook", keyboard_initialize_window);
592 function for_each_key_binding(keymap_or_buffer, callback) {
593     var keymap;
594     if (keymap_or_buffer instanceof conkeror.buffer) {
595         var buffer = keymap_or_buffer;
596         var window = buffer.window;
597         keymap = window.keyboard.override_keymap || buffer.keymap;
598     } else {
599         keymap = keymap_or_buffer;
600     }
601     var keymap_stack = [keymap];
602     var binding_stack = [];
603     function helper2(bind) {
604         binding_stack.push(bind);
605         callback(binding_stack);
606         if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
607             keymap_stack.push(bind.keymap);
608             helper();
609             keymap_stack.pop();
610         }
611         binding_stack.pop();
612     }
613     function helper() {
614         var unmodified_keys_masked = false;
615         var keycode_masks = [];
616         while (true) {
617             var keymap = keymap_stack[keymap_stack.length - 1];
618             for (var i in keymap.keycode_bindings) {
619                 var b = keymap.keycode_bindings[i];
620                 if (!(i in keycode_masks))
621                     keycode_masks[i] = [];
622                 for (var j in b) {
623                     if (unmodified_keys_masked && ((j & MOD_SHIFT) == j))
624                         continue;
625                     if (!keycode_masks[i][j]) {
626                         helper2(b[j]);
627                         keycode_masks[i][j] = true;
628                     }
629                 }
630             }
631             for (var i in  keymap.predicate_bindings) {
632                 var bind = keymap.predicate_bindings[i];
633                 helper2(bind);
634                 var p = bind.key.match_function;
635                 if (p == match_any_key)
636                     return;
637                 if (p == match_any_unmodified_key)
638                     unmodified_keys_masked = true;
639             }
640             if (keymap.parent)
641                 keymap_stack[keymap_stack.length - 1] = keymap.parent;
642             else
643                 break;
644         }
645     }
646     helper();
649 function find_command_in_keymap(keymap_or_buffer, command) {
650     var list = [];
652     for_each_key_binding(keymap_or_buffer, function (bind_seq) {
653             var bind = bind_seq[bind_seq.length - 1];
654             if (bind.command == command)
655                 list.push(format_binding_sequence(bind_seq));
656         });
657     return list;
660 define_keymap("key_binding_reader_keymap");
661 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
663 define_keywords("$buffer", "$keymap");
664 function key_binding_reader(continuation) {
665     keywords(arguments, $prompt = "Describe key:");
667     this.continuation = continuation;
669     if (arguments.$keymap)
670         this.target_keymap = arguments.$keymap;
671     else {
672         var buffer = arguments.$buffer;
673         var window = buffer.window;
674         this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
675     }
677     this.key_sequence = [];
678     
679     minibuffer_state.call(this, key_binding_reader_keymap, arguments.$prompt);
681 key_binding_reader.prototype = {
682     __proto__: minibuffer_state.prototype,
683     destroy: function () {
684         if (this.continuation)
685             this.continuation.throw(abort());
686     }
689 function invalid_key_binding(seq) {
690     var e = new Error(seq.join(" ") + " is undefined");
691     e.key_sequence = seq;
692     e.__proto__ = invalid_key_binding.prototype;
693     return e;
695 invalid_key_binding.prototype = {
696     __proto__: interactive_error.prototype
699 function read_key_binding_key(window, state, event) {
700     var binding = lookup_key_binding(state.target_keymap, event);
702     state.key_sequence.push(format_key_event(event));
704     if (binding == null) {
705         var c = state.continuation;
706         delete state.continuation;
707         window.minibuffer.pop_state();
708         c.throw(invalid_key_binding(state.key_sequence));
709         return;
710     }
712     if (binding.keymap) {
713         window.minibuffer._ensure_input_area_showing();
714         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
715         state.target_keymap = binding.keymap;
716         return;
717     }
719     var c = state.continuation;
720     delete state.continuation;
722     window.minibuffer.pop_state();
724     if (c != null)
725         c([state.key_sequence, binding]);
727 interactive("read-key-binding-key", function (I) {
728     read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
731 minibuffer.prototype.read_key_binding = function () {
732     keywords(arguments);
733     var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
734     this.push_state(s);
735     var result = yield SUSPEND;
736     yield co_return(result);