2 * Copyright Neil Brown ©2020-2023 <neil@brown.name>
3 * May be distributed under terms of GPLv2 - see file:COPYING
5 * wiggle - mark word-wise differences and merges
7 * ranges currently have to be in same file.
8 * We use code from wiggle which requires that a 'stream'
9 * (a char[] with length) be split as words into a 'file', then two
10 * such 'file's passed to 'diff()' to produce a 'csl' (common subsequence list).
11 * The a/b/len of each element in the csl are index to the respective
12 * files, which have indexes into the streams.
16 #define PANE_DATA_TYPE struct wiggle_data
19 #include "wiggle/wiggle.h"
21 /* We provide a command that handles wiggling across multiple panes. It
22 * is paired with a private pane which can get notifications when those
26 struct pane *private safe;
29 struct mark *start, *end;
30 short skip; /* prefix chars to skip */
31 short choose; /* if !=0, only chose lines with non-space
32 * in this position 1..skip
36 /* After set-wiggle is called, these are set */
37 int space_conflicts, conflicts, wiggles;
40 #include "core-pane.h"
42 static bool has_nonspace(const char *s, int len)
45 const char *end = s+len;
47 while ((ch = get_utf8(&s, end)) < WERR &&
53 static bool only_spaces(struct file f, struct csl *csl safe, int which)
60 for (; csl->len; csl += 1) {
61 int o = which ? csl->b : csl->a;
63 for (; fpos < o; fpos += 1) {
64 struct elmnt e = f.list[fpos];
66 if (has_nonspace(e.start, e.len))
71 for (; fpos < f.elcnt; fpos += 1) {
72 struct elmnt e = f.list[fpos];
74 if (has_nonspace(e.start, e.len))
80 static void doskip(struct pane *p safe,
81 struct mark *m safe, struct mark *end,
84 /* If skip > 0, then the first 'skip' chars on each line
86 * If 'choose' is also > 0 then the whole line is skipped
88 * choose <= skip and the "choose"th char is not '+'
89 * choose > skip and none of the skip chars are '-'
92 bool chosen = choose == 0 || choose > skip;
94 while ((!end || mark_ordered_not_same(m, end)) &&
95 (toskip || !chosen)) {
96 /* Don't want this char */
97 wint_t wch = doc_next(p, m);
102 chosen = choose == 0 || choose > skip;
105 if (choose > skip && wch == '-')
107 if (skip - toskip == choose && wch != '+')
113 static bool collect(struct pane *p, struct mark *start, struct mark *end,
114 int skip, int choose, struct stream *s safe)
120 if (!p || !start || !end)
125 while (mark_ordered_not_same(m, end)) {
127 doskip(p, m, end, skip, choose);
128 if (!mark_ordered_not_same(m, end))
131 wch = doc_next(p, m);
136 s->body = buf_final(&b);
143 static void add_markup(struct pane *p, struct mark *start,
144 int skip, int choose,
145 struct stream astream, struct file afile,
146 struct csl *csl safe, const char *attr safe, int which)
148 /* Each range of characters that is mentioned in csl gets an attribute
149 * named 'attr' with value 'len' from csl.
150 * If a range crosses a newline, the first (non-skipped) character
151 * also gets the attribute with the remaining length.
153 const char *pos = astream.body;
157 if (!p || !start || !afile.list || !pos)
161 int st = which ? csl->b : csl->a;
162 const char *startp = afile.list[st].start - afile.list[st].prefix;
163 const char *endp = afile.list[st + csl->len - 1].start +
164 afile.list[st + csl->len - 1].len;
169 doskip(p, m, NULL, skip, choose);
170 while (pos < startp) {
171 if (get_utf8(&pos, NULL) >= WERR)
175 doskip(p, m, NULL, skip, choose);
177 /* Convert csl->len in bytes to len in codepoints. */
180 if (get_utf8(&pos, NULL) >= WERR)
185 snprintf(buf, sizeof(buf), "%d %d", len, which);
186 call("doc:set-attr", p, 0, m, attr, 0, NULL, buf);
189 if (get_utf8(&pos, NULL) >= WERR)
192 doskip(p, m, NULL, skip, choose);
193 snprintf(buf, sizeof(buf), "%d %d", len, which);
194 call("doc:set-attr", p, 0, m, attr,
205 DEF_CMD(notify_close)
207 /* Private pane received a "close" notification. */
208 struct wiggle_data *wd = ci->home->data;
211 for (i = 0; i < 3; i++)
212 if (ci->focus == wd->texts[i].text ||
213 ci->focus == wd->private) {
214 mark_free(wd->texts[i].start);
215 wd->texts[i].start = NULL;
216 mark_free(wd->texts[i].end);
217 wd->texts[i].end = NULL;
218 wd->texts[i].text = NULL;
223 DEF_CMD_CLOSED(wiggle_close)
225 struct wiggle_data *wd = ci->home->data;
228 for (i = 0; i < 3 ; i++) {
229 mark_free(wd->texts[i].start);
230 wd->texts[i].start = NULL;
231 mark_free(wd->texts[i].end);
232 wd->texts[i].end = NULL;
233 wd->texts[i].text = NULL;
239 static void wiggle_free(struct command *c safe)
241 struct wiggle_data *wd = container_of(c, struct wiggle_data, c);
243 pane_close(wd->private);
248 struct wiggle_data *wd = container_of(ci->comm, struct wiggle_data, c);
250 return home_call(wd->private, ci->key, ci->focus,
251 ci->num, ci->mark, ci->str,
252 ci->num2, ci->mark2, ci->str2,
253 ci->x, ci->y, ci->comm2);
256 static void forward_lines(struct pane *p safe, struct mark *m safe,
257 int skip, int choose, int lines)
260 doskip(p, m, NULL, skip, choose);
261 call("doc:EOL", p, 1, m, NULL, 1);
268 /* remember pane, mark1, mark2, num, num2 */
269 struct wiggle_data *wd = ci->home->data;
271 char k0 = ci->key[0];
272 int which = k0 == 'b' ? 1 : k0 == 'a' ? 2 : 0;
274 /* Always clean out, even it not given enough args */
275 mark_free(wd->texts[which].start);
276 wd->texts[which].start = NULL;
277 mark_free(wd->texts[which].end);
278 wd->texts[which].end = NULL;
279 /* It isn't possible to drop individual notification links.
280 * We will lose them all on close, and ignore any before that.
282 wd->texts[which].text = NULL;
284 if (!ci->mark || (!ci->mark2 && !ci->str))
286 if (ci->num < 0 || ci->num2 < 0 || ci->num2 > ci->num+1)
289 int lines = atoi(ci->str ?: "1");
290 m2 = mark_dup(ci->mark);
291 forward_lines(ci->focus, m2, ci->num, ci->num2, lines);
293 m2 = mark_dup(ci->mark2);
295 wd->texts[which].text = ci->focus;
296 pane_add_notify(ci->home, ci->focus, "Notify:Close");
297 wd->texts[which].start = mark_dup(ci->mark);
298 wd->texts[which].end = m2;
299 wd->texts[which].skip = ci->num;
300 wd->texts[which].choose = ci->num2;
305 DEF_CMD(wiggle_extract)
307 struct wiggle_data *wd = ci->home->data;
311 if (!ci->str || !ci->comm2)
313 if (strcmp(ci->str, "orig") == 0)
315 else if (strcmp(ci->str, "before") == 0)
317 else if (strcmp(ci->str, "after") == 0)
321 if (!collect(wt->text, wt->start, wt->end, wt->skip, wt->choose,
325 comm_call(ci->comm2, "cb", ci->focus, 0, NULL, str.body);
330 DEF_CMD(wiggle_set_common)
332 /* Set the attribute 'str' on all common ranges in
333 * 'before' and 'after'
335 struct wiggle_data *wd = ci->home->data;
336 const char *attr = ci->str ?: "render:common";
337 struct stream before, after;
338 struct file bfile, afile;
341 int ignore_blanks = 0 /* IgnoreBlanks */;
343 if (!collect(wd->texts[1].text, wd->texts[1].start, wd->texts[1].end,
344 wd->texts[1].skip, wd->texts[1].choose, &before))
346 if (!collect(wd->texts[2].text, wd->texts[2].start, wd->texts[2].end,
347 wd->texts[2].skip, wd->texts[2].choose, &after)) {
352 bfile = wiggle_split_stream(before, ByWord | ignore_blanks);
353 afile = wiggle_split_stream(after, ByWord | ignore_blanks);
354 csl = wiggle_diff(bfile, afile, 1);
356 add_markup(wd->texts[1].text, wd->texts[1].start,
357 wd->texts[1].skip, wd->texts[1].choose,
358 before, bfile, csl, attr, 0);
359 add_markup(wd->texts[2].text, wd->texts[2].start,
360 wd->texts[2].skip, wd->texts[2].choose,
361 after, afile, csl, attr, 1);
364 if (only_spaces(bfile, csl, 0) &&
365 only_spaces(afile, csl, 1))
366 ret = 2; /* only space differences */
378 static const char *typenames[] = {
380 [Unmatched] = "Unmatched",
381 [Unchanged] = "Unchanged",
382 [Extraneous] = "Extraneous",
383 [Changed] = "Changed",
384 [Conflict] = "Conflict",
385 [AlreadyApplied] = "AlreadyApplied",
388 static bool merge_has_nonspace(struct file f, int pos, int len)
397 endcp = f.list[pos+len-1].start + f.list[pos+len-1].len;
398 cp = f.list[pos].start;
399 return has_nonspace(cp, endcp-cp);
402 static int count_space_conflicts(struct merge *merge safe,
403 struct file a, struct file b, struct file c)
408 for (m = merge; m->type != End; m++) {
410 if (m->type != Conflict)
412 if (!merge_has_nonspace(a, m->a, m->al) &&
413 !merge_has_nonspace(b, m->b, m->bl) &&
414 !merge_has_nonspace(c, m->c, m->cl))
420 static void add_merge_markup(struct pane *p safe,
422 int skip, int choose,
423 struct file f, struct merge *merge safe,
424 const char *attr safe, int which,
436 LOG("which=%d", which);
437 for (m = merge, mergenum=0; m->type != End; m++, mergenum++) {
440 case 0: pos2 = m->a; break;
441 case 1: pos2 = m->b; break;
442 case 2: pos2 = m->c; break;
444 LOG("M:%d %-10s %d %d %d (%d+%d)", mergenum, typenames[m->type], m->al, m->bl, m->cl,
445 f.list[pos2].prefix, f.list[pos2].len);
448 doskip(p, st, NULL, skip, choose);
449 for (m = merge, mergenum=0; m->type != End; m++, mergenum++) {
451 const char *cp, *endcp;
459 case 0: /* orig - no Extraneous */
460 if (m->type == Extraneous)
462 if (pos != m->a) abort();
465 case 1: /* before - no Unmatched */
466 if (m->type == Unmatched)
468 if (pos != m->b) abort();
471 case 2: /* after - no Unmatched */
472 if (m->type == Unmatched)
474 if (pos != m->c) abort();
478 /* From here for 'len' element in f are 'm->type' */
481 cp = f.list[pos].start - f.list[pos].prefix;
482 endcp = f.list[pos+len-1].start + f.list[pos+len-1].len;
483 if (ignore_blanks && endcp) {
484 while (*endcp == ' ' || *endcp == '\t')
492 while ((wch = get_utf8(&cp, endcp)) < WERR) {
498 if (m->type == Conflict && !non_space)
500 snprintf(buf, sizeof(buf), "M %d %s %d%s",
501 chars, typenames[m->type], mergenum, suffix);
502 call("doc:set-attr", p, 0, st, attr, 0, NULL, buf);
504 wint_t ch = doc_next(p, st);
509 doskip(p, st, NULL, skip, choose);
511 if (is_eol(ch) && chars > 0) {
512 snprintf(buf, sizeof(buf), "L %d %s %d%s", chars,
513 typenames[m->type], mergenum, suffix);
514 call("doc:set-attr", p, 0, st, attr,
522 static int copy_words(char *str, struct file *f safe, int start, int len)
527 while (len > 0 && start < f->elcnt) {
528 struct elmnt e = f->list[start];
530 memcpy(str, e.start - e.prefix, e.plen + e.prefix);
531 str += e.plen + e.prefix;
533 ret += e.plen + e.prefix;
540 static char *collect_merge(struct merge *merge safe,
541 struct file of, struct file bf, struct file af)
547 if (!of.list || !bf.list || !af.list)
549 /* First determine size */
551 for (m = merge; m->type != End; m++) {
552 if (m->type == Unmatched ||
553 m->type == AlreadyApplied ||
554 m->type == Conflict ||
555 m->type == Unchanged)
556 l += copy_words(NULL, &of, m->a, m->al);
557 else if (m->type == Changed)
558 l += copy_words(NULL, &af, m->c, m->cl);
561 /* Now copy content in */
563 for (m = merge; m->type != End; m++) {
564 if (m->type == Unmatched ||
565 m->type == AlreadyApplied ||
566 m->type == Conflict ||
567 m->type == Unchanged)
568 l += copy_words(str+l, &of, m->a, m->al);
569 else if (m->type == Changed)
570 l += copy_words(str+l, &af, m->c, m->cl);
576 DEF_CMD(wiggle_set_wiggle)
578 struct wiggle_data *wd = ci->home->data;
579 struct stream ostr, astr, bstr;
580 struct file of, af, bf;
581 struct csl *csl1, *csl2;
583 const char *attr = ci->str ?: "render:wiggle";
584 int ignore_blanks = 0 /*IgnoreBlanks*/;
586 if (!collect(wd->texts[0].text, wd->texts[0].start, wd->texts[0].end,
587 wd->texts[0].skip, wd->texts[0].choose, &ostr))
589 if (!collect(wd->texts[1].text, wd->texts[1].start, wd->texts[1].end,
590 wd->texts[1].skip, wd->texts[1].choose, &bstr)) {
594 if (!collect(wd->texts[2].text, wd->texts[2].start, wd->texts[2].end,
595 wd->texts[2].skip, wd->texts[2].choose, &astr)) {
601 of = wiggle_split_stream(ostr, ByWord | ignore_blanks);
602 bf = wiggle_split_stream(bstr, ByWord | ignore_blanks);
603 af = wiggle_split_stream(astr, ByWord | ignore_blanks);
605 csl1 = wiggle_diff(of, bf, 1);
606 csl2 = wiggle_diff(bf, af, 1);
607 info = wiggle_make_merger(of, bf, af, csl1, csl2, 1, 1, 0);
611 wd->conflicts = info.conflicts;
612 wd->wiggles = info.wiggles;
613 wd->space_conflicts = count_space_conflicts(info.merger,
615 if (info.conflicts == wd->space_conflicts)
616 wd->wiggle = collect_merge(info.merger, of, bf, af);
618 add_merge_markup(ci->focus,
620 wd->texts[0].skip, wd->texts[0].choose,
621 of, info.merger, attr, 0, ignore_blanks);
622 add_merge_markup(ci->focus,
624 wd->texts[1].skip, wd->texts[1].choose,
625 bf, info.merger, attr, 1, ignore_blanks);
626 add_merge_markup(ci->focus,
628 wd->texts[2].skip, wd->texts[2].choose,
629 af, info.merger, attr, 2, ignore_blanks);
642 return info.conflicts + 1;
647 /* Find orig, before or after in 'focus' near 'mark'
648 * str in "orig", "before" or "after"
649 * num is max number of lines to stripe (fuzz)
650 * num2 is max number of lines, defaults to searching whole file.
651 * Returns number of fuzz lines, plus 1
653 struct wiggle_data *wd = ci->home->data;
654 int lines = ci->num2;
655 struct pane *p = ci->focus;
662 if (!ci->mark || !ci->str)
664 if (strcmp(ci->str, "orig") == 0)
666 else if (strcmp(ci->str, "before") == 0)
668 else if (strcmp(ci->str, "after") == 0)
672 if (!collect(wt->text, wt->start, wt->end, wt->skip, wt->choose,
680 struct mark *early, *late;
682 early = mark_dup(ci->mark);
683 call("doc:EOL", p, -1, early);
684 late = mark_dup(ci->mark);
685 call("doc:EOL", p, 1, late, NULL, 1);
686 if (doc_following(p, late) == WEOF) {
691 while (early || late) {
693 ret = call("text-equals", p, 0, early, match);
695 mark_to_mark(ci->mark, early);
698 if (ret != Efalse || doc_prior(p, early) == WEOF) {
702 call("doc:EOL", p, -2, early);
705 ret = call("text-equals", p, 0, late, match);
707 mark_to_mark(ci->mark, late);
710 if (ret != Efalse || doc_following(p, late) == WEOF) {
714 call("doc:EOL", p, 1, late, NULL, 1);
729 match = strchr(match, '\n');
733 end = strrchr(match, '\n');
736 end = strrchr(match, '\n');
741 } while (fuzz < ci->num);
744 return ret > 0 ? fuzz + 1 : Efail;
749 struct wiggle_data *wd = ci->home->data;
751 if (wd->conflicts < 0)
755 if (strcmp(ci->str, "wiggle") == 0) {
757 return comm_call(ci->comm2, "cb", ci->focus, 0, NULL,
762 if (strcmp(ci->str, "space-conflicts") == 0)
763 return wd->space_conflicts + 1;
764 if (strcmp(ci->str, "conflicts") == 0)
765 return wd->conflicts + 1;
766 if (strcmp(ci->str, "wiggles") == 0)
767 return wd->wiggles + 1;
771 DEF_CMD(wiggle_find_best) { return 0; }
773 static struct map *wiggle_map;
774 DEF_LOOKUP_CMD(wiggle_pane, wiggle_map);
778 struct wiggle_data *wd;
782 wiggle_map = key_alloc();
783 key_add(wiggle_map, "Notify:Close", ¬ify_close);
784 key_add(wiggle_map, "Close", &wiggle_close);
785 key_add(wiggle_map, "orig", &wiggle_text);
786 key_add(wiggle_map, "before", &wiggle_text);
787 key_add(wiggle_map, "after", &wiggle_text);
788 key_add(wiggle_map, "extract", &wiggle_extract);
789 key_add(wiggle_map, "set-common", &wiggle_set_common);
790 key_add(wiggle_map, "set-wiggle", &wiggle_set_wiggle);
791 key_add(wiggle_map, "find", &wiggle_find);
792 key_add(wiggle_map, "find-best", &wiggle_find_best);
793 key_add(wiggle_map, "get-result", &wiggle_get);
796 p = pane_register(pane_root(ci->focus), 0,
802 wd->c.free = wiggle_free;
806 comm_call(ci->comm2, "cb", ci->focus,
808 0, NULL, NULL, 0,0, &wd->c);
813 void edlib_init(struct pane *ed safe)
815 call_comm("global-set-command", ed, &make_wiggle,
816 0, NULL, "MakeWiggle");