]> git.neil.brown.name Git - edlib.git/blob - lib-history.c
TODO: clean out done items.
[edlib.git] / lib-history.c
1 /*
2  * Copyright Neil Brown ©2016-2023 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * history
6  *
7  * A history pane supports selection of lines from a separate
8  * document.  The underlying document is assumed to be one line
9  * and this line can be replaced by various lines from the history document.
10  * When a line is replaced, if it had been modified, it is saved first so it
11  * can be revisited when "down" movement gets back to the end.
12  * When a selection is committed (:Enter), it is added to end of history.
13  * :A-p - replace current line with previous line from history, if there is one
14  * :A-n - replace current line with next line from history.  If none, restore
15  *        saved line
16  * :A-r - enter incremental search, looking back
17  * :A-s - enter incremental search, looking forward
18  *
19  * In incremental search mode the current search string appears in the
20  * prompt and:
21  *   -glyph appends to the search string and repeats search from start
22  *          in current direction
23  *   :Backspace strips a glyph and repeats search
24  *   :A-r - sets prev line as search start and repeats search
25  *   :A-s - sets next line as search start and repeats.
26  *   :Enter - drops out of search mode
27  * Anything else drops out of search mode and repeats the command as normal
28  *
29  * For each history document a number of "favourites" can be registered.
30  * These are accessed by moving "down" from the start point rather than "up"
31  * for previous history items.
32  */
33
34 #include <unistd.h>
35 #include <stdlib.h>
36 #include <stdio.h>
37 #include <string.h>
38 #include <ctype.h>
39
40 #define PANE_DATA_TYPE struct history_info
41 #include "core.h"
42 #include "misc.h"
43
44 struct history_info {
45         struct pane     *history;
46         char            *saved;
47         char            *prompt;
48         struct buf      search;
49         int             search_back;
50         int             favourite;
51         struct si {
52                 int i;
53                 struct si *prev;
54                 struct mark *line;
55         } *prev;
56         int             changed;
57 };
58 #include "core-pane.h"
59
60 static struct map *history_map;
61 DEF_LOOKUP_CMD(history_handle, history_map);
62
63 static void free_si(struct si **sip safe)
64 {
65         struct si *i;;
66
67         while ((i = *sip) != NULL) {
68                 *sip = i->prev;
69                 if (i->prev == NULL || i->prev->line != i->line)
70                         mark_free(i->line);
71                 free(i);
72         }
73 }
74
75 DEF_CMD_CLOSED(history_close)
76 {
77         struct history_info *hi = ci->home->data;
78
79         free_si(&hi->prev);
80         if (hi->history)
81                 pane_close(hi->history);
82         free(hi->search.b);
83         free(hi->saved);
84         free(hi->prompt);
85         return 1;
86 }
87
88 DEF_CMD(history_notify_close)
89 {
90         struct history_info *hi = ci->home->data;
91
92         if (ci->focus == hi->history) {
93                 /* The history document is going away!!! */
94                 free_si(&hi->prev);
95                 hi->history = NULL;
96         }
97         return 1;
98 }
99
100 DEF_CMD(history_save)
101 {
102         struct history_info *hi = ci->home->data;
103         const char *eol;
104         const char *line = ci->str;
105         const char *prev;
106
107         if (!hi->history || !ci->str)
108                 /* history document was destroyed */
109                 return 1;
110         /* Must never include a newline in a history entry! */
111         eol = strchr(ci->str, '\n');
112         if (eol)
113                 line = strnsave(ci->home, ci->str, eol - ci->str);
114
115         prev = call_ret(strsave, "history:get-last", ci->focus);
116         if (prev && line && strcmp(prev, line) == 0)
117                 return 1;
118
119         call("doc:file", hi->history, 1);
120         call("Replace", hi->history, 1, NULL, line);
121         call("Replace", hi->history, 1, NULL, "\n", 1);
122         return 1;
123 }
124
125 DEF_CMD(history_done)
126 {
127         history_save_func(ci);
128         return Efallthrough;
129 }
130
131 DEF_CMD(history_notify_replace)
132 {
133         struct history_info *hi = ci->home->data;
134
135         if (hi->history)
136                 hi->changed = 1;
137         return 1;
138 }
139
140 static void recall_line(struct pane *p safe, struct pane *focus safe, int fore)
141 {
142         struct history_info *hi = p->data;
143         struct mark *m;
144         char *l, *e;
145
146         if (!hi->history)
147                 return;
148         m = mark_at_point(hi->history, NULL, MARK_UNGROUPED);
149         call("doc:EOL", hi->history, 1, m, NULL, 1);
150         l = call_ret(str, "doc:get-str", hi->history, 0, NULL, NULL, 0, m);
151         mark_free(m);
152         if (!l || !*l) {
153                 /* No more history */
154                 free(l);
155                 if (!fore)
156                         return;
157
158                 l = hi->saved;
159         }
160         if (l) {
161                 e = strchr(l, '\n');
162                 if (e)
163                         *e = 0;
164         }
165         call("doc:EOL", focus, -1);
166         m = mark_at_point(focus, NULL, MARK_UNGROUPED);
167         call("doc:EOL", focus, 1, m);
168         if (hi->changed) {
169                 if (l != hi->saved)
170                         free(hi->saved);
171                 hi->saved = call_ret(str, "doc:get-str", focus,
172                                      0, NULL, NULL,
173                                      0, m);
174         }
175         call("Replace", focus, 1, m, l);
176         if (l != hi->saved){
177                 free(l);
178                 hi->changed = 0;
179         }
180         mark_free(m);
181 }
182
183 DEF_CMD(history_move)
184 {
185         struct history_info *hi = ci->home->data;
186         const char *suffix = ksuffix(ci, "K:A-");
187         char attr[sizeof("doc:favourite-") + 12];
188
189         if (!hi->history)
190                 return Enoarg;
191         if (*suffix == 'p') {
192                 if (hi->favourite > 0)
193                         hi->favourite -= 1;
194                 else
195                         call("doc:EOL", hi->history, -2);
196         } else {
197                 if (hi->favourite > 0)
198                         hi->favourite += 1;
199                 else if (call("doc:EOL", hi->history, 1, NULL, NULL, 1) < 0)
200                         hi->favourite = 1;
201         }
202         while (hi->favourite > 0) {
203                 char *f;
204                 struct mark *m;
205                 snprintf(attr, sizeof(attr)-1, "doc:favourite-%d",
206                          hi->favourite);
207                 f = pane_attr_get(hi->history, attr);
208                 if (!f) {
209                         hi->favourite -= 1;
210                         continue;
211                 }
212                 call("doc:EOL", ci->focus, -1);
213                 m = mark_at_point(ci->focus, NULL, MARK_UNGROUPED);
214                 call("doc:EOL", ci->focus, 1, m);
215                 call("Replace", ci->focus, 1, m, f);
216                 mark_free(m);
217                 return 1;
218         }
219         recall_line(ci->home, ci->focus, *suffix == 'n');
220         return 1;
221 }
222
223 DEF_CMD(history_add_favourite)
224 {
225         struct history_info *hi = ci->home->data;
226         char attr[sizeof("doc:favourite-") + 10];
227         int f;
228         char *l;
229
230         if (!hi->history)
231                 return 1;
232         l = call_ret(strsave, "doc:get-str", ci->focus);
233         if (!l || !*l)
234                 return 1;
235         for (f = 1; f < 100; f++) {
236                 snprintf(attr, sizeof(attr)-1, "doc:favourite-%d", f);
237                 if (pane_attr_get(hi->history, attr))
238                         continue;
239                 call("doc:set:", hi->history, 0, NULL, l, 0, NULL, attr);
240                 call("Message:modal", ci->focus, 0, NULL, "Added as favourite");
241                 break;
242         }
243         return 1;
244 }
245
246 DEF_CMD(history_attach)
247 {
248         struct history_info *hi;
249         struct pane *p, *history;
250
251         if (!ci->str)
252                 return Enoarg;
253
254         p = call_ret(pane, "docs:byname", ci->focus, 0, NULL, ci->str);
255         if (!p)
256                 p = call_ret(pane, "doc:from-text", ci->focus, 0, NULL, ci->str);
257         if (!p)
258                 return Efail;
259
260         history = call_ret(pane, "doc:attach-view", p, -1, NULL, "invisible");
261         if (!history)
262                 return Efail;
263         call("doc:file", history, 1);
264         p = pane_register(ci->focus, 0, &history_handle.c);
265         if (!p) {
266                 pane_free(history); // FIXME should I send a close message?
267                 return Efail;
268         }
269         hi = p->data;
270         hi->history = history;
271         buf_init(&hi->search);
272         buf_concat(&hi->search, "?0"); /* remaining chars are searched verbatim */
273         pane_add_notify(p, hi->history, "Notify:Close");
274         call("doc:request:doc:replaced", p);
275         return comm_call(ci->comm2, "callback:attach", p);
276 }
277
278 DEF_CMD(history_hlast)
279 {
280         struct history_info *hi = ci->home->data;
281         struct pane *doc = hi->history;
282         struct mark *m, *m2;
283         int rv;
284
285         if (!doc)
286                 return Einval;
287
288         m = mark_new(doc);
289         if (!m)
290                 return 1;
291         call("doc:set-ref", doc, 0, m);
292         call("doc:set", doc, 0, m, NULL, 1);
293         doc_prev(doc,m);
294         m2 = mark_dup(m);
295         while (doc_prior(doc, m) != '\n')
296                 if (doc_prev(doc,m) == WEOF)
297                         break;
298         rv = call_comm("doc:get-str", doc, ci->comm2, 0, m, NULL, 0, m2);
299         mark_free(m);
300         mark_free(m2);
301         return rv;
302 }
303
304 static bool has_name(struct pane *doc safe, struct mark *m safe,
305                      const char *name safe)
306 {
307         char *a;
308
309         a = call_ret(strsave, "doc:get-attr", doc, 0, m, "history:name");
310         return a && strcmp(a, name) == 0;
311 }
312
313 DEF_CMD(history_last)
314 {
315         /* Get last line from the given history document
316          * If ci->num > 1 get nth last line
317          * else if ci->str, get the line with given name
318          * If both set, assign str to the nth last line
319          * Names are assign with attribute "history:name"
320          */
321         struct pane *doc;
322         struct mark *m, *m2;
323         int num = ci->num;
324         const char *name = ci->str2;
325         int rv;
326
327         doc = call_ret(pane, "docs:byname", ci->focus, 0, NULL, ci->str);
328         if (!doc)
329                 return 1;
330         m = mark_new(doc);
331         if (!m)
332                 return 1;
333         call("doc:set-ref", doc, 0, m);
334         call("doc:set", doc, 0, m, NULL, 1);
335         do {
336                 doc_prev(doc,m);
337                 m2 = mark_dup(m);
338                 while (doc_prior(doc, m) != '\n')
339                         if (doc_prev(doc,m) == WEOF)
340                                 break;
341         } while (!mark_same(m, m2) && num > 1 &&
342                  (name == NULL || has_name(doc, m, name)));
343         if (mark_same(m, m2) || num > 1)
344                 rv = Efail;
345         else {
346                 if (num == 1 && name)
347                         call("doc:set-attr", doc, 0, m, "history:name",
348                              0, NULL, name);
349                 rv = call_comm("doc:get-str", doc, ci->comm2,
350                                0, m, NULL, 0, m2);
351         }
352         mark_free(m);
353         mark_free(m2);
354         return rv;
355 }
356
357 DEF_CMD(history_search)
358 {
359         struct history_info *hi = ci->home->data;
360         char *prompt, *prefix;
361
362         if (!hi->history)
363                 return 1;
364         call("Mode:set-mode", ci->focus, 0, NULL, ":History-search");
365         buf_reinit(&hi->search);
366         buf_concat(&hi->search, "?0");
367         free_si(&hi->prev);
368         prompt = pane_attr_get(ci->focus, "prompt");
369         if (!prompt)
370                 prompt = "?";
371         free(hi->prompt);
372         hi->prompt = strdup(prompt);
373         prefix = strconcat(ci->focus, prompt, " (): ");
374         attr_set_str(&ci->focus->attrs, "prefix", prefix);
375         call("view:changed", ci->focus);
376
377         hi->search_back = (toupper(ci->key[4]) == 'R');
378         return 1;
379 }
380
381 static void update_search(struct pane *p safe, struct pane *focus safe,
382                           int offset)
383 {
384         struct history_info *hi = p->data;
385         struct si *i;
386         struct mark *m;
387         const char *prefix;
388         int ret;
389
390         if (!hi->history)
391                 return;
392         if (offset >= 0) {
393                 alloc(i, pane);
394                 i->i = offset;
395                 i->line = mark_at_point(hi->history, NULL, MARK_UNGROUPED);
396                 i->prev = hi->prev;
397                 hi->prev = i;
398         }
399         prefix = strconcat(focus, hi->prompt?:"?",
400                            " (", buf_final(&hi->search)+2, "): ");
401         attr_set_str(&focus->attrs, "prefix", prefix);
402         call("view:changed", focus);
403         call("Mode:set-mode", focus, 0, NULL, ":History-search");
404         m = mark_at_point(hi->history, NULL, MARK_UNGROUPED);
405         /* Alway search backwards from the end-of-line of last match */
406         call("doc:EOL", hi->history, 1, m);
407         ret = call("text-search", hi->history, 1, m, buf_final(&hi->search),
408                    hi->search_back);
409         if (ret <= 0) {
410                 // clear line
411                 mark_free(m);
412                 return;
413         }
414         /* Leave point at start-of-line */
415         call("doc:EOL", hi->history, -1, m);
416         call("Move-to", hi->history, 0, m);
417         mark_free(m);
418         recall_line(p, focus, 0);
419 }
420
421 DEF_CMD(history_search_again)
422 {
423         struct history_info *hi = ci->home->data;
424         const char *k;
425
426         k = ksuffix(ci, "K:History-search-");
427         if (*k) {
428                 int l = hi->search.len;
429                 buf_concat(&hi->search, k);
430                 update_search(ci->home, ci->focus, l);
431         }
432         return 1;
433 }
434
435 DEF_CMD(history_search_retry);
436
437 DEF_CMD(history_search_bs)
438 {
439         struct history_info *hi = ci->home->data;
440         struct si *i = hi->prev;
441
442         if (!i || !hi->history) {
443                 history_search_retry_func(ci);
444                 return 1;
445         }
446
447         call("Mode:set-mode", ci->focus, 0, NULL, ":History-search");
448
449         hi->search.len = i->i;
450         call("Move:to", hi->history, 0, i->line);
451         if (!i->prev || i->line != i->prev->line)
452                 mark_free(i->line);
453         hi->prev = i->prev;
454         free(i);
455         update_search(ci->home, ci->focus, -1);
456         return 1;
457 }
458
459 DEF_CMD(history_search_repeat)
460 {
461         struct history_info *hi = ci->home->data;
462         const char *suffix = ksuffix(ci, "K:History-search:C-");
463
464         if (!hi->history)
465                 return Enoarg;
466         hi->search_back = toupper(*suffix) == 'R';
467         if (hi->search_back)
468                 call("doc:EOL", hi->history, -2);
469         else
470                 call("doc:EOL", hi->history, 1, NULL, NULL, 1);
471
472         update_search(ci->home, ci->focus, hi->search.len);
473         return 1;
474 }
475
476 DEF_CMD(history_search_cancel)
477 {
478         struct history_info *hi = ci->home->data;
479         const char *prefix;
480
481         prefix = strconcat(ci->focus, hi->prompt?:"?", ": ");
482         attr_set_str(&ci->focus->attrs, "prefix", prefix);
483         call("view:changed", ci->focus);
484         return 1;
485 }
486
487 REDEF_CMD(history_search_retry)
488 {
489         struct history_info *hi = ci->home->data;
490         const char *prefix;
491         char *k = strconcat(ci->home, "K", ksuffix(ci, "K:History-search"));
492
493         prefix = strconcat(ci->focus, hi->prompt?:"?", ": ");
494         attr_set_str(&ci->focus->attrs, "prefix", prefix);
495         call("view:changed", ci->focus);
496         return call(k, ci->focus, ci->num, ci->mark, ci->str,
497                     ci->num2, ci->mark2, ci->str2);
498 }
499
500 DEF_CMD(history_add)
501 {
502         const char *docname = ci->str;
503         const char *line = ci->str2;
504         struct pane *doc;
505
506         if (!docname || !line || strchr(line, '\n'))
507                 return Einval;
508         doc = call_ret(pane, "docs:byname", ci->focus, 0, NULL, ci->str);
509         if (!doc) {
510                 doc = call_ret(pane, "doc:from-text", ci->focus,
511                                0, NULL, ci->str);
512                 if (doc)
513                         call("global-multicall-doc:appeared-", doc);
514         }
515         if (!doc)
516                 return Efail;
517         call("doc:replace", doc, 1, NULL, line, 1);
518         call("doc:replace", doc, 1, NULL, "\n", 1);
519         return 1;
520 }
521
522 void edlib_init(struct pane *ed safe)
523 {
524         call_comm("global-set-command", ed, &history_attach, 0, NULL, "attach-history");
525         call_comm("global-set-command", ed, &history_last, 0, NULL, "history:get-last");
526         call_comm("global-set-command", ed, &history_add, 0, NULL, "history:add");
527
528         if (history_map)
529                 return;
530
531         history_map = key_alloc();
532         key_add(history_map, "Close", &history_close);
533         key_add(history_map, "Notify:Close", &history_notify_close);
534         key_add(history_map, "doc:replaced", &history_notify_replace);
535         key_add(history_map, "K:A-p", &history_move);
536         key_add(history_map, "K:A-n", &history_move);
537         key_add(history_map, "K:A-r", &history_search);
538         key_add(history_map, "K:A-s", &history_search);
539         key_add(history_map, "K:A-*", &history_add_favourite);
540         key_add_prefix(history_map, "K:History-search-", &history_search_again);
541         key_add_prefix(history_map, "K:History-search:",
542                        &history_search_retry);
543         key_add(history_map, "K:History-search:Backspace",
544                        &history_search_bs);
545         key_add(history_map, "K:History-search:A-r",
546                        &history_search_repeat);
547         key_add(history_map, "K:History-search:A-s",
548                        &history_search_repeat);
549         key_add(history_map, "K:History-search:Enter",
550                        &history_search_cancel);
551         key_add(history_map, "K:History-search:ESC",
552                        &history_search_cancel);
553         key_add(history_map, "history:save", &history_save);
554         key_add(history_map, "history:get-last", &history_hlast);
555         key_add(history_map, "popup:close", &history_done);
556 }