]> git.neil.brown.name Git - edlib.git/blob - mode-basic.c
TODO: clean out done items.
[edlib.git] / mode-basic.c
1 /*
2  * Copyright Neil Brown ©2015-2023 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * Define basic key/mouse interactions that conform to
6  * CUA.  All special modes should build on this.
7  *
8  */
9 #include <unistd.h>
10 #include <stdlib.h>
11 #include <wctype.h>
12
13
14 #define PANE_DATA_VOID
15 #include "core.h"
16
17 /* num2 is used to track if successive commands are related.
18  * Only low 16 bits identify command, other bits are free.
19  */
20 enum {
21         N2_zero = 0,
22         N2_undo_insert, /* adjacent commands form a single undo set */
23         N2_undo_delete, /* adjacent commands form a single undo set */
24         N2_undo_change, /* adjacent commands form a single undo set */
25         N2_recentre,    /* repeated recentre goes to different places */
26         N2_yank,        /* repeated yank-pop takes different result */
27         N2_match,       /* repeated ` after Cx` repeats the search */
28         N2_undo,        /* Last command was 'undo' too */
29         N2_close_others,/* Last command was close-other, just a 1 can repeat */
30         N2_runmacro,    /* Last command was CX-e, just an 'e' can repeat */
31         N2_shift,       /* Last command was CX-< or CX-> */
32         N2_growx,       /* Last command was CX-{ or CX-} */
33         N2_uniquote,    /* Last command was :C-q inserting a unicode from name */
34 };
35 static inline int N2(const struct cmd_info *ci safe)
36 {
37         return ci->num2 & 0xffff;
38 }
39
40 static inline int N2a(const struct cmd_info *ci safe)
41 {
42         return ci->num2 >> 16;
43 }
44
45
46 /* selection:active encodes 4 different states for the selection
47  * 0 - inactive.  The other-end might exist but it is passive, not displayed
48  *                and not used unless explicitly asked for (Cx Cx)
49  * 1 - active.    Selection is active and will remain active if cursor
50  *                moves or text is changed.  Is used in various ways.
51  * 2 - transient. Is active, but will be canceled on movement or change.
52  *                Will be used for copy/cut, but little else
53  * 3 - replacable Transient plus text entry will delete selected content.
54  */
55
56 static void set_selection(struct pane *p safe, struct mark *pt,
57                           struct mark *mk, int type)
58 {
59         int active;
60         if (!type || !mk)
61                 return;
62         active = attr_find_int(mk->attrs, "selection:active");
63         if (active == type)
64                 return;
65         attr_set_int(&mk->attrs, "selection:active", type);
66         if (!pt)
67                 pt = call_ret(mark, "doc:point", p);
68         if (!pt)
69                 return;
70         if (active <= 0)
71                 attr_set_int(&pt->attrs, "selection:active", 1);
72         if (!mark_same(pt, mk))
73                 call("view:changed", p, 0, pt, NULL, 0, mk);
74 }
75
76 DEF_CMD(basic_selection_set)
77 {
78         set_selection(ci->focus, ci->mark2, ci->mark, ci->num);
79         return 1;
80 }
81
82 static bool clear_selection(struct pane *p safe, struct mark *pt,
83                             struct mark *mk, int type)
84 {
85         int active;
86         if (!mk)
87                 return False;
88         active = attr_find_int(mk->attrs, "selection:active");
89         if (active <= 0)
90                 return False;
91         if (type && active < type)
92                 return False;
93         attr_set_int(&mk->attrs, "selection:active", 0);
94         if (!pt)
95                 pt = call_ret(mark, "doc:point", p);
96         if (!pt)
97                 return True;
98         attr_set_int(&pt->attrs, "selection:active", 0);
99         if (!mark_same(pt, mk))
100                 call("view:changed", p, 0, pt, NULL, 0, mk);
101         return True;
102 }
103
104 DEF_CMD(basic_selection_clear)
105 {
106         if (clear_selection(ci->focus, ci->mark2, ci->mark, ci->num))
107                 return 1;
108         else
109                 return Efalse;
110 }
111
112 static void update_sel(struct pane *p safe,
113                        struct mark *pt safe, struct mark *m2 safe,
114                        const char *type)
115 {
116         struct mark *mfirst, *mlast;
117         struct mark *mk;
118
119         call("Move-to", p, 1, m2);
120         mk = call_ret(mark2, "doc:point", p);
121         if (!mk)
122                 return;
123         if (!type)
124                 type = attr_find(m2->attrs, "selection-type");
125         else
126                 attr_set_str(&m2->attrs, "selection-type", type);
127
128         if (type && strcmp(type, "char") != 0) {
129
130                 if (pt->seq < mk->seq) {
131                         mfirst = pt;
132                         mlast = mk;
133                 } else {
134                         mfirst = mk;
135                         mlast = pt;
136                 }
137                 if (strcmp(type, "word") == 0) {
138                         wint_t wch = doc_prior(p, mfirst);
139                         /* never move back over spaces */
140                         if (wch != WEOF && !iswspace(wch))
141                                 call("doc:word", p, -1,  mfirst);
142                         wch = doc_following(p, mlast);
143                         /* For forward over a single space is OK */
144                         if (wch != WEOF && iswspace(wch))
145                                 doc_next(p, mlast);
146                         else
147                                 call("doc:word", p, 1, mlast);
148                 } else {
149                         call("doc:EOL", p, -1,  mfirst);
150                         /* Include trailing newline */
151                         call("doc:EOL", p, 1, mlast, NULL, 1);
152                 }
153         }
154
155         /* Don't set selection until range is non-empty, else we
156          * might clear some other selection too early.
157          */
158         if (!mark_same(pt, mk)) {
159                 /* Must call 'claim' first as it might be claiming from us */
160                 call("selection:claim", p);
161                 set_selection(p, pt, mk, 2);
162         }
163 }
164
165 DEF_CMD(basic_press)
166 {
167         /* The second mark (managed by core-doc) is used to record the
168          * selected starting point.  When double- or triple- click
169          * asks for word or line selection, the actually start, which
170          * is stored in the first mark, may be different.
171          * That selected starting point will record the current unit
172          * siez in the emacs:selection-type attribute.
173          */
174         struct mark *pt = call_ret(mark, "doc:point", ci->focus);
175         struct mark *mk = call_ret(mark2, "doc:point", ci->focus);
176         struct mark *m2 = call_ret(mark2, "doc:point", ci->focus, 2);
177         struct mark *m = mark_new(ci->focus);
178         char *type;
179
180         if (!m || !pt) {
181                 /* Not in document, not my problem */
182                 mark_free(m);
183                 return Efallthrough;
184         }
185         /* NOTE must find new location before view changes. */
186         call("Move-CursorXY", ci->focus, 0, m, "prepare",
187              0, NULL, NULL, ci->x, ci->y);
188
189         clear_selection(ci->focus, pt, mk, 0);
190         call("Move-to", ci->focus, 0, m);
191         pane_take_focus(ci->focus);
192
193         if (m2 && strcmp(ci->key, "M:DPress-1") == 0) {
194                 type = attr_find(m2->attrs, "emacs:selection-type");
195                 if (!type)
196                         type = "char";
197                 else if (strcmp(type, "char") == 0)
198                         type = "word";
199                 else if (strcmp(type, "word") == 0)
200                         type = "line";
201                 else
202                         type = "char";
203         } else {
204                 type = "char";
205                 /* Record start of selection */
206                 call("Move-to", ci->focus, 2, m);
207                 m2 = call_ret(mark2, "doc:point", ci->focus, 2);
208                 if (m2)
209                         attr_set_str(&m2->attrs, "emacs:selection-type", type);
210         }
211         if (m2) {
212                 /* Record co-ordinate of start so we can tell if the mouse moved. */
213                 attr_set_int(&m2->attrs, "emacs:track-selection",
214                              1 + ci->x * 10000 + ci->y);
215                 update_sel(ci->focus, pt, m2, type);
216         }
217         mark_free(m);
218
219         return 1;
220 }
221
222 DEF_CMD(basic_release)
223 {
224         struct mark *p = call_ret(mark, "doc:point", ci->focus);
225         struct mark *mk = call_ret(mark2, "doc:point", ci->focus);
226         struct mark *m2 = call_ret(mark2, "doc:point", ci->focus, 2);
227         struct mark *m = mark_new(ci->focus);
228         char *type;
229         int prev_pos;
230         int moved;
231
232         if (!p || !m2 || !m) {
233                 /* Not in a document or no selection start - not my problem */
234                 mark_free(m);
235                 return Efallthrough;
236         }
237
238         prev_pos = attr_find_int(m2->attrs, "emacs:track-selection");
239         type = attr_find(m2->attrs, "emacs:selection-type");
240         moved = prev_pos != (1 + ci->x * 10000 + ci->y);
241         attr_set_int(&m2->attrs, "emacs:track-selection", 0);
242
243         call("Move-CursorXY", ci->focus,
244              0, m, "activate", 0, NULL, NULL, ci->x, ci->y);
245         /* That action might have closed a pane.  Better check... */
246         if (ci->focus->damaged & DAMAGED_CLOSED) {
247                 /* Do nothing */
248         } else if (moved) {
249                 /* Moved the mouse, so new location is point */
250                 call("Move-to", ci->focus, 0, m);
251                 update_sel(ci->focus, p, m2, NULL);
252         } else if (type && strcmp(type, "char") != 0) {
253                 /* Otherwise use the old location.  Point might not
254                  * be there exactly if it was moved to end of word/line
255                  */
256                 call("Move-to", ci->focus, 0, m2);
257                 update_sel(ci->focus, p, m2, NULL);
258         } else
259                 clear_selection(ci->focus, p, mk, 0);
260
261         mark_free(m);
262
263         return 1;
264 }
265
266 DEF_CMD(basic_menu_open)
267 {
268         /* If there is a menu action here, activate it. */
269         /* Don't move the cursor though */
270         struct mark *m = mark_new(ci->focus);
271         int ret;
272
273         ret = call("Move-CursorXY", ci->focus, 0, m, "menu",
274                    0, NULL, NULL, ci->x, ci->y);
275         mark_free(m);
276         return ret;
277 }
278
279 DEF_CMD(basic_menu_select)
280 {
281         /* If a menu was opened it should have claimed the mouse focus
282          * so ci->focus is now the menu.  We want to activate the entry
283          * under the mouse
284          */
285         struct mark *m = mark_new(ci->focus);
286         int ret;
287
288         ret = call("Move-CursorXY", ci->focus, 0, m, "activate",
289                    0, NULL, NULL, ci->x, ci->y);
290         mark_free(m);
291         return ret;
292 }
293
294 DEF_CMD(basic_motion)
295 {
296         struct mark *p = call_ret(mark, "doc:point", ci->focus);
297         struct mark *m2 = call_ret(mark2, "doc:point", ci->focus, 2);
298
299         if (!p || !m2)
300                 return Enoarg;
301
302         if (attr_find_int(m2->attrs, "emacs:track-selection") <= 0)
303                 return Efallthrough;
304
305         call("Move-CursorXY", ci->focus,
306              0, NULL, NULL, 0, NULL, NULL, ci->x, ci->y);
307
308         update_sel(ci->focus, p, m2, NULL);
309         return 1;
310 }
311
312 DEF_CMD(basic_paste_direct)
313 {
314         /* This command is an explicit paste command and the content
315          * is available via "Paste:get".
316          * It might come via the mouse (with x,y) or via a keystroke.
317          */
318         char *s;
319         if (ci->key[0] == 'M') {
320                 call("Move-CursorXY", ci->focus,
321                      0, NULL, NULL, 0, NULL, NULL, ci->x, ci->y);
322                 pane_take_focus(ci->focus);
323         }
324
325         s = call_ret(str, "Paste:get", ci->focus);
326         if (s && *s) {
327                 struct mark *pt = call_ret(mark, "doc:point", ci->focus);
328                 struct mark *mk;
329                 call("Move-to", ci->focus, 1);
330                 mk = call_ret(mark2, "doc:point", ci->focus);
331                 call("Replace", ci->focus, 0, mk, s, 0, pt);
332                 set_selection(ci->focus, pt, mk, 2);
333         }
334         free(s);
335         return 1;
336 }
337
338
339 DEF_CMD(basic_attrs)
340 {
341         struct call_return cr;
342         int active;
343         char *selection = "bg:white-80,vis-nl,menu-at-mouse,action-menu:emacs:selection-menu"; // grey
344
345         if (!ci->str)
346                 return Enoarg;
347
348         cr = call_ret(all, "doc:point", ci->focus);
349         if (cr.ret <= 0 || !cr.m || !cr.m2 || !ci->mark)
350                 return 1;
351         active = attr_find_int(cr.m2->attrs, "selection:active");
352         if (active <= 0)
353                 return 1;
354         if (active >= 3) /* replacable */
355                 selection = "bg:red+80,vis-nl"; // pink
356         if (mark_same(cr.m, cr.m2))
357                 return 1;
358         if (strcmp(ci->str, "render:interactive-mark") == 0) {
359                 if (ci->mark == cr.m2 && cr.m2->seq < cr.m->seq)
360                         return comm_call(ci->comm2, "attr:callback", ci->focus, 0,
361                                          ci->mark, selection, 210);
362                 if (ci->mark == cr.m2)
363                         return comm_call(ci->comm2, "attr:callback", ci->focus, -1,
364                                          ci->mark, selection, 210);
365         }
366         if (strcmp(ci->str, "render:interactive-point") == 0) {
367                 if (cr.m == ci->mark && cr.m->seq < cr.m2->seq)
368                         return comm_call(ci->comm2, "attr:cb", ci->focus, 0,
369                                          ci->mark, selection, 210);
370                 if (cr.m == ci->mark)
371                         return comm_call(ci->comm2, "attr:callback", ci->focus, -1,
372                                          ci->mark, selection, 210);
373         }
374         if (strcmp(ci->str, "start-of-line") == 0) {
375                 if ((cr.m->seq < ci->mark->seq && ci->mark->seq < cr.m2->seq &&
376                      !mark_same(ci->mark, cr.m2)) ||
377                     (cr.m2->seq < ci->mark->seq && ci->mark->seq < cr.m->seq &&
378                      !mark_same(ci->mark, cr.m)))
379                         return comm_call(ci->comm2, "attr:cb", ci->focus, 0,
380                                          ci->mark, selection, 210);
381         }
382         return Efallthrough;
383 }
384
385 DEF_CMD(basic_selection_menu)
386 {
387         struct pane *p;
388
389         p = call_ret(pane, "attach-menu", ci->focus, 0, NULL, "V", 0, NULL,
390                      "emacs:selection-menu-action", ci->x, ci->y+1);
391         if (!p)
392                 return Efail;
393         call("global-multicall-selection-menu:add-", p);
394         call("menu-add", p, 0, NULL, "de-select", 0, NULL, ":ESC");
395         return 1;
396 }
397
398 DEF_CMD(basic_selection_menu_action)
399 {
400         struct pane *home = ci->home;
401         const char *c = ci->str;
402
403         if (!c)
404                 return 1;
405         if (*c == ' ') {
406                 /* command for focus */
407                 call(c+1, ci->focus, 0, ci->mark);
408                 return 1;
409         }
410
411         call("Keystroke-sequence", home, 0, NULL, c);
412         return 1;
413 }
414
415 DEF_CMD(basic_abort)
416 {
417         /* On abort, forget mark */
418         struct mark *m = call_ret(mark2, "doc:point", ci->focus);
419
420         clear_selection(ci->focus, NULL, m, 0);
421         return Efallthrough;
422 }
423
424 DEF_CMD(basic_sel_claimed)
425 {
426         /* Should possibly just change the color of our selection */
427         struct mark *mk = call_ret(mark2, "doc:point", ci->focus);
428
429         clear_selection(ci->focus, NULL, mk, 0);
430         return 1;
431 }
432
433 DEF_CMD(basic_sel_commit)
434 {
435         struct mark *mk = call_ret(mark2, "doc:point", ci->focus);
436         struct mark *p = call_ret(mark, "doc:point", ci->focus);
437
438         if (p && mk && !mark_same(p, mk)) {
439                 char *str;
440
441                 str = call_ret(strsave, "doc:get-str", ci->focus,
442                                0, p, NULL,
443                                0, mk);
444                 if (str && *str)
445                         call("copy:save", ci->focus, 0, NULL, str);
446         }
447
448         return 1;
449 }
450
451 DEF_CMD(basic_insert)
452 {
453         int ret;
454         const char *str;
455         struct mark *mk = call_ret(mark2, "doc:point", ci->focus);
456         char dc[20];
457         bool first = N2(ci) != N2_undo_insert;
458
459         if (!ci->mark)
460                 return Enoarg;
461
462         if (clear_selection(ci->focus, NULL, mk, 3)) {
463                 call("Replace", ci->focus, 1, mk, NULL, !first);
464                 first = False;
465         } else
466                 clear_selection(ci->focus, NULL, mk, 2);
467
468         str = ksuffix(ci, "K-");
469         /* Resubmit as doc:char-$str.  By default this will be inserted
470          * but panes like lib-viewer might have other plans.
471          * lib-viewer could catch the original "K-", but sometimes
472          * the major mode might not want that.
473          */
474         strcat(strcpy(dc, "doc:char-"), str);
475         ret = call(dc, ci->focus, ci->num, ci->mark, NULL, !first);
476         call("Mode:set-num2", ci->focus, N2_undo_insert);
477
478         return ret < 0 ? ret : 1;
479 }
480
481 static struct {
482         char *key;
483         char *insert;
484 } other_inserts[] = {
485         {"K:Tab", "\t"},
486         {"K:LF", "\n"},
487         {"K:Enter", "\n"},
488         {NULL, NULL}
489 };
490
491 DEF_CMD(basic_insert_other)
492 {
493         int ret;
494         int i;
495         struct mark *m = NULL;
496         struct mark *mk = call_ret(mark2, "doc:point", ci->focus);
497         bool first = N2(ci) != N2_undo_insert;
498         char *ins;
499
500         if (!ci->mark)
501                 return Enoarg;
502
503         for (i = 0; other_inserts[i].key; i++)
504                 if (strcmp(safe_cast other_inserts[i].key, ci->key) == 0)
505                         break;
506         ins = other_inserts[i].insert;
507         if (ins == NULL)
508                 return Efallthrough;
509
510         if (clear_selection(ci->focus, NULL, mk, 3)) {
511                 call("Replace", ci->focus, 1, mk, NULL, !first);
512                 first = False;
513         } else
514                 clear_selection(ci->focus, NULL, mk, 2);
515
516         if (!*ins) {
517                 ins++;
518                 m = mark_dup(ci->mark);
519                 /* Move m before ci->mark, so it doesn't move when we insert */
520                 mark_step(m, 0);
521         }
522
523         ret = call("Replace", ci->focus, 1, m, ins, !first, ci->mark);
524         if (m) {
525                 mark_to_mark(ci->mark, m);
526                 mark_free(m);
527         }
528         /* A newline starts a new undo */
529         call("Mode:set-num2", ci->focus, (*ins == '\n') ? 0 : N2_undo_insert);
530         return ret < 0 ? ret : 1;
531 }
532
533 DEF_CMD(basic_interactive_insert)
534 {
535         /* If some pane want to insert text just like it was typed,
536          * it calls this, and we set up for proper undo
537          */
538         int ret;
539         bool first = N2(ci) != N2_undo_insert;
540
541         if (!ci->str)
542                 return Enoarg;
543
544         if (clear_selection(ci->focus, NULL, ci->mark, 3)) {
545                 call("Replace", ci->focus, 1, ci->mark, NULL, !first);
546                 first = False;
547         } else
548                 clear_selection(ci->focus, NULL, ci->mark, 2);
549         ret = call("Replace", ci->focus, 1, ci->mark, ci->str,
550                    !first);
551         call("Mode:set-num2", ci->focus,
552              strchr(ci->str, '\n') ? 0 : N2_undo_insert);
553         return ret < 0 ? ret : 1;
554 }
555
556 DEF_CMD(basic_interactive_delete)
557 {
558         /* If some pane want to delete text just like backspace was typed,
559          * it calls this, and we set up for proper undo
560          */
561         int ret;
562
563         if (!ci->str)
564                 return Enoarg;
565         ret = call("Replace", ci->focus, 1, ci->mark, "",
566                    N2(ci) == N2_undo_insert, ci->mark2);
567         call("Mode:set-num2", ci->focus,
568              strchr(ci->str, '\n') ? 0 : N2_undo_delete);
569         return ret < 0 ? ret : 1;
570 }
571
572 DEF_CMD(basic_close)
573 {
574         call("Tile:close", ci->focus);
575         return 1;
576 }
577
578 DEF_CMD(basic_refresh)
579 {
580         call("Window:refresh", ci->focus);
581         return 1;
582 }
583
584 static struct map *basic_map;
585 DEF_LOOKUP_CMD(mode_basic, basic_map);
586
587 DEF_PFX_CMD(help_cmd, ":Help");
588
589 static void basic_init(void)
590 {
591         struct map *m;
592
593         m = key_alloc();
594
595         /* Some Function keys that CUA defines */
596         key_add(m, "K:F1", &help_cmd.c);
597         key_add(m, "K:F4", &basic_close);
598         key_add(m, "K:F5", &basic_refresh);
599
600         key_add_range(m, "K- ", "K-~", &basic_insert);
601         key_add_range(m, "K-\200", "K-\377\377\377\377", &basic_insert);
602         key_add(m, "K:Tab", &basic_insert_other);
603         //key_add(m, "K:LF", &basic_insert_other);
604         key_add(m, "K:Enter", &basic_insert_other);
605         key_add(m, "Interactive:insert", &basic_interactive_insert);
606         key_add(m, "Interactive:delete", &basic_interactive_delete);
607
608         key_add(m, "M:Press-1", &basic_press);
609         key_add(m, "M:Release-1", &basic_release);
610         key_add(m, "M:Press-3", &basic_menu_open);
611         key_add(m, "M:Release-3", &basic_menu_select);
612         key_add(m, "M:DPress-1", &basic_press);
613         key_add(m, "M:Motion", &basic_motion);
614         key_add(m, "K:Paste", &basic_paste_direct);
615         key_add(m, "M:Paste", &basic_paste_direct);
616
617         key_add(m, "Notify:selection:claimed", &basic_sel_claimed);
618         key_add(m, "Notify:selection:commit", &basic_sel_commit);
619
620         key_add(m, "map-attr", &basic_attrs);
621         key_add(m, "emacs:selection-menu", &basic_selection_menu);
622         key_add(m, "emacs:selection-menu-action", &basic_selection_menu_action);
623
624         key_add(m, "selection:set", &basic_selection_set);
625         key_add(m, "selection:clear", &basic_selection_clear);
626
627         key_add(m, "Abort", &basic_abort);
628         key_add(m, "Cancel", &basic_abort);
629
630         basic_map = m;
631 }
632
633 DEF_CMD(attach_mode_basic)
634 {
635         struct pane *p = pane_register(ci->focus, 0, &mode_basic.c);
636
637         if (!p)
638                 return Efail;
639         comm_call(ci->comm2, "cb", p);
640         return 1;
641 }
642
643 void edlib_init(struct pane *ed safe)
644 {
645         basic_init();
646         call_comm("global-set-command", ed, &attach_mode_basic,
647                   0, NULL, "attach-mode-basic");
648 }