]> git.neil.brown.name Git - edlib.git/blob - mode-basic.c
Create mode-basic.
[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, NULL, 0, NULL, NULL, ci->x, ci->y);
187
188         clear_selection(ci->focus, pt, mk, 0);
189         call("Move-to", ci->focus, 0, m);
190         pane_take_focus(ci->focus);
191
192         if (m2 && strcmp(ci->key, "M:DPress-1") == 0) {
193                 type = attr_find(m2->attrs, "emacs:selection-type");
194                 if (!type)
195                         type = "char";
196                 else if (strcmp(type, "char") == 0)
197                         type = "word";
198                 else if (strcmp(type, "word") == 0)
199                         type = "line";
200                 else
201                         type = "char";
202         } else {
203                 type = "char";
204                 /* Record start of selection */
205                 call("Move-to", ci->focus, 2, m);
206                 m2 = call_ret(mark2, "doc:point", ci->focus, 2);
207                 if (m2)
208                         attr_set_str(&m2->attrs, "emacs:selection-type", type);
209         }
210         if (m2) {
211                 /* Record co-ordinate of start so we can tell if the mouse moved. */
212                 attr_set_int(&m2->attrs, "emacs:track-selection",
213                              1 + ci->x * 10000 + ci->y);
214                 update_sel(ci->focus, pt, m2, type);
215         }
216         mark_free(m);
217
218         return 1;
219 }
220
221 DEF_CMD(basic_release)
222 {
223         struct mark *p = call_ret(mark, "doc:point", ci->focus);
224         struct mark *mk = call_ret(mark2, "doc:point", ci->focus);
225         struct mark *m2 = call_ret(mark2, "doc:point", ci->focus, 2);
226         struct mark *m = mark_new(ci->focus);
227         char *type;
228         int prev_pos;
229         int moved;
230
231         if (!p || !m2 || !m) {
232                 /* Not in a document or no selection start - not my problem */
233                 mark_free(m);
234                 return Efallthrough;
235         }
236
237         prev_pos = attr_find_int(m2->attrs, "emacs:track-selection");
238         type = attr_find(m2->attrs, "emacs:selection-type");
239         moved = prev_pos != (1 + ci->x * 10000 + ci->y);
240         attr_set_int(&m2->attrs, "emacs:track-selection", 0);
241
242         call("Move-CursorXY", ci->focus,
243              0, m, "activate", 0, NULL, NULL, ci->x, ci->y);
244         /* That action might have closed a pane.  Better check... */
245         if (ci->focus->damaged & DAMAGED_CLOSED) {
246                 /* Do nothing */
247         } else if (moved) {
248                 /* Moved the mouse, so new location is point */
249                 call("Move-to", ci->focus, 0, m);
250                 update_sel(ci->focus, p, m2, NULL);
251         } else if (type && strcmp(type, "char") != 0) {
252                 /* Otherwise use the old location.  Point might not
253                  * be there exactly if it was moved to end of word/line
254                  */
255                 call("Move-to", ci->focus, 0, m2);
256                 update_sel(ci->focus, p, m2, NULL);
257         } else
258                 clear_selection(ci->focus, p, mk, 0);
259
260         mark_free(m);
261
262         return 1;
263 }
264
265 DEF_CMD(basic_menu_open)
266 {
267         /* If there is a menu action here, activate it. */
268         /* Don't move the cursor though */
269         struct mark *m = mark_new(ci->focus);
270         int ret;
271
272         ret = call("Move-CursorXY", ci->focus, 0, m, "menu",
273                    0, NULL, NULL, ci->x, ci->y);
274         mark_free(m);
275         return ret;
276 }
277
278 DEF_CMD(basic_menu_select)
279 {
280         /* If a menu was opened it should have claimed the mouse focus
281          * so ci->focus is now the menu.  We want to activate the entry
282          * under the mouse
283          */
284         struct mark *m = mark_new(ci->focus);
285         int ret;
286
287         ret = call("Move-CursorXY", ci->focus, 0, m, "activate",
288                    0, NULL, NULL, ci->x, ci->y);
289         mark_free(m);
290         return ret;
291 }
292
293 DEF_CMD(basic_motion)
294 {
295         struct mark *p = call_ret(mark, "doc:point", ci->focus);
296         struct mark *m2 = call_ret(mark2, "doc:point", ci->focus, 2);
297
298         if (!p || !m2)
299                 return Enoarg;
300
301         if (attr_find_int(m2->attrs, "emacs:track-selection") <= 0)
302                 return Efallthrough;
303
304         call("Move-CursorXY", ci->focus,
305              0, NULL, NULL, 0, NULL, NULL, ci->x, ci->y);
306
307         update_sel(ci->focus, p, m2, NULL);
308         return 1;
309 }
310
311 DEF_CMD(basic_paste_direct)
312 {
313         /* This command is an explicit paste command and the content
314          * is available via "Paste:get".
315          * It might come via the mouse (with x,y) or via a keystroke.
316          */
317         char *s;
318         if (ci->key[0] == 'M') {
319                 call("Move-CursorXY", ci->focus,
320                      0, NULL, NULL, 0, NULL, NULL, ci->x, ci->y);
321                 pane_take_focus(ci->focus);
322         }
323
324         s = call_ret(str, "Paste:get", ci->focus);
325         if (s && *s) {
326                 struct mark *pt = call_ret(mark, "doc:point", ci->focus);
327                 struct mark *mk;
328                 call("Move-to", ci->focus, 1);
329                 mk = call_ret(mark2, "doc:point", ci->focus);
330                 call("Replace", ci->focus, 0, mk, s, 0, pt);
331                 set_selection(ci->focus, pt, mk, 2);
332         }
333         free(s);
334         return 1;
335 }
336
337
338 DEF_CMD(basic_attrs)
339 {
340         struct call_return cr;
341         int active;
342         char *selection = "bg:white-80,vis-nl,menu-at-mouse,action-menu:emacs:selection-menu"; // grey
343
344         if (!ci->str)
345                 return Enoarg;
346
347         cr = call_ret(all, "doc:point", ci->focus);
348         if (cr.ret <= 0 || !cr.m || !cr.m2 || !ci->mark)
349                 return 1;
350         active = attr_find_int(cr.m2->attrs, "selection:active");
351         if (active <= 0)
352                 return 1;
353         if (active >= 3) /* replacable */
354                 selection = "bg:red+80,vis-nl"; // pink
355         if (mark_same(cr.m, cr.m2))
356                 return 1;
357         if (strcmp(ci->str, "render:interactive-mark") == 0) {
358                 if (ci->mark == cr.m2 && cr.m2->seq < cr.m->seq)
359                         return comm_call(ci->comm2, "attr:callback", ci->focus, 0,
360                                          ci->mark, selection, 210);
361                 if (ci->mark == cr.m2)
362                         return comm_call(ci->comm2, "attr:callback", ci->focus, -1,
363                                          ci->mark, selection, 210);
364         }
365         if (strcmp(ci->str, "render:interactive-point") == 0) {
366                 if (cr.m == ci->mark && cr.m->seq < cr.m2->seq)
367                         return comm_call(ci->comm2, "attr:cb", ci->focus, 0,
368                                          ci->mark, selection, 210);
369                 if (cr.m == ci->mark)
370                         return comm_call(ci->comm2, "attr:callback", ci->focus, -1,
371                                          ci->mark, selection, 210);
372         }
373         if (strcmp(ci->str, "start-of-line") == 0) {
374                 if ((cr.m->seq < ci->mark->seq && ci->mark->seq < cr.m2->seq &&
375                      !mark_same(ci->mark, cr.m2)) ||
376                     (cr.m2->seq < ci->mark->seq && ci->mark->seq < cr.m->seq &&
377                      !mark_same(ci->mark, cr.m)))
378                         return comm_call(ci->comm2, "attr:cb", ci->focus, 0,
379                                          ci->mark, selection, 210);
380         }
381         return Efallthrough;
382 }
383
384 DEF_CMD(basic_selection_menu)
385 {
386         struct pane *p;
387
388         p = call_ret(pane, "attach-menu", ci->focus, 0, NULL, "V", 0, NULL,
389                      "emacs:selection-menu-action", ci->x, ci->y+1);
390         if (!p)
391                 return Efail;
392         call("global-multicall-selection-menu:add-", p);
393         call("menu-add", p, 0, NULL, "de-select", 0, NULL, ":ESC");
394         return 1;
395 }
396
397 DEF_CMD(basic_selection_menu_action)
398 {
399         struct pane *home = ci->home;
400         const char *c = ci->str;
401
402         if (!c)
403                 return 1;
404         if (*c == ' ') {
405                 /* command for focus */
406                 call(c+1, ci->focus, 0, ci->mark);
407                 return 1;
408         }
409
410         call("Keystroke-sequence", home, 0, NULL, c);
411         return 1;
412 }
413
414 DEF_CMD(basic_abort)
415 {
416         /* On abort, forget mark */
417         struct mark *m = call_ret(mark2, "doc:point", ci->focus);
418
419         clear_selection(ci->focus, NULL, m, 0);
420         return Efallthrough;
421 }
422
423 DEF_CMD(basic_sel_claimed)
424 {
425         /* Should possibly just change the color of our selection */
426         struct mark *mk = call_ret(mark2, "doc:point", ci->focus);
427
428         clear_selection(ci->focus, NULL, mk, 0);
429         return 1;
430 }
431
432 DEF_CMD(basic_sel_commit)
433 {
434         struct mark *mk = call_ret(mark2, "doc:point", ci->focus);
435         struct mark *p = call_ret(mark, "doc:point", ci->focus);
436
437         if (p && mk && !mark_same(p, mk)) {
438                 char *str;
439
440                 str = call_ret(strsave, "doc:get-str", ci->focus,
441                                0, p, NULL,
442                                0, mk);
443                 if (str && *str)
444                         call("copy:save", ci->focus, 0, NULL, str);
445         }
446
447         return 1;
448 }
449
450 DEF_CMD(basic_insert)
451 {
452         int ret;
453         const char *str;
454         struct mark *mk = call_ret(mark2, "doc:point", ci->focus);
455         char dc[20];
456         bool first = N2(ci) != N2_undo_insert;
457
458         if (!ci->mark)
459                 return Enoarg;
460
461         if (clear_selection(ci->focus, NULL, mk, 3)) {
462                 call("Replace", ci->focus, 1, mk, NULL, !first);
463                 first = False;
464         } else
465                 clear_selection(ci->focus, NULL, mk, 2);
466
467         str = ksuffix(ci, "K-");
468         /* Resubmit as doc:char-$str.  By default this will be inserted
469          * but panes like lib-viewer might have other plans.
470          * lib-viewer could catch the original "K-", but sometimes
471          * the major mode might not want that.
472          */
473         strcat(strcpy(dc, "doc:char-"), str);
474         ret = call(dc, ci->focus, ci->num, ci->mark, NULL, !first);
475         call("Mode:set-num2", ci->focus, N2_undo_insert);
476
477         return ret < 0 ? ret : 1;
478 }
479
480 static struct {
481         char *key;
482         char *insert;
483 } other_inserts[] = {
484         {"K:Tab", "\t"},
485         {"K:LF", "\n"},
486         {"K:Enter", "\n"},
487         {NULL, NULL}
488 };
489
490 DEF_CMD(basic_insert_other)
491 {
492         int ret;
493         int i;
494         struct mark *m = NULL;
495         struct mark *mk = call_ret(mark2, "doc:point", ci->focus);
496         bool first = N2(ci) != N2_undo_insert;
497         char *ins;
498
499         if (!ci->mark)
500                 return Enoarg;
501
502         for (i = 0; other_inserts[i].key; i++)
503                 if (strcmp(safe_cast other_inserts[i].key, ci->key) == 0)
504                         break;
505         ins = other_inserts[i].insert;
506         if (ins == NULL)
507                 return Efallthrough;
508
509         if (clear_selection(ci->focus, NULL, mk, 3)) {
510                 call("Replace", ci->focus, 1, mk, NULL, !first);
511                 first = False;
512         } else
513                 clear_selection(ci->focus, NULL, mk, 2);
514
515         if (!*ins) {
516                 ins++;
517                 m = mark_dup(ci->mark);
518                 /* Move m before ci->mark, so it doesn't move when we insert */
519                 mark_step(m, 0);
520         }
521
522         ret = call("Replace", ci->focus, 1, m, ins, !first, ci->mark);
523         if (m) {
524                 mark_to_mark(ci->mark, m);
525                 mark_free(m);
526         }
527         /* A newline starts a new undo */
528         call("Mode:set-num2", ci->focus, (*ins == '\n') ? 0 : N2_undo_insert);
529         return ret < 0 ? ret : 1;
530 }
531
532 DEF_CMD(basic_interactive_insert)
533 {
534         /* If some pane want to insert text just like it was typed,
535          * it calls this, and we set up for proper undo
536          */
537         int ret;
538         bool first = N2(ci) != N2_undo_insert;
539
540         if (!ci->str)
541                 return Enoarg;
542
543         if (clear_selection(ci->focus, NULL, ci->mark, 3)) {
544                 call("Replace", ci->focus, 1, ci->mark, NULL, !first);
545                 first = False;
546         } else
547                 clear_selection(ci->focus, NULL, ci->mark, 2);
548         ret = call("Replace", ci->focus, 1, ci->mark, ci->str,
549                    !first);
550         call("Mode:set-num2", ci->focus,
551              strchr(ci->str, '\n') ? 0 : N2_undo_insert);
552         return ret < 0 ? ret : 1;
553 }
554
555 DEF_CMD(basic_interactive_delete)
556 {
557         /* If some pane want to delete text just like backspace was typed,
558          * it calls this, and we set up for proper undo
559          */
560         int ret;
561
562         if (!ci->str)
563                 return Enoarg;
564         ret = call("Replace", ci->focus, 1, ci->mark, "",
565                    N2(ci) == N2_undo_insert, ci->mark2);
566         call("Mode:set-num2", ci->focus,
567              strchr(ci->str, '\n') ? 0 : N2_undo_delete);
568         return ret < 0 ? ret : 1;
569 }
570
571 static struct map *basic_map;
572 DEF_LOOKUP_CMD(mode_basic, basic_map);
573
574 DEF_PFX_CMD(help_cmd, ":Help");
575
576 static void basic_init(void)
577 {
578         struct map *m;
579
580         m = key_alloc();
581
582         key_add(m, "K:F1", &help_cmd.c);
583
584         key_add_range(m, "K- ", "K-~", &basic_insert);
585         key_add_range(m, "K-\200", "K-\377\377\377\377", &basic_insert);
586         key_add(m, "K:Tab", &basic_insert_other);
587         //key_add(m, "K:LF", &basic_insert_other);
588         key_add(m, "K:Enter", &basic_insert_other);
589         key_add(m, "Interactive:insert", &basic_interactive_insert);
590         key_add(m, "Interactive:delete", &basic_interactive_delete);
591
592         key_add(m, "M:Press-1", &basic_press);
593         key_add(m, "M:Release-1", &basic_release);
594         key_add(m, "M:Press-3", &basic_menu_open);
595         key_add(m, "M:Release-3", &basic_menu_select);
596         key_add(m, "M:DPress-1", &basic_press);
597         key_add(m, "M:Motion", &basic_motion);
598         key_add(m, "K:Paste", &basic_paste_direct);
599         key_add(m, "M:Paste", &basic_paste_direct);
600
601         key_add(m, "Notify:selection:claimed", &basic_sel_claimed);
602         key_add(m, "Notify:selection:commit", &basic_sel_commit);
603
604         key_add(m, "map-attr", &basic_attrs);
605         key_add(m, "emacs:selection-menu", &basic_selection_menu);
606         key_add(m, "emacs:selection-menu-action", &basic_selection_menu_action);
607
608         key_add(m, "selection:set", &basic_selection_set);
609         key_add(m, "selection:clear", &basic_selection_clear);
610
611         key_add(m, "Abort", &basic_abort);
612         key_add(m, "Cancel", &basic_abort);
613
614         basic_map = m;
615 }
616
617 DEF_CMD(attach_mode_basic)
618 {
619         struct pane *p = pane_register(ci->focus, 0, &mode_basic.c);
620
621         if (!p)
622                 return Efail;
623         comm_call(ci->comm2, "cb", p);
624         return 1;
625 }
626
627 void edlib_init(struct pane *ed safe)
628 {
629         basic_init();
630         call_comm("global-set-command", ed, &attach_mode_basic,
631                   0, NULL, "attach-mode-basic");
632 }