]> git.neil.brown.name Git - edlib.git/blob - lib-history.c
Update copyright dates.
[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  * :C-r - enter incremental search, looking back
17  * :C-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  *   :C-r - sets prev line as search start and repeats search
25  *   :C-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
30 #include <unistd.h>
31 #include <stdlib.h>
32 #include <string.h>
33
34 #include "core.h"
35 #include "misc.h"
36
37 struct history_info {
38         struct pane     *history;
39         char            *saved;
40         char            *prompt;
41         struct buf      search;
42         int             search_back;
43         struct si {
44                 int i;
45                 struct si *prev;
46                 struct mark *line;
47         } *prev;
48         int             changed;
49         struct map      *done_map;
50         struct lookup_cmd handle;
51 };
52
53 static struct map *history_map;
54 DEF_LOOKUP_CMD(history_handle, history_map);
55
56 static void free_si(struct si **sip safe)
57 {
58         struct si *i;;
59
60         while ((i = *sip) != NULL) {
61                 *sip = i->prev;
62                 if (i->prev == NULL || i->prev->line != i->line)
63                         mark_free(i->line);
64                 free(i);
65         }
66 }
67
68 DEF_CMD(history_close)
69 {
70         struct history_info *hi = ci->home->data;
71
72         free_si(&hi->prev);
73         if (hi->history)
74                 pane_close(hi->history);
75         return 1;
76 }
77
78 DEF_CMD(history_free)
79 {
80         struct history_info *hi = ci->home->data;
81
82         free(hi->search.b);
83         free(hi->saved);
84         free(hi->prompt);
85         unalloc(hi, pane);
86         /* handle was in 'hi' */
87         ci->home->handle = NULL;
88         return 1;
89 }
90
91 DEF_CMD(history_notify_close)
92 {
93         struct history_info *hi = ci->home->data;
94
95         if (ci->focus == hi->history) {
96                 /* The history document is going away!!! */
97                 free_si(&hi->prev);
98                 hi->history = NULL;
99         }
100         return 1;
101 }
102
103 DEF_CMD(history_save)
104 {
105         struct history_info *hi = ci->home->data;
106         const char *eol;
107         const char *line = ci->str;
108         const char *prev;
109
110         if (!hi->history || !ci->str)
111                 /* history document was destroyed */
112                 return 1;
113         /* Must never include a newline in a history entry! */
114         eol = strchr(ci->str, '\n');
115         if (eol)
116                 line = strnsave(ci->home, ci->str, eol - ci->str);
117
118         prev = call_ret(strsave, "history:get-last", ci->focus);
119         if (prev && line && strcmp(prev, line) == 0)
120                 return 1;
121
122         call("doc:file", hi->history, 1);
123         call("Replace", hi->history, 1, NULL, line);
124         call("Replace", hi->history, 1, NULL, "\n", 1);
125         return 1;
126 }
127
128 DEF_CMD(history_done)
129 {
130         history_save_func(ci);
131         return Efallthrough;
132 }
133
134 DEF_CMD(history_notify_replace)
135 {
136         struct history_info *hi = ci->home->data;
137
138         if (hi->history)
139                 hi->changed = 1;
140         return 1;
141 }
142
143 static void recall_line(struct pane *p safe, struct pane *focus safe, int fore)
144 {
145         struct history_info *hi = p->data;
146         struct mark *m;
147         char *l, *e;
148
149         if (!hi->history)
150                 return;
151         m = mark_at_point(hi->history, NULL, MARK_UNGROUPED);
152         call("doc:EOL", hi->history, 1, m, NULL, 1);
153         l = call_ret(str, "doc:get-str", hi->history, 0, NULL, NULL, 0, m);
154         mark_free(m);
155         if (!l || !*l) {
156                 /* No more history */
157                 free(l);
158                 if (!fore)
159                         return;
160
161                 l = hi->saved;
162         }
163         if (l) {
164                 e = strchr(l, '\n');
165                 if (e)
166                         *e = 0;
167         }
168         call("doc:EOL", focus, -1);
169         m = mark_at_point(focus, NULL, MARK_UNGROUPED);
170         call("doc:EOL", focus, 1, m);
171         if (hi->changed) {
172                 if (l != hi->saved)
173                         free(hi->saved);
174                 hi->saved = call_ret(str, "doc:get-str", focus,
175                                      0, NULL, NULL,
176                                      0, m);
177         }
178         call("Replace", focus, 1, m, l);
179         if (l != hi->saved){
180                 free(l);
181                 hi->changed = 0;
182         }
183         mark_free(m);
184 }
185
186 DEF_CMD(history_move)
187 {
188         struct history_info *hi = ci->home->data;
189         const char *suffix = ksuffix(ci, "K:A-");
190
191         if (!hi->history)
192                 return Enoarg;
193         if (*suffix == 'p')
194                 call("doc:EOL", hi->history, -2);
195         else
196                 call("doc:EOL", hi->history, 1, NULL, NULL, 1);
197
198         recall_line(ci->home, ci->focus, *suffix == 'n');
199         return 1;
200 }
201
202 DEF_CMD(history_attach)
203 {
204         struct history_info *hi;
205         struct pane *p;
206
207         if (!ci->str || !ci->str2)
208                 return Enoarg;
209
210         alloc(hi, pane);
211         hi->done_map = key_alloc();
212         hi->handle = history_handle;
213         hi->handle.m = &hi->done_map;
214         key_add_chain(hi->done_map, history_map);
215         key_add(hi->done_map, ci->str2, &history_done);
216         p = call_ret(pane, "docs:byname", ci->focus, 0, NULL, ci->str);
217         if (!p)
218                 p = call_ret(pane, "doc:from-text", ci->focus, 0, NULL, ci->str);
219
220         if (!p) {
221                 free(hi);
222                 return Efail;
223         }
224         hi->history = call_ret(pane, "doc:attach-view", p, -1, NULL, "invisible");
225         if (!hi->history)
226                 return Efail;
227         call("doc:file", hi->history, 1);
228         buf_init(&hi->search);
229         buf_concat(&hi->search, "?0"); /* remaining chars are searched verbatim */
230         p = pane_register(ci->focus, 0, &hi->handle.c, hi);
231         if (!p)
232                 return Efail;
233         pane_add_notify(p, hi->history, "Notify:Close");
234         call("doc:request:doc:replaced", p);
235         return comm_call(ci->comm2, "callback:attach", p);
236 }
237
238 DEF_CMD(history_hlast)
239 {
240         struct history_info *hi = ci->home->data;
241         struct pane *doc = hi->history;
242         struct mark *m, *m2;
243         int rv;
244
245         if (!doc)
246                 return Einval;
247
248         m = mark_new(doc);
249         if (!m)
250                 return 1;
251         call("doc:set-ref", doc, 0, m);
252         call("doc:set", doc, 0, m, NULL, 1);
253         doc_prev(doc,m);
254         m2 = mark_dup(m);
255         while (doc_prior(doc, m) != '\n')
256                 if (doc_prev(doc,m) == WEOF)
257                         break;
258         rv = call_comm("doc:get-str", doc, ci->comm2, 0, m, NULL, 0, m2);
259         mark_free(m);
260         mark_free(m2);
261         return rv;
262 }
263
264 static bool has_name(struct pane *doc safe, struct mark *m safe,
265                      const char *name safe)
266 {
267         char *a;
268
269         a = call_ret(strsave, "doc:get-attr", doc, 0, m, "history:name");
270         return a && strcmp(a, name) == 0;
271 }
272
273 DEF_CMD(history_last)
274 {
275         /* Get last line from the given history document
276          * If ci->num > 1 get nth last line
277          * else if ci->str, get the line with given name
278          * If both set, assign str to the nth last line
279          * Names are assign with attribute "history:name"
280          */
281         struct pane *doc;
282         struct mark *m, *m2;
283         int num = ci->num;
284         const char *name = ci->str2;
285         int rv;
286
287         doc = call_ret(pane, "docs:byname", ci->focus, 0, NULL, ci->str);
288         if (!doc)
289                 return 1;
290         m = mark_new(doc);
291         if (!m)
292                 return 1;
293         call("doc:set-ref", doc, 0, m);
294         call("doc:set", doc, 0, m, NULL, 1);
295         do {
296                 doc_prev(doc,m);
297                 m2 = mark_dup(m);
298                 while (doc_prior(doc, m) != '\n')
299                         if (doc_prev(doc,m) == WEOF)
300                                 break;
301         } while (!mark_same(m, m2) && num > 1 &&
302                  (name == NULL || has_name(doc, m, name)));
303         if (mark_same(m, m2) || num > 1)
304                 rv = Efail;
305         else {
306                 if (num == 1 && name)
307                         call("doc:set-attr", doc, 0, m, "history:name",
308                              0, NULL, name);
309                 rv = call_comm("doc:get-str", doc, ci->comm2,
310                                0, m, NULL, 0, m2);
311         }
312         mark_free(m);
313         mark_free(m2);
314         return rv;
315 }
316
317 DEF_CMD(history_search)
318 {
319         struct history_info *hi = ci->home->data;
320         char *prompt, *prefix;
321
322         if (!hi->history)
323                 return 1;
324         call("Mode:set-mode", ci->focus, 0, NULL, ":History-search");
325         buf_reinit(&hi->search);
326         buf_concat(&hi->search, "?0");
327         free_si(&hi->prev);
328         prompt = pane_attr_get(ci->focus, "prompt");
329         if (!prompt)
330                 prompt = "?";
331         free(hi->prompt);
332         hi->prompt = strdup(prompt);
333         prefix = strconcat(ci->focus, prompt, " (): ");
334         attr_set_str(&ci->focus->attrs, "prefix", prefix);
335         call("view:changed", ci->focus);
336
337         hi->search_back = (ci->key[4] == 'R');
338         return 1;
339 }
340
341 static void update_search(struct pane *p safe, struct pane *focus safe,
342                           int offset)
343 {
344         struct history_info *hi = p->data;
345         struct si *i;
346         struct mark *m;
347         const char *prefix;
348         int ret;
349
350         if (!hi->history)
351                 return;
352         if (offset >= 0) {
353                 alloc(i, pane);
354                 i->i = offset;
355                 i->line = mark_at_point(hi->history, NULL, MARK_UNGROUPED);
356                 i->prev = hi->prev;
357                 hi->prev = i;
358         }
359         prefix = strconcat(focus, hi->prompt?:"?",
360                            " (", buf_final(&hi->search)+2, "): ");
361         attr_set_str(&focus->attrs, "prefix", prefix);
362         call("view:changed", focus);
363         call("Mode:set-mode", focus, 0, NULL, ":History-search");
364         m = mark_at_point(hi->history, NULL, MARK_UNGROUPED);
365         ret = call("text-search", hi->history, 1, m, buf_final(&hi->search),
366                    hi->search_back);
367         if (ret <= 0) {
368                 // clear line
369                 mark_free(m);
370                 return;
371         }
372         call("doc:EOL", hi->history, -1, m);
373         call("Move-to", hi->history, 0, m);
374         mark_free(m);
375         recall_line(p, focus, 0);
376 }
377
378 DEF_CMD(history_search_again)
379 {
380         struct history_info *hi = ci->home->data;
381         const char *k;
382
383         k = ksuffix(ci, "K:History-search-");
384         if (*k) {
385                 int l = hi->search.len;
386                 buf_concat(&hi->search, k);
387                 update_search(ci->home, ci->focus, l);
388         }
389         return 1;
390 }
391
392 DEF_CMD(history_search_retry);
393
394 DEF_CMD(history_search_bs)
395 {
396         struct history_info *hi = ci->home->data;
397         struct si *i = hi->prev;
398
399         if (!i || !hi->history) {
400                 history_search_retry_func(ci);
401                 return 1;
402         }
403
404         call("Mode:set-mode", ci->focus, 0, NULL, ":History-search");
405
406         hi->search.len = i->i;
407         call("Move:to", hi->history, 0, i->line);
408         if (!i->prev || i->line != i->prev->line)
409                 mark_free(i->line);
410         hi->prev = i->prev;
411         free(i);
412         update_search(ci->home, ci->focus, -1);
413         return 1;
414 }
415
416 DEF_CMD(history_search_repeat)
417 {
418         struct history_info *hi = ci->home->data;
419         const char *suffix = ksuffix(ci, "K:History-search:C-");
420
421         if (!hi->history)
422                 return Enoarg;
423         hi->search_back = *suffix == 'R';
424         if (hi->search_back)
425                 call("doc:EOL", hi->history, -2);
426         else
427                 call("doc:EOL", hi->history, 1, NULL, NULL, 1);
428
429         update_search(ci->home, ci->focus, hi->search.len);
430         return 1;
431 }
432
433 DEF_CMD(history_search_cancel)
434 {
435         struct history_info *hi = ci->home->data;
436         const char *prefix;
437
438         prefix = strconcat(ci->focus, hi->prompt?:"?", ": ");
439         attr_set_str(&ci->focus->attrs, "prefix", prefix);
440         call("view:changed", ci->focus);
441         return 1;
442 }
443
444 REDEF_CMD(history_search_retry)
445 {
446         struct history_info *hi = ci->home->data;
447         const char *prefix;
448         char *k = strconcat(ci->home, "K", ksuffix(ci, "K:History-search"));
449
450         prefix = strconcat(ci->focus, hi->prompt?:"?", ": ");
451         attr_set_str(&ci->focus->attrs, "prefix", prefix);
452         call("view:changed", ci->focus);
453         return call(k, ci->focus, ci->num, ci->mark, ci->str,
454                     ci->num2, ci->mark2, ci->str2);
455 }
456
457 DEF_CMD(history_add)
458 {
459         const char *docname = ci->str;
460         const char *line = ci->str2;
461         struct pane *doc;
462
463         if (!docname || !line || strchr(line, '\n'))
464                 return Einval;
465         doc = call_ret(pane, "docs:byname", ci->focus, 0, NULL, ci->str);
466         if (!doc) {
467                 doc = call_ret(pane, "doc:from-text", ci->focus,
468                                0, NULL, ci->str);
469                 if (doc)
470                         call("global-multicall-doc:appeared-", doc);
471         }
472         if (!doc)
473                 return Efail;
474         call("doc:replace", doc, 1, NULL, line, 1);
475         call("doc:replace", doc, 1, NULL, "\n", 1);
476         return 1;
477 }
478
479 void edlib_init(struct pane *ed safe)
480 {
481         call_comm("global-set-command", ed, &history_attach, 0, NULL, "attach-history");
482         call_comm("global-set-command", ed, &history_last, 0, NULL, "history:get-last");
483         call_comm("global-set-command", ed, &history_add, 0, NULL, "history:add");
484
485         if (history_map)
486                 return;
487
488         history_map = key_alloc();
489         key_add(history_map, "Close", &history_close);
490         key_add(history_map, "Free", &history_free);
491         key_add(history_map, "Notify:Close", &history_notify_close);
492         key_add(history_map, "doc:replaced", &history_notify_replace);
493         key_add(history_map, "K:A-p", &history_move);
494         key_add(history_map, "K:A-n", &history_move);
495         key_add(history_map, "K:C-R", &history_search);
496         key_add(history_map, "K:C-S", &history_search);
497         key_add_prefix(history_map, "K:History-search-", &history_search_again);
498         key_add_prefix(history_map, "K:History-search:",
499                        &history_search_retry);
500         key_add(history_map, "K:History-search:Backspace",
501                        &history_search_bs);
502         key_add(history_map, "K:History-search:C-R",
503                        &history_search_repeat);
504         key_add(history_map, "K:History-search:C-S",
505                        &history_search_repeat);
506         key_add(history_map, "K:History-search:Enter",
507                        &history_search_cancel);
508         key_add(history_map, "K:History-search:ESC",
509                        &history_search_cancel);
510         key_add(history_map, "history:save", &history_save);
511         key_add(history_map, "history:get-last", &history_hlast);
512 }