forums.tonybtw.com

https://git.tonybtw.com/forums.tonybtw.com.git git://git.tonybtw.com/forums.tonybtw.com.git
26,095 bytes raw
1
'use strict';
2
3
/**
4
 * this is the little modal editor we bolt onto a textarea. it tracks its
5
 * own caret + mode, draws a status line underneath, and translates key
6
 * presses into vim-ish motions and edits on the textarea value. the pure
7
 * text helpers (line_start, word_forward, ...) hang off the class as static
8
 * methods since they take (string, position) and return a number with no
9
 * instance state
10
 */
11
export class Vim_Editor {
12
13
    /**
14
     * this function returns the index of the start of the line that pos sits
15
     * on
16
     *
17
     * @param {string} s
18
     * @param {number} pos
19
     * @return {number}
20
     */
21
    static line_start(s, pos) {
22
        const i = s.lastIndexOf('\n', pos - 1);
23
        return i === -1 ? 0 : i + 1;
24
    }
25
26
    /**
27
     * this function returns the index of the end of the line pos sits on,
28
     * i.e. the next newline or the end of the string
29
     *
30
     * @param {string} s
31
     * @param {number} pos
32
     * @return {number}
33
     */
34
    static line_end(s, pos) {
35
        const i = s.indexOf('\n', pos);
36
        return i === -1 ? s.length : i;
37
    }
38
39
    /**
40
     * this function returns the index of the last real character on pos's
41
     * line (where the block cursor can sit in normal mode)
42
     *
43
     * @param {string} s
44
     * @param {number} pos
45
     * @return {number}
46
     */
47
    static last_col(s, pos) {
48
        const ls = Vim_Editor.line_start(s, pos);
49
        const le = Vim_Editor.line_end(s, pos);
50
        return le > ls ? le - 1 : ls;
51
    }
52
53
    /**
54
     * this function returns the index of the first non-blank character on
55
     * pos's line (for ^, I, gg, G)
56
     *
57
     * @param {string} s
58
     * @param {number} pos
59
     * @return {number}
60
     */
61
    static first_nonblank(s, pos) {
62
        const ls = Vim_Editor.line_start(s, pos);
63
        const le = Vim_Editor.line_end(s, pos);
64
        let i = ls;
65
        while (i < le && (s[i] === ' ' || s[i] === '\t')) {
66
            i++;
67
        }
68
        return i < le ? i : ls;
69
    }
70
71
    /**
72
     * this function buckets a character for word motions: 0 = whitespace,
73
     * 1 = word char (\w), 2 = punctuation
74
     *
75
     * @param {string} ch
76
     * @return {number}
77
     */
78
    static char_class(ch) {
79
        if (ch === undefined || ch === ' ' || ch === '\t' || ch === '\n') {
80
            return 0;
81
        }
82
        if (/[A-Za-z0-9_]/.test(ch)) {
83
            return 1;
84
        }
85
        return 2;
86
    }
87
88
    /**
89
     * this function returns the position of the next word start (like w)
90
     *
91
     * @param {string} s
92
     * @param {number} pos
93
     * @return {number}
94
     */
95
    static word_forward(s, pos) {
96
        const n = s.length;
97
        if (pos >= n) {
98
            return n;
99
        }
100
        const c = Vim_Editor.char_class(s[pos]);
101
        if (c !== 0) {
102
            while (pos < n && Vim_Editor.char_class(s[pos]) === c) {
103
                pos++;
104
            }
105
        }
106
        while (pos < n && Vim_Editor.char_class(s[pos]) === 0) {
107
            pos++;
108
        }
109
        return pos;
110
    }
111
112
    /**
113
     * this function returns the position of the end of the current/next word
114
     * (like e)
115
     *
116
     * @param {string} s
117
     * @param {number} pos
118
     * @return {number}
119
     */
120
    static word_end(s, pos) {
121
        const n = s.length;
122
        pos++;
123
        while (pos < n && Vim_Editor.char_class(s[pos]) === 0) {
124
            pos++;
125
        }
126
        if (pos >= n) {
127
            return n - 1;
128
        }
129
        const c = Vim_Editor.char_class(s[pos]);
130
        while (pos + 1 < n && Vim_Editor.char_class(s[pos + 1]) === c) {
131
            pos++;
132
        }
133
        return pos;
134
    }
135
136
    /**
137
     * this function returns the position of the previous word start (like b)
138
     *
139
     * @param {string} s
140
     * @param {number} pos
141
     * @return {number}
142
     */
143
    static word_back(s, pos) {
144
        pos--;
145
        while (pos > 0 && Vim_Editor.char_class(s[pos]) === 0) {
146
            pos--;
147
        }
148
        if (pos <= 0) {
149
            return 0;
150
        }
151
        const c = Vim_Editor.char_class(s[pos]);
152
        while (pos - 1 >= 0 && Vim_Editor.char_class(s[pos - 1]) === c) {
153
            pos--;
154
        }
155
        return pos;
156
    }
157
158
    constructor(textarea) {
159
        this.ta = textarea;
160
        this.mode = 'normal';
161
        this.caret = textarea.selectionStart || 0;
162
        this.count = '';
163
        this.op = null;
164
        this.prefix = null;
165
        this.await_char = null;
166
        this.anchor = 0;
167
        this.reg = { text: '', linewise: false };
168
        this.history = [];
169
        this.cmd_active = false;
170
        this.cmd = '';
171
172
        this.status = document.createElement('div');
173
        this.status.className = 'vim-status';
174
        this.ta.insertAdjacentElement('afterend', this.status);
175
176
        this.on_key = this.handle_key.bind(this);
177
        this.on_click = this.handle_click.bind(this);
178
        this.ta.addEventListener('keydown', this.on_key);
179
        this.ta.addEventListener('mouseup', this.on_click);
180
        this.ta.classList.add('vim-on');
181
        this.render();
182
    }
183
184
    /**
185
     * this function unhooks the editor from its textarea and removes the
186
     * status line, putting the box back to a plain textarea
187
     *
188
     * @return {void}
189
     */
190
    detach() {
191
        this.ta.removeEventListener('keydown', this.on_key);
192
        this.ta.removeEventListener('mouseup', this.on_click);
193
        this.ta.classList.remove('vim-on', 'vim-normal', 'vim-insert', 'vim-visual');
194
        if (this.status && this.status.parentNode) {
195
            this.status.parentNode.removeChild(this.status);
196
        }
197
    }
198
199
    text() {
200
        return this.ta.value;
201
    }
202
203
    /**
204
     * this function pushes the current value + caret onto the undo stack
205
     * (capped so it doesnt grow forever)
206
     *
207
     * @return {void}
208
     */
209
    snapshot() {
210
        this.history.push({ v: this.ta.value, caret: this.caret });
211
        if (this.history.length > 200) {
212
            this.history.shift();
213
        }
214
    }
215
216
    /**
217
     * this function pops the last snapshot and restores it (u)
218
     *
219
     * @return {void}
220
     */
221
    undo() {
222
        const prev = this.history.pop();
223
        if (!prev) {
224
            return;
225
        }
226
        this.ta.value = prev.v;
227
        this.caret = Math.min(prev.caret, this.ta.value.length);
228
    }
229
230
    /**
231
     * this function keeps the caret inside the current line in normal mode,
232
     * so it never lands past the last character or on the newline
233
     *
234
     * @return {void}
235
     */
236
    clamp_normal() {
237
        const s = this.text();
238
        if (this.caret > s.length) {
239
            this.caret = s.length;
240
        }
241
        const ls = Vim_Editor.line_start(s, this.caret);
242
        const lc = Vim_Editor.last_col(s, this.caret);
243
        if (this.caret > lc) {
244
            this.caret = lc;
245
        }
246
        if (this.caret < ls) {
247
            this.caret = ls;
248
        }
249
    }
250
251
    /**
252
     * this function redraws the status line for the current mode and paints
253
     * the "cursor": a one-char selection in normal mode (so it reads like a
254
     * block cursor) or the full range in visual mode
255
     *
256
     * @return {void}
257
     */
258
    render() {
259
        const s = this.text();
260
        this.ta.classList.remove('vim-normal', 'vim-insert', 'vim-visual');
261
        this.ta.classList.add('vim-' + this.mode);
262
263
        if (this.mode === 'insert') {
264
            this.status.textContent = '-- INSERT --';
265
            return;
266
        }
267
268
        if (this.cmd_active) {
269
            this.status.textContent = ':' + this.cmd;
270
        } else if (this.mode === 'visual') {
271
            this.status.textContent = '-- VISUAL --';
272
        } else {
273
            this.status.textContent = 'NORMAL';
274
        }
275
276
        if (this.mode === 'visual') {
277
            const lo = Math.min(this.anchor, this.caret);
278
            const hi = Math.max(this.anchor, this.caret) + 1;
279
            this.ta.setSelectionRange(lo, Math.min(hi, s.length));
280
        } else {
281
            const end = this.caret < s.length && s[this.caret] !== '\n' ? this.caret + 1 : this.caret;
282
            this.ta.setSelectionRange(this.caret, end);
283
        }
284
    }
285
286
    /**
287
     * this function syncs our caret to wherever the mouse put it (so clicking
288
     * around in normal mode behaves)
289
     *
290
     * @return {void}
291
     */
292
    handle_click() {
293
        if (this.mode === 'insert') {
294
            return;
295
        }
296
        this.caret = this.ta.selectionStart;
297
        this.clamp_normal();
298
        this.render();
299
    }
300
301
    /**
302
     * this function inserts text at the current selection in insert mode
303
     * (used for the tab key)
304
     *
305
     * @param {string} text
306
     * @return {void}
307
     */
308
    insert_text(text) {
309
        const ta = this.ta;
310
        const start = ta.selectionStart;
311
        const end = ta.selectionEnd;
312
        ta.value = ta.value.slice(0, start) + text + ta.value.slice(end);
313
        ta.setSelectionRange(start + text.length, start + text.length);
314
    }
315
316
    /**
317
     * this function drops into insert mode at the given caret, snapshotting
318
     * first so the whole insert can be undone in one go
319
     *
320
     * @param {number} caret
321
     * @return {void}
322
     */
323
    to_insert(caret) {
324
        this.snapshot();
325
        this.mode = 'insert';
326
        this.caret = caret;
327
        this.ta.setSelectionRange(caret, caret);
328
        this.render();
329
    }
330
331
    /**
332
     * this function leaves insert mode back to normal, nudging the caret left
333
     * one like vim does on escape
334
     *
335
     * @return {void}
336
     */
337
    to_normal() {
338
        if (this.mode === 'insert') {
339
            this.caret = this.ta.selectionStart;
340
            const ls = Vim_Editor.line_start(this.text(), this.caret);
341
            if (this.caret > ls) {
342
                this.caret--;
343
            }
344
        }
345
        this.mode = 'normal';
346
        this.op = null;
347
        this.prefix = null;
348
        this.count = '';
349
        this.clamp_normal();
350
        this.render();
351
    }
352
353
    /**
354
     * this function resolves a character motion (h l w b e j k) n times and
355
     * returns the resulting index without moving the caret
356
     *
357
     * @param {string} key
358
     * @param {number} n
359
     * @return {number}
360
     */
361
    motion(key, n) {
362
        const s = this.text();
363
        let pos = this.caret;
364
        for (let i = 0; i < n; i++) {
365
            if (key === 'h') {
366
                pos = Math.max(Vim_Editor.line_start(s, pos), pos - 1);
367
            } else if (key === 'l') {
368
                pos = Math.min(Vim_Editor.last_col(s, pos), pos + 1);
369
            } else if (key === 'w') {
370
                pos = Vim_Editor.word_forward(s, pos);
371
            } else if (key === 'b') {
372
                pos = Vim_Editor.word_back(s, pos);
373
            } else if (key === 'e') {
374
                pos = Vim_Editor.word_end(s, pos);
375
            } else if (key === 'j' || key === 'k') {
376
                const col = pos - Vim_Editor.line_start(s, pos);
377
                if (key === 'j') {
378
                    const nle = Vim_Editor.line_end(s, pos);
379
                    if (nle >= s.length) {
380
                        break;
381
                    }
382
                    const ns = nle + 1;
383
                    pos = Math.min(ns + col, Vim_Editor.last_col(s, ns));
384
                } else {
385
                    const cls = Vim_Editor.line_start(s, pos);
386
                    if (cls === 0) {
387
                        break;
388
                    }
389
                    const ps = Vim_Editor.line_start(s, cls - 1);
390
                    pos = Math.min(ps + col, Vim_Editor.last_col(s, ps));
391
                }
392
            }
393
        }
394
        return pos;
395
    }
396
397
    /**
398
     * this function returns the [from, to) range covering n whole lines from
399
     * the caret, used by linewise operators like dd / yy
400
     *
401
     * @param {number} n
402
     * @return {{from: number, to: number}}
403
     */
404
    line_range(n) {
405
        const s = this.text();
406
        const from = Vim_Editor.line_start(s, this.caret);
407
        let to = from;
408
        for (let i = 0; i < n; i++) {
409
            to = Vim_Editor.line_end(s, to);
410
            if (to < s.length) {
411
                to++;
412
            }
413
        }
414
        return { from: from, to: to };
415
    }
416
417
    /**
418
     * this function applies an operator (d/c/y) over a range. it yanks into
419
     * the register, and for d/c it also deletes the text (c then drops into
420
     * insert)
421
     *
422
     * @param {string} op
423
     * @param {number} from
424
     * @param {number} to
425
     * @param {boolean} linewise
426
     * @return {void}
427
     */
428
    apply_op(op, from, to, linewise) {
429
        const s = this.text();
430
        if (from > to) {
431
            const t = from;
432
            from = to;
433
            to = t;
434
        }
435
        const chunk = s.slice(from, to);
436
        if (op === 'y') {
437
            this.reg = { text: chunk, linewise: linewise };
438
            this.caret = from;
439
            return;
440
        }
441
        this.snapshot();
442
        this.reg = { text: chunk, linewise: linewise };
443
        this.ta.value = s.slice(0, from) + s.slice(to);
444
        this.caret = from;
445
        if (op === 'c') {
446
            if (linewise) {
447
                this.ta.value = this.ta.value.slice(0, from) + '\n' + this.ta.value.slice(from);
448
            }
449
            this.mode = 'normal';
450
            this.to_insert(from);
451
            return;
452
        }
453
        this.clamp_normal();
454
    }
455
456
    /**
457
     * this function pastes the register after (p) or before (P) the caret,
458
     * handling linewise vs charwise registers the way vim does
459
     *
460
     * @param {boolean} after
461
     * @return {void}
462
     */
463
    paste(after) {
464
        const s = this.text();
465
        this.snapshot();
466
        if (this.reg.linewise) {
467
            let insert_at;
468
            if (after) {
469
                insert_at = Vim_Editor.line_end(s, this.caret);
470
                this.ta.value = s.slice(0, insert_at) + '\n' + this.reg.text.replace(/\n$/, '') + s.slice(insert_at);
471
                this.caret = insert_at + 1;
472
            } else {
473
                insert_at = Vim_Editor.line_start(s, this.caret);
474
                this.ta.value = s.slice(0, insert_at) + this.reg.text.replace(/\n$/, '') + '\n' + s.slice(insert_at);
475
                this.caret = insert_at;
476
            }
477
        } else {
478
            const at = after ? Math.min(this.caret + 1, s.length) : this.caret;
479
            this.ta.value = s.slice(0, at) + this.reg.text + s.slice(at);
480
            this.caret = at + this.reg.text.length - 1;
481
        }
482
        this.clamp_normal();
483
    }
484
485
    /**
486
     * this function is the keydown handler. in insert mode it only intercepts
487
     * escape and tab and lets everything else type through; otherwise it
488
     * routes the key into dispatch (or the command line if one is open)
489
     *
490
     * @param {KeyboardEvent} e
491
     * @return {void}
492
     */
493
    handle_key(e) {
494
        if (this.cmd_active) {
495
            this.handle_cmdline(e);
496
            return;
497
        }
498
        if (this.mode === 'insert') {
499
            if (e.key === 'Escape') {
500
                e.preventDefault();
501
                this.to_normal();
502
            } else if (e.key === 'Tab') {
503
                e.preventDefault();
504
                this.insert_text('    ');
505
            }
506
            return;
507
        }
508
        if (e.ctrlKey || e.metaKey || e.altKey) {
509
            return;
510
        }
511
        if (e.key === 'Shift' || e.key === 'CapsLock') {
512
            return;
513
        }
514
        e.preventDefault();
515
        this.dispatch(e.key);
516
    }
517
518
    /**
519
     * this function is the normal/visual mode command interpreter. it handles
520
     * pending operators, counts, the g prefix, r, motions, edits, and the
521
     * mode switches
522
     *
523
     * @param {string} key
524
     * @return {void}
525
     */
526
    dispatch(key) {
527
        const s = this.text();
528
529
        if (this.await_char) {
530
            if (this.await_char === 'r' && key.length === 1) {
531
                this.snapshot();
532
                this.ta.value = s.slice(0, this.caret) + key + s.slice(this.caret + 1);
533
            }
534
            this.await_char = null;
535
            this.render();
536
            return;
537
        }
538
539
        if (this.prefix === 'g') {
540
            this.prefix = null;
541
            if (key === 'g') {
542
                this.caret = Vim_Editor.first_nonblank(s, 0);
543
            }
544
            this.render();
545
            return;
546
        }
547
548
        if (this.mode === 'visual' && 'dxyc'.indexOf(key) !== -1) {
549
            this.handle_visual(key);
550
            return;
551
        }
552
553
        if (/[1-9]/.test(key) || (key === '0' && this.count !== '')) {
554
            this.count += key;
555
            return;
556
        }
557
558
        const n = this.count === '' ? 1 : parseInt(this.count, 10);
559
        this.count = '';
560
561
        if (key === 'g') {
562
            this.prefix = 'g';
563
            return;
564
        }
565
566
        if (this.op) {
567
            this.handle_operator_motion(key, n);
568
            return;
569
        }
570
571
        if ('hjklwbe'.indexOf(key) !== -1) {
572
            this.caret = this.motion(key, n);
573
            this.clamp_normal_for(key);
574
            this.render();
575
            return;
576
        }
577
578
        switch (key) {
579
            case '0':
580
                this.caret = Vim_Editor.line_start(s, this.caret);
581
                break;
582
            case '^':
583
                this.caret = Vim_Editor.first_nonblank(s, this.caret);
584
                break;
585
            case '$':
586
                this.caret = Vim_Editor.last_col(s, this.caret);
587
                break;
588
            case 'G':
589
                this.caret = Vim_Editor.first_nonblank(s, s.length);
590
                break;
591
            case 'i':
592
                this.to_insert(this.caret);
593
                return;
594
            case 'a':
595
                this.to_insert(Math.min(this.caret + 1, s.length));
596
                return;
597
            case 'I':
598
                this.to_insert(Vim_Editor.first_nonblank(s, this.caret));
599
                return;
600
            case 'A':
601
                this.to_insert(Vim_Editor.line_end(s, this.caret));
602
                return;
603
            case 'o':
604
                this.open_line(true);
605
                return;
606
            case 'O':
607
                this.open_line(false);
608
                return;
609
            case 'x':
610
                this.delete_chars(n);
611
                break;
612
            case 'D':
613
                this.apply_op('d', this.caret, Vim_Editor.line_end(s, this.caret), false);
614
                break;
615
            case 'C':
616
                this.apply_op('c', this.caret, Vim_Editor.line_end(s, this.caret), false);
617
                return;
618
            case 'r':
619
                this.await_char = 'r';
620
                return;
621
            case 'd':
622
            case 'c':
623
            case 'y':
624
                this.op = key;
625
                this.op_count = n;
626
                return;
627
            case 'p':
628
                this.paste(true);
629
                break;
630
            case 'P':
631
                this.paste(false);
632
                break;
633
            case 'u':
634
                this.undo();
635
                break;
636
            case 'v':
637
                this.mode = 'visual';
638
                this.anchor = this.caret;
639
                break;
640
            case ':':
641
                this.cmd_active = true;
642
                this.cmd = '';
643
                this.render();
644
                return;
645
            case 'Escape':
646
                if (this.mode === 'visual') {
647
                    this.mode = 'normal';
648
                }
649
                break;
650
            default:
651
                if (this.mode === 'visual') {
652
                    this.handle_visual(key);
653
                    return;
654
                }
655
                this.render();
656
                return;
657
        }
658
659
        if (this.mode === 'visual' && 'hjklwbe0^$G'.indexOf(key) !== -1) {
660
            this.render();
661
            return;
662
        }
663
        this.clamp_normal();
664
        this.render();
665
    }
666
667
    clamp_normal_for(key) {
668
        if (this.mode === 'visual') {
669
            const s = this.text();
670
            if (this.caret > s.length) {
671
                this.caret = s.length;
672
            }
673
            return;
674
        }
675
        this.clamp_normal();
676
    }
677
678
    /**
679
     * this function handles d/x/y/c while in visual mode: it operates over
680
     * the current selection then drops back to normal mode
681
     *
682
     * @param {string} key
683
     * @return {void}
684
     */
685
    handle_visual(key) {
686
        const s = this.text();
687
        if (key === 'd' || key === 'x' || key === 'y' || key === 'c') {
688
            const lo = Math.min(this.anchor, this.caret);
689
            const hi = Math.max(this.anchor, this.caret) + 1;
690
            const op = key === 'x' ? 'd' : key;
691
            this.mode = 'normal';
692
            this.apply_op(op, lo, Math.min(hi, s.length), false);
693
            if (op !== 'c') {
694
                this.render();
695
            }
696
            return;
697
        }
698
        this.render();
699
    }
700
701
    /**
702
     * this function completes an operator that was waiting on a motion (dd,
703
     * dw, cw, d$, yy, ...) and applies it over the resulting range
704
     *
705
     * @param {string} key
706
     * @param {number} n
707
     * @return {void}
708
     */
709
    handle_operator_motion(key, n) {
710
        const s = this.text();
711
        const op = this.op;
712
        this.op = null;
713
        const nn = (this.op_count || 1) * n;
714
715
        if (key === op || (op === 'd' && key === 'd') || (op === 'c' && key === 'c') || (op === 'y' && key === 'y')) {
716
            const lr = this.line_range(nn);
717
            this.apply_op(op, lr.from, lr.to, true);
718
            this.render();
719
            return;
720
        }
721
722
        const from = this.caret;
723
        let to = this.caret;
724
        if ('wbe'.indexOf(key) !== -1) {
725
            to = this.motion(key, nn);
726
            if (key === 'e') {
727
                to += 1;
728
            }
729
        } else if (key === 'l' || key === 'h') {
730
            to = this.motion(key, nn);
731
        } else if (key === '$') {
732
            to = Vim_Editor.line_end(s, this.caret);
733
        } else if (key === '0') {
734
            to = Vim_Editor.line_start(s, this.caret);
735
        } else {
736
            this.render();
737
            return;
738
        }
739
        this.apply_op(op, from, to, false);
740
        this.render();
741
    }
742
743
    /**
744
     * this function deletes n characters from the caret (x), stopping at the
745
     * end of the line
746
     *
747
     * @param {number} n
748
     * @return {void}
749
     */
750
    delete_chars(n) {
751
        const s = this.text();
752
        const le = Vim_Editor.line_end(s, this.caret);
753
        const to = Math.min(this.caret + n, le);
754
        if (to <= this.caret) {
755
            return;
756
        }
757
        this.apply_op('d', this.caret, to, false);
758
    }
759
760
    /**
761
     * this function opens a new line below (o) or above (O) and drops into
762
     * insert mode on it
763
     *
764
     * @param {boolean} below
765
     * @return {void}
766
     */
767
    open_line(below) {
768
        const s = this.text();
769
        this.snapshot();
770
        let at;
771
        if (below) {
772
            at = Vim_Editor.line_end(s, this.caret);
773
            this.ta.value = s.slice(0, at) + '\n' + s.slice(at);
774
            this.mode = 'normal';
775
            this.to_insert(at + 1);
776
        } else {
777
            at = Vim_Editor.line_start(s, this.caret);
778
            this.ta.value = s.slice(0, at) + '\n' + s.slice(at);
779
            this.mode = 'normal';
780
            this.to_insert(at);
781
        }
782
    }
783
784
    /**
785
     * this function feeds keystrokes into the ":" command line and runs it on
786
     * enter (escape cancels, backspace edits)
787
     *
788
     * @param {KeyboardEvent} e
789
     * @return {void}
790
     */
791
    handle_cmdline(e) {
792
        e.preventDefault();
793
        if (e.key === 'Escape') {
794
            this.cmd_active = false;
795
            this.cmd = '';
796
            this.render();
797
            return;
798
        }
799
        if (e.key === 'Enter') {
800
            this.cmd_active = false;
801
            this.run_command(this.cmd);
802
            this.cmd = '';
803
            this.render();
804
            return;
805
        }
806
        if (e.key === 'Backspace') {
807
            this.cmd = this.cmd.slice(0, -1);
808
            this.render();
809
            return;
810
        }
811
        if (e.key.length === 1) {
812
            this.cmd += e.key;
813
            this.render();
814
        }
815
    }
816
817
    /**
818
     * this function runs an ex command. only the "write" variants are wired
819
     * up, and they submit the post form via its primary button
820
     *
821
     * @param {string} cmd
822
     * @return {void}
823
     */
824
    run_command(cmd) {
825
        cmd = cmd.trim();
826
        if (cmd === 'w' || cmd === 'wq' || cmd === 'x' || cmd === 'wq!') {
827
            const form = this.ta.form;
828
            if (!form) {
829
                return;
830
            }
831
            const primary = form.querySelector('button[value="create"], button[value="post"]');
832
            if (primary && form.requestSubmit) {
833
                form.requestSubmit(primary);
834
            } else {
835
                form.submit();
836
            }
837
        }
838
    }
839
}
840
841
/**
842
 * this function wires up the page: it reads the saved preference, syncs the
843
 * "vim" checkboxes, and attaches/detaches editors on the post boxes when the
844
 * toggle changes. its progressive enhancement, so with js off the boxes are
845
 * just plain textareas
846
 *
847
 * @return {void}
848
 */
849
function vim_init() {
850
    let pref = false;
851
    try {
852
        pref = localStorage.getItem('forum_vim') === '1';
853
    } catch (e) {}
854
855
    const toggles = Array.prototype.slice.call(document.querySelectorAll('.vim-toggle input'));
856
    let editors = [];
857
858
    function targets() {
859
        return Array.prototype.slice.call(document.querySelectorAll('form .vim-area'));
860
    }
861
    function enable() {
862
        editors = targets().map((t) => new Vim_Editor(t));
863
    }
864
    function disable() {
865
        editors.forEach((ed) => ed.detach());
866
        editors = [];
867
    }
868
869
    toggles.forEach((cb) => {
870
        cb.checked = pref;
871
        cb.addEventListener('change', () => {
872
            toggles.forEach((t) => { t.checked = cb.checked; });
873
            try {
874
                localStorage.setItem('forum_vim', cb.checked ? '1' : '0');
875
            } catch (e) {}
876
            disable();
877
            if (cb.checked) {
878
                enable();
879
            }
880
        });
881
    });
882
883
    if (pref) {
884
        enable();
885
    }
886
}
887
888
if (typeof document !== 'undefined' && document.querySelectorAll) {
889
    vim_init();
890
}