Add support for documenting user variables.
[conkeror.git] / modules / keyboard.js
blob9086d01a5837b4983cab2126f8fca467dfec3d6b
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);
157 /* This function is no longer used for normal keymap lookups.  It is
158  * only used to check if the current key matches the abort key. */
159 function match_binding(key, event)
161     return (key.keyCode
162             && event.keyCode == key.keyCode
163             && get_modifiers(event) == key.modifiers)
164         || (key.match_function && key.match_function (event));
167 function lookup_key_binding(kmap, event)
169     do {
170         // Check if the key matches the keycode table
171         var mods = get_modifiers(event);
172         var keycode_binds = kmap.keycode_bindings;
173         var arr;;
174         var bind;
175         if ((arr = keycode_binds[event.keyCode]) != null &&
176             (bind = arr[mods]) != null)
177             return bind;
179         // Check if the key matches a predicate
180         var pred_binds = kmap.predicate_bindings;
181         for (var i = 0; i < pred_binds.length; ++i)
182         {
183             var bind = pred_binds[i];
184             if (bind.key.match_function(event))
185                 return bind;
186         }
187         kmap = kmap.parent;
188     } while (kmap);
189     return null;
192 function match_any_unmodified_key (event)
194     try {
195         return event.charCode
196             && !meta_pressed(event)
197             && !event.ctrlKey;
198     } catch (e) {return false; }
201 function kbd (spec, mods)
203     var key = {};
204     if (typeof spec == "function")
205         key.match_function = spec;
206     else if (typeof spec == "string")
207     {
208         /* Attempt to parse a key specification.  In order to allow
209          * the user to specify the "-" key literally, special case the
210          * parsing of that. */
211         var parts;
212         if (spec.substr(spec.length - 1) == "-")
213         {
214             parts = spec.substr(0, spec.length - 1).split("-");
215             parts.push("-");
216         } else
217             parts = spec.split("-");
218         var parsed_modifiers = 0;
219         if (parts.length > 1)
220         {
221             // Attempt to parse modifiers
222             for (var i = 0; i < parts.length - 1; ++i)
223             {
224                 var k = modifier_names.indexOf(parts[i]);
225                 if (k < 0)
226                     continue;
227                 var mod = 1 << k;
228                 parsed_modifiers |= mod;
229             }
230         }
231         // Attempt to lookup keycode
232         var name = parts[parts.length - 1];
233         var code = null;
234         if (name in name_to_keycode)
235             code = name_to_keycode[name];
236         else if (name in name_to_shifted_keycode) {
237             code = name_to_shifted_keycode[name];
238             parsed_modifiers |= MOD_SHIFT;
239         } else
240             throw "Invalid key specification: " + spec;
241         key.keyCode = code;
242         if (mods)
243             parsed_modifiers |= mods;
244         key.modifiers = parsed_modifiers;
245     }
246     else {
247         key.keyCode = spec;
248         if (mods)
249             key.modifiers = mods;
250         else
251             key.modifiers = 0;
252     }
254     return key;
257 // bind key to either the keymap or command in the keymap, kmap
258 define_keywords("$fallthrough", "$hook");
259 function define_key(kmap, keys, cmd)
261     keywords(arguments);
263     var ref = get_caller_source_code_reference();
265     var args = arguments;
266     
267     if (typeof(keys) == "string" && keys.length > 1)
268         keys = keys.split(" ");
269     if (!(keys instanceof Array))
270         keys = [keys];
272     var new_command = null, new_keymap = null;
273     if (typeof(cmd) == "string")
274         new_command = cmd;
275     else if (cmd instanceof keymap)
276         new_keymap = cmd;
277     else if (cmd != null)
278         throw new Error("Invalid `cmd' argument: " + cmd);
280     var parent_kmap = kmap.parent;
282 outer:
283     for (var i = 0; i < keys.length; ++i) {
284         var key = keys[i];
285         var final_binding = (i == keys.length - 1);
287         if (typeof key == "string")
288             key = kbd(key);
289         else if (typeof key == "function")
290             key = kbd(key);
292         /* Replace `bind' with the binding specified by (cmd, fallthrough) */
293         function replace_binding(bind)
294         {
295             if (final_binding) {
296                 bind.command = new_command;
297                 bind.keymap = new_keymap;
298                 bind.fallthrough = args.$fallthrough;
299                 bind.hook = args.$hook;
300                 bind.source_code_reference = ref;
301             } else {
302                 if (!bind.keymap)
303                     throw new Error("Key sequence has a non-keymap in prefix");
304                 kmap = bind.keymap;
305             }
306         }
308         function make_binding()
309         {
310             if (final_binding) {
311                 return {key: key, fallthrough: args.$fallthrough, hook: args.$hook,
312                         command: new_command, keymap: new_keymap,
313                         source_code_reference: ref};
314             }
315             else
316             {
317                 // Check for a corresponding binding a parent
318                 kmap = new keymap($parent = parent_kmap);
319                 return {key: key, keymap: kmap,
320                         source_code_reference: ref};
321             }
322         }
324         // Check if the specified binding is already present in the kmap
325         if (key.match_function)
326         {
327             var pred_binds = kmap.predicate_bindings;
328             for (var i = 0; i < pred_binds.length; i++)
329             {
330                 var cur_bind = pred_binds[i];
331                 if (cur_bind.key.match_function == key.match_function)
332                 {
333                     replace_binding(cur_bind);
334                     continue outer;
335                 }
336             }
338             if (!final_binding && parent_kmap) {
339                 var parent_pred_binds = parent_kmap.predicate_bindings;
340                 parent_kmap = null;
341                 for (var i = 0; i < parent_pred_binds.length; i++)
342                 {
343                     var cur_bind = parent_pred_binds[i];
344                     if (cur_bind.key.match_function == key.match_function && cur_bind.keymap)
345                     {
346                         parent_kmap = cur_bind.keymap;
347                         break;
348                     }
349                 }
350             }
351             // Not already present, must be added
352             pred_binds.push(make_binding());
353         } else
354         {
355             // This is a binding by keycode: look it up in the table
356             var keycode_binds = kmap.keycode_bindings;
357             var arr = keycode_binds[key.keyCode];
359             if (arr && arr[key.modifiers])
360             {
361                 replace_binding(arr[key.modifiers]);
362                 continue outer;
363             }
365             if (!final_binding && parent_kmap) {
366                 var p_keycode_binds = parent_kmap.keycode_bindings;
367                 parent_kmap = null;
368                 var p_arr = p_keycode_binds[key.keyCode];
369                 var p_bind;
370                 if (p_arr && (p_bind = p_arr[key.modifiers]) != null && p_bind.keymap)
371                     parent_kmap = p_bind.keymap;
372             }
374             if (!arr)
375                 arr = (keycode_binds[key.keyCode] = []);
377             arr[key.modifiers] = make_binding();
378         }
379     }
382 define_keywords("$parent", "$help");
383 function keymap ()
385     keywords(arguments);
386     /* For efficiency, a table indexed by the key code, and then by
387      * the modifiers is used to lookup key bindings, rather than
388      * looping through all bindings in the key map to find one.  The
389      * array keycode_bindings is indexed by the keyCode; if the
390      * corresponding element for a keyCode is non-null, it is itself
391      * an array indexed by the result of get_modifiers (i.e. from 0 to 7).
392      * As before, match_function-based bindings are stored as a simple
393      * list, predicate_bindings. */
394     this.parent = arguments.$parent;
395     this.keycode_bindings = [];
396     this.predicate_bindings = [];
397     this.help = arguments.$help;
400 function copy_event(event)
402     var ev = {};
403     ev.keyCode = event.keyCode;
404     ev.charCode = event.charCode;
405     ev.ctrlKey = event.ctrlKey;
406     ev.metaKey = event.metaKey;
407     ev.altKey = event.altKey;
408     ev.shiftKey = event.shiftKey;
409     return ev;
412 function key_down_handler(event)
414     var window = this;
415     //window.dumpln("key down: " + conkeror.format_key_press(event.keyCode, conkeror.get_modifiers(event)));
417     var state = window.keyboard;
418     state.last_key_down_event = copy_event(event);
419     state.last_char_code = null;
420     state.last_key_code = null;
423 define_user_variable("keyboard_key_sequence_help_timeout", 0,
424                      "Delay (in millseconds) before the current key sequence prefix is displayed in the minibuffer.");
426 function key_press_handler(true_event)
428     try{
429         var window = this;
430         var state = window.keyboard;
432         /* ASSERT(state.last_key_down_event != null); */
434         var event = state.last_key_down_event;
435         event.charCode = true_event.charCode;
437         // If the true_event includes a keyCode, we can just use that
438         if (true_event.keyCode)
439             event.keyCode = true_event.keyCode;
441         /* Filter out events from keys like the Windows/Super/Hyper key */
442         if (event.keyCode == 0)
443             return;
445         /* Clear minibuffer message */
446         window.minibuffer.clear();    
448         var binding = null;
449         var done = true;
451         var ctx;
452         if (!state.current_context)
453             ctx = state.current_context = { window: window, key_sequence: [] };
454         else
455             ctx = state.current_context;
457         ctx.event = event;
459         var active_keymap =
460             state.active_keymap ||
461             state.override_keymap ||
462             window.buffers.current.keymap;
463         var overlay_keymap = ctx.overlay_keymap;
465         binding =
466             lookup_key_binding(active_keymap, event) ||
467             (overlay_keymap && lookup_key_binding(overlay_keymap, event));
469         ctx.overlay_keymap = null;
471         // Should we stop this event from being processed by the gui?
472         //
473         // 1) we have a binding, and the binding's fallthrough property is not
474         //    true.
475         //
476         // 2) we are in the middle of a key sequence, and we need to say that
477         //    the key sequence given has no command.
478         //
479         if (!binding || !binding.fallthrough)
480         {
481             true_event.preventDefault();
482             true_event.stopPropagation();
483         }
485         // Finally, process the binding.
486         ctx.key_sequence.push(format_key_event(event));
487         if (binding) {
488             if (binding.keymap) {
489                 if (binding.hook)
490                     binding.hook.call(null, ctx, active_keymap, overlay_keymap);
491                 state.active_keymap = binding.keymap;
492                 if (!state.help_displayed)
493                 {
494                     state.help_timer_ID = window.setTimeout(function () {
495                             window.minibuffer.show(ctx.key_sequence.join(" "));
496                             state.help_displayed = true;
497                             state.help_timer_ID = null;
498                         }, keyboard_key_sequence_help_timeout);
499                 }
500                 else
501                     window.minibuffer.show(ctx.key_sequence.join(" "));
503                 // We're going for another round
504                 done = false;
505             } else if (binding.command) {
506                 call_interactively(ctx, binding.command);
507             }
508         } else {
509             window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
510         }
512         // Clean up if we're done
513         if (done)
514         {
515             if (state.help_timer_ID != null)
516             {
517                 window.clearTimeout(state.help_timer_ID);
518                 state.help_timer_ID = null;
519             }
520             state.help_displayed = false;
521             state.active_keymap = null;
522             state.current_context = null;
523         }
524     } catch(e) { dump_error(e);}
527 function keyboard(window)
529     this.window = window;
532 keyboard.prototype = {
533     last_key_down_event : null,
534     current_context : null,
535     active_keymap : null,
536     help_timer_ID : null,
537     help_displayed : false,
539     /* If this is non-null, it is used instead of the current buffer's
540      * keymap. */
541     override_keymap : null,
543     set_override_keymap : function (keymap) {
544         /* Clear out any in-progress key sequence. */
545         this.active_keymap = null;
546         this.current_context = null;
547         if (this.help_timer_ID != null)
548         {
549             this.window.clearTimeout(this.help_timer_ID);
550             this.help_timer_ID = null;
551         }
552         this.override_keymap = keymap;
553     }
557 function keyboard_initialize_window(window)
559     window.keyboard = new keyboard(window);
561     window.addEventListener ("keydown", key_down_handler, true /* capture */,
562                             false /* ignore untrusted events */);
563     window.addEventListener ("keypress", key_press_handler, true /* capture */,
564                             false /* ignore untrusted events */);
567 add_hook("window_initialize_hook", keyboard_initialize_window);
569 function for_each_key_binding(keymap_or_buffer, callback) {
570     var keymap;
571     if (keymap_or_buffer instanceof conkeror.buffer) {
572         var buffer = keymap_or_buffer;
573         var window = buffer.window;
574         keymap = window.keyboard.override_keymap || buffer.keymap;
575     } else {
576         keymap = keymap_or_buffer;
577     }
578     var keymap_stack = [keymap];
579     var binding_stack = [];
580     function helper2(bind) {
581         binding_stack.push(bind);
582         callback(binding_stack);
583         if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
584             keymap_stack.push(bind.keymap);
585             helper();
586             keymap_stack.pop();
587         }
588         binding_stack.pop();
589     }
590     function helper() {
591         var modifier_keys_masked = false;
592         var keycode_masks = [];
593         while (true) {
594             var keymap = keymap_stack[keymap_stack.length - 1];
595             for (var i in keymap.keycode_bindings) {
596                 var b = keymap.keycode_bindings[i];
597                 if (!(i in keycode_masks))
598                     keycode_masks[i] = [];
599                 for (var j in b) {
600                     if (modifier_keys_masked && j != 0)
601                         continue;
602                     if (!keycode_masks[i][j]) {
603                         helper2(b[j]);
604                         keycode_masks[i][j] = true;
605                     }
606                 }
607             }
608             for (var i in  keymap.predicate_bindings) {
609                 var bind = keymap.predicate_bindings[i];
610                 helper2(bind);
611                 var p = bind.key.match_function;
612                 if (p == match_any_key)
613                     return;
614                 if (p == match_any_unmodified_key)
615                     modifier_keys_masked = true;
616             }
617             if (keymap.parent)
618                 keymap_stack[keymap_stack.length - 1] = keymap.parent;
619             else
620                 break;
621         }
622     }
623     helper();
626 function find_command_in_keymap(keymap_or_buffer, command) {
627     var list = [];
629     for_each_key_binding(keymap_or_buffer, function (bind_seq) {
630             var bind = bind_seq[bind_seq.length - 1];
631             if (bind.command == command)
632                 list.push(format_binding_sequence(bind_seq));
633         });
634     return list;
637 var key_binding_reader_keymap = new keymap();
638 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
640 define_keywords("$buffer", "$keymap");
641 function key_binding_reader(continuation) {
642     keywords(arguments, $prompt = "Describe key:");
644     this.continuation = continuation;
646     if (arguments.$keymap)
647         this.target_keymap = arguments.$keymap;
648     else {
649         var buffer = arguments.$buffer;
650         var window = buffer.window;
651         this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
652     }
654     this.key_sequence = [];
655     
656     minibuffer_state.call(this, key_binding_reader_keymap, arguments.$prompt);
658 key_binding_reader.prototype = {
659     __proto__: minibuffer_state.prototype,
660     destroy: function () {
661         if (this.continuation)
662             this.continuation.throw(abort());
663     }
666 function invalid_key_binding(seq) {
667     var e = new Error(seq.join(" ") + " is undefined");
668     e.key_sequence = seq;
669     e.__proto__ = invalid_key_binding.prototype;
670     return e;
672 invalid_key_binding.prototype = {
673     __proto__: interactive_error.prototype
676 function read_key_binding_key(window, state, event) {
677     var binding = lookup_key_binding(state.target_keymap, event);
679     state.key_sequence.push(format_key_event(event));
681     if (binding == null) {
682         var c = state.continuation;
683         delete state.continuation;
684         window.minibuffer.pop_state();
685         c.throw(invalid_key_binding(state.key_sequence));
686         return;
687     }
689     if (binding.keymap) {
690         window.minibuffer._ensure_input_area_showing();
691         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
692         state.target_keymap = binding.keymap;
693         return;
694     }
696     var c = state.continuation;
697     delete state.continuation;
699     window.minibuffer.pop_state();
701     if (c != null)
702         c([state.key_sequence, binding]);
704 interactive("read-key-binding-key", function (I) {
705     read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
708 minibuffer.prototype.read_key_binding = function () {
709     keywords(arguments);
710     var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
711     this.push_state(s);
712     var result = yield SUSPEND;
713     yield co_return(result);