Factor out mime.js matching logic to predicate_alist_match
[conkeror.git] / modules / keyboard.js
blobd8f342ec6eb4de87c536c951816c98aa7fe06c72
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");
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             } else {
305                 if (!bind.keymap)
306                     throw new Error("Key sequence has a non-keymap in prefix");
307                 kmap = bind.keymap;
308             }
309         }
311         function make_binding()
312         {
313             if (final_binding) {
314                 return {key: key, fallthrough: args.$fallthrough, hook: args.$hook,
315                         command: new_command, keymap: new_keymap,
316                         source_code_reference: ref};
317             }
318             else
319             {
320                 // Check for a corresponding binding a parent
321                 kmap = new keymap($parent = parent_kmap);
322                 return {key: key, keymap: kmap,
323                         source_code_reference: ref};
324             }
325         }
327         // Check if the specified binding is already present in the kmap
328         if (key.match_function)
329         {
330             var pred_binds = kmap.predicate_bindings;
331             for (var i = 0; i < pred_binds.length; i++)
332             {
333                 var cur_bind = pred_binds[i];
334                 if (cur_bind.key.match_function == key.match_function)
335                 {
336                     replace_binding(cur_bind);
337                     continue outer;
338                 }
339             }
341             if (!final_binding && parent_kmap) {
342                 var parent_pred_binds = parent_kmap.predicate_bindings;
343                 parent_kmap = null;
344                 for (var i = 0; i < parent_pred_binds.length; i++)
345                 {
346                     var cur_bind = parent_pred_binds[i];
347                     if (cur_bind.key.match_function == key.match_function && cur_bind.keymap)
348                     {
349                         parent_kmap = cur_bind.keymap;
350                         break;
351                     }
352                 }
353             }
354             // Not already present, must be added
355             pred_binds.push(make_binding());
356         } else
357         {
358             // This is a binding by keycode: look it up in the table
359             var keycode_binds = kmap.keycode_bindings;
360             var arr = keycode_binds[key.keyCode];
362             if (arr && arr[key.modifiers])
363             {
364                 replace_binding(arr[key.modifiers]);
365                 continue outer;
366             }
368             if (!final_binding && parent_kmap) {
369                 var p_keycode_binds = parent_kmap.keycode_bindings;
370                 parent_kmap = null;
371                 var p_arr = p_keycode_binds[key.keyCode];
372                 var p_bind;
373                 if (p_arr && (p_bind = p_arr[key.modifiers]) != null && p_bind.keymap)
374                     parent_kmap = p_bind.keymap;
375             }
377             if (!arr)
378                 arr = (keycode_binds[key.keyCode] = []);
380             arr[key.modifiers] = make_binding();
381         }
382     }
385 define_keywords("$parent", "$help");
386 function keymap ()
388     keywords(arguments);
389     /* For efficiency, a table indexed by the key code, and then by
390      * the modifiers is used to lookup key bindings, rather than
391      * looping through all bindings in the key map to find one.  The
392      * array keycode_bindings is indexed by the keyCode; if the
393      * corresponding element for a keyCode is non-null, it is itself
394      * an array indexed by the result of get_modifiers (i.e. from 0 to 7).
395      * As before, match_function-based bindings are stored as a simple
396      * list, predicate_bindings. */
397     this.parent = arguments.$parent;
398     this.keycode_bindings = [];
399     this.predicate_bindings = [];
400     this.help = arguments.$help;
403 function copy_event(event)
405     var ev = {};
406     ev.keyCode = event.keyCode;
407     ev.charCode = event.charCode;
408     ev.ctrlKey = event.ctrlKey;
409     ev.metaKey = event.metaKey;
410     ev.altKey = event.altKey;
411     ev.shiftKey = event.shiftKey;
412     ev.sticky_modifiers = event.sticky_modifiers;
413     return ev;
416 function key_down_handler(event)
418     var window = this;
419     //window.dumpln("key down: " + conkeror.format_key_press(event.keyCode, conkeror.get_modifiers(event)));
421     var state = window.keyboard;
422     state.last_key_down_event = copy_event(event);
423     state.last_char_code = null;
424     state.last_key_code = null;
427 define_variable("keyboard_key_sequence_help_timeout", 0,
428                 "Delay (in millseconds) before the current key sequence prefix is displayed in the minibuffer.");
430 define_window_local_hook("key_press_hook", RUN_HOOK_UNTIL_SUCCESS);
432 function key_press_handler(true_event)
434     try{
435         var window = this;
436         var state = window.keyboard;
438         /* ASSERT(state.last_key_down_event != null); */
440         var event = state.last_key_down_event;
441         event.charCode = true_event.charCode;
443         // If the true_event includes a keyCode, we can just use that
444         if (true_event.keyCode)
445             event.keyCode = true_event.keyCode;
447         /* Filter out events from keys like the Windows/Super/Hyper key */
448         if (event.keyCode == 0)
449             return;
451         /* Clear minibuffer message */
452         window.minibuffer.clear();    
454         var binding = null;
455         var done = true;
457         var ctx;
458         if (!state.current_context)
459             ctx = state.current_context = { window: window, key_sequence: [], sticky_modifiers: 0 };
460         else
461             ctx = state.current_context;
463         event.sticky_modifiers = ctx.sticky_modifiers;
464         ctx.sticky_modifiers = 0;
466         ctx.event = event;
468         if (key_press_hook.run(window, ctx, true_event))
469             return;
471         var active_keymap =
472             state.active_keymap ||
473             state.override_keymap ||
474             window.buffers.current.keymap;
475         var overlay_keymap = ctx.overlay_keymap;
477         binding =
478             lookup_key_binding(active_keymap, event) ||
479             (overlay_keymap && lookup_key_binding(overlay_keymap, event));
481         ctx.overlay_keymap = null;
483         // Should we stop this event from being processed by the gui?
484         //
485         // 1) we have a binding, and the binding's fallthrough property is not
486         //    true.
487         //
488         // 2) we are in the middle of a key sequence, and we need to say that
489         //    the key sequence given has no command.
490         //
491         if (!binding || !binding.fallthrough)
492         {
493             true_event.preventDefault();
494             true_event.stopPropagation();
495         }
497         // Finally, process the binding.
498         ctx.key_sequence.push(format_key_event(event));
499         if (binding) {
500             if (binding.keymap) {
501                 if (binding.hook)
502                     binding.hook.call(null, ctx, active_keymap, overlay_keymap);
503                 state.active_keymap = binding.keymap;
504                 if (!state.help_displayed)
505                 {
506                     state.help_timer_ID = window.setTimeout(function () {
507                             window.minibuffer.show(ctx.key_sequence.join(" "));
508                             state.help_displayed = true;
509                             state.help_timer_ID = null;
510                         }, keyboard_key_sequence_help_timeout);
511                 }
512                 else
513                     window.minibuffer.show(ctx.key_sequence.join(" "));
515                 // We're going for another round
516                 done = false;
517             } else if (binding.command) {
518                 call_interactively(ctx, binding.command);
519             }
520         } else {
521             window.minibuffer.message(ctx.key_sequence.join(" ") + " is undefined");
522         }
524         // Clean up if we're done
525         if (done)
526         {
527             if (state.help_timer_ID != null)
528             {
529                 window.clearTimeout(state.help_timer_ID);
530                 state.help_timer_ID = null;
531             }
532             state.help_displayed = false;
533             state.active_keymap = null;
534             state.current_context = null;
535         }
536     } catch(e) { dump_error(e);}
539 function keyboard(window)
541     this.window = window;
544 keyboard.prototype = {
545     last_key_down_event : null,
546     current_context : null,
547     active_keymap : null,
548     help_timer_ID : null,
549     help_displayed : false,
551     /* If this is non-null, it is used instead of the current buffer's
552      * keymap. */
553     override_keymap : null,
555     set_override_keymap : function (keymap) {
556         /* Clear out any in-progress key sequence. */
557         this.active_keymap = null;
558         this.current_context = null;
559         if (this.help_timer_ID != null)
560         {
561             this.window.clearTimeout(this.help_timer_ID);
562             this.help_timer_ID = null;
563         }
564         this.override_keymap = keymap;
565     }
569 function keyboard_initialize_window(window)
571     window.keyboard = new keyboard(window);
573     window.addEventListener ("keydown", key_down_handler, true /* capture */,
574                             false /* ignore untrusted events */);
575     window.addEventListener ("keypress", key_press_handler, true /* capture */,
576                             false /* ignore untrusted events */);
579 add_hook("window_initialize_hook", keyboard_initialize_window);
581 function for_each_key_binding(keymap_or_buffer, callback) {
582     var keymap;
583     if (keymap_or_buffer instanceof conkeror.buffer) {
584         var buffer = keymap_or_buffer;
585         var window = buffer.window;
586         keymap = window.keyboard.override_keymap || buffer.keymap;
587     } else {
588         keymap = keymap_or_buffer;
589     }
590     var keymap_stack = [keymap];
591     var binding_stack = [];
592     function helper2(bind) {
593         binding_stack.push(bind);
594         callback(binding_stack);
595         if (bind.keymap && keymap_stack.indexOf(bind.keymap) == -1) {
596             keymap_stack.push(bind.keymap);
597             helper();
598             keymap_stack.pop();
599         }
600         binding_stack.pop();
601     }
602     function helper() {
603         var unmodified_keys_masked = false;
604         var keycode_masks = [];
605         while (true) {
606             var keymap = keymap_stack[keymap_stack.length - 1];
607             for (var i in keymap.keycode_bindings) {
608                 var b = keymap.keycode_bindings[i];
609                 if (!(i in keycode_masks))
610                     keycode_masks[i] = [];
611                 for (var j in b) {
612                     if (unmodified_keys_masked && ((j & MOD_SHIFT) == j))
613                         continue;
614                     if (!keycode_masks[i][j]) {
615                         helper2(b[j]);
616                         keycode_masks[i][j] = true;
617                     }
618                 }
619             }
620             for (var i in  keymap.predicate_bindings) {
621                 var bind = keymap.predicate_bindings[i];
622                 helper2(bind);
623                 var p = bind.key.match_function;
624                 if (p == match_any_key)
625                     return;
626                 if (p == match_any_unmodified_key)
627                     unmodified_keys_masked = true;
628             }
629             if (keymap.parent)
630                 keymap_stack[keymap_stack.length - 1] = keymap.parent;
631             else
632                 break;
633         }
634     }
635     helper();
638 function find_command_in_keymap(keymap_or_buffer, command) {
639     var list = [];
641     for_each_key_binding(keymap_or_buffer, function (bind_seq) {
642             var bind = bind_seq[bind_seq.length - 1];
643             if (bind.command == command)
644                 list.push(format_binding_sequence(bind_seq));
645         });
646     return list;
649 var key_binding_reader_keymap = new keymap();
650 define_key(key_binding_reader_keymap, match_any_key, "read-key-binding-key");
652 define_keywords("$buffer", "$keymap");
653 function key_binding_reader(continuation) {
654     keywords(arguments, $prompt = "Describe key:");
656     this.continuation = continuation;
658     if (arguments.$keymap)
659         this.target_keymap = arguments.$keymap;
660     else {
661         var buffer = arguments.$buffer;
662         var window = buffer.window;
663         this.target_keymap = window.keyboard.override_keymap || buffer.keymap;
664     }
666     this.key_sequence = [];
667     
668     minibuffer_state.call(this, key_binding_reader_keymap, arguments.$prompt);
670 key_binding_reader.prototype = {
671     __proto__: minibuffer_state.prototype,
672     destroy: function () {
673         if (this.continuation)
674             this.continuation.throw(abort());
675     }
678 function invalid_key_binding(seq) {
679     var e = new Error(seq.join(" ") + " is undefined");
680     e.key_sequence = seq;
681     e.__proto__ = invalid_key_binding.prototype;
682     return e;
684 invalid_key_binding.prototype = {
685     __proto__: interactive_error.prototype
688 function read_key_binding_key(window, state, event) {
689     var binding = lookup_key_binding(state.target_keymap, event);
691     state.key_sequence.push(format_key_event(event));
693     if (binding == null) {
694         var c = state.continuation;
695         delete state.continuation;
696         window.minibuffer.pop_state();
697         c.throw(invalid_key_binding(state.key_sequence));
698         return;
699     }
701     if (binding.keymap) {
702         window.minibuffer._ensure_input_area_showing();
703         window.minibuffer._input_text = state.key_sequence.join(" ") + " ";
704         state.target_keymap = binding.keymap;
705         return;
706     }
708     var c = state.continuation;
709     delete state.continuation;
711     window.minibuffer.pop_state();
713     if (c != null)
714         c([state.key_sequence, binding]);
716 interactive("read-key-binding-key", function (I) {
717     read_key_binding_key(I.window, I.minibuffer.check_state(key_binding_reader), I.event);
720 minibuffer.prototype.read_key_binding = function () {
721     keywords(arguments);
722     var s = new key_binding_reader((yield CONTINUATION), forward_keywords(arguments));
723     this.push_state(s);
724     var result = yield SUSPEND;
725     yield co_return(result);