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.
18 #include "wiggle/wiggle.h"
20 static bool has_nonspace(const char *s, int len)
23 const char *end = s+len;
25 while ((ch = get_utf8(&s, end)) < WERR &&
31 static bool only_spaces(struct file f, struct csl *csl safe, int which)
38 for (; csl->len; csl += 1) {
39 int o = which ? csl->b : csl->a;
41 for (; fpos < o; fpos += 1) {
42 struct elmnt e = f.list[fpos];
44 if (has_nonspace(e.start, e.len))
49 for (; fpos < f.elcnt; fpos += 1) {
50 struct elmnt e = f.list[fpos];
52 if (has_nonspace(e.start, e.len))
58 static void doskip(struct pane *p safe,
59 struct mark *m safe, struct mark *end,
62 /* If skip > 0, then the first 'skip' chars on each line
64 * If 'choose' is also > 0 then the whole line is skipped
66 * choose <= skip and the "choose"th char is not '+'
67 * choose > skip and none of the skip chars are '-'
70 bool chosen = choose == 0 || choose > skip;
72 while ((!end || mark_ordered_not_same(m, end)) &&
73 (toskip || !chosen)) {
74 /* Don't want this char */
75 wint_t wch = doc_next(p, m);
80 chosen = choose == 0 || choose > skip;
83 if (choose > skip && wch == '-')
85 if (skip - toskip == choose && wch != '+')
91 static bool collect(struct pane *p, struct mark *start, struct mark *end,
92 int skip, int choose, struct stream *s safe)
98 if (!p || !start || !end)
103 while (mark_ordered_not_same(m, end)) {
105 doskip(p, m, end, skip, choose);
106 if (!mark_ordered_not_same(m, end))
109 wch = doc_next(p, m);
114 s->body = buf_final(&b);
121 static void add_markup(struct pane *p, struct mark *start,
122 int skip, int choose,
123 struct stream astream, struct file afile,
124 struct csl *csl safe, const char *attr safe, int which)
126 /* Each range of characters that is mentioned in csl gets an attribute
127 * named 'attr' with value 'len' from csl.
128 * If a range crosses a newline, the first (non-skipped) character
129 * also gets the attribute with the remaining length.
131 const char *pos = astream.body;
135 if (!p || !start || !afile.list || !pos)
139 int st = which ? csl->b : csl->a;
140 const char *startp = afile.list[st].start - afile.list[st].prefix;
141 const char *endp = afile.list[st + csl->len - 1].start +
142 afile.list[st + csl->len - 1].len;
147 doskip(p, m, NULL, skip, choose);
148 while (pos < startp) {
149 if (get_utf8(&pos, NULL) >= WERR)
153 doskip(p, m, NULL, skip, choose);
155 /* Convert csl->len in bytes to len in codepoints. */
158 if (get_utf8(&pos, NULL) >= WERR)
163 snprintf(buf, sizeof(buf), "%d %d", len, which);
164 call("doc:set-attr", p, 0, m, attr, 0, NULL, buf);
167 if (get_utf8(&pos, NULL) >= WERR)
170 doskip(p, m, NULL, skip, choose);
171 snprintf(buf, sizeof(buf), "%d %d", len, which);
172 call("doc:set-attr", p, 0, m, attr,
183 /* We provide a command that handles wiggling across multiple panes. It
184 * is paired with a private pane which can get notifications when those
188 struct pane *private safe;
191 struct mark *start, *end;
192 short skip; /* prefix chars to skip */
193 short choose; /* if !=0, only chose lines with non-space
194 * in this position 1..skip
198 /* After set-wiggle is called, these are set */
199 int space_conflicts, conflicts, wiggles;
203 DEF_CMD(notify_close)
205 /* Private pane received a "close" notification. */
206 struct wiggle_data *wd = ci->home->data;
209 for (i = 0; i < 3; i++)
210 if (ci->focus == wd->texts[i].text ||
211 ci->focus == wd->private) {
212 mark_free(wd->texts[i].start);
213 wd->texts[i].start = NULL;
214 mark_free(wd->texts[i].end);
215 wd->texts[i].end = NULL;
216 wd->texts[i].text = NULL;
221 DEF_CMD(wiggle_close)
223 struct wiggle_data *wd = ci->home->data;
226 for (i = 0; i < 3 ; i++) {
227 mark_free(wd->texts[i].start);
228 wd->texts[i].start = NULL;
229 mark_free(wd->texts[i].end);
230 wd->texts[i].end = NULL;
231 wd->texts[i].text = NULL;
237 static void wiggle_free(struct command *c safe)
239 struct wiggle_data *wd = container_of(c, struct wiggle_data, c);
241 pane_close(wd->private);
246 struct wiggle_data *wd = container_of(ci->comm, struct wiggle_data, c);
248 return home_call(wd->private, ci->key, ci->focus,
249 ci->num, ci->mark, ci->str,
250 ci->num2, ci->mark2, ci->str2,
251 ci->x, ci->y, ci->comm2);
254 static void forward_lines(struct pane *p safe, struct mark *m safe,
255 int skip, int choose, int lines)
258 doskip(p, m, NULL, skip, choose);
259 call("doc:EOL", p, 1, m, NULL, 1);
266 /* remember pane, mark1, mark2, num, num2 */
267 struct wiggle_data *wd = ci->home->data;
269 char k0 = ci->key[0];
270 int which = k0 == 'b' ? 1 : k0 == 'a' ? 2 : 0;
272 /* Always clean out, even it not given enough args */
273 mark_free(wd->texts[which].start);
274 wd->texts[which].start = NULL;
275 mark_free(wd->texts[which].end);
276 wd->texts[which].end = NULL;
277 /* It isn't possible to drop individual notification links.
278 * We will lose them all on close, and ignore any before that.
280 wd->texts[which].text = NULL;
282 if (!ci->mark || (!ci->mark2 && !ci->str))
284 if (ci->num < 0 || ci->num2 < 0 || ci->num2 > ci->num+1)
287 int lines = atoi(ci->str ?: "1");
288 m2 = mark_dup(ci->mark);
289 forward_lines(ci->focus, m2, ci->num, ci->num2, lines);
291 m2 = mark_dup(ci->mark2);
293 wd->texts[which].text = ci->focus;
294 pane_add_notify(ci->home, ci->focus, "Notify:Close");
295 wd->texts[which].start = mark_dup(ci->mark);
296 wd->texts[which].end = m2;
297 wd->texts[which].skip = ci->num;
298 wd->texts[which].choose = ci->num2;
303 DEF_CMD(wiggle_extract)
305 struct wiggle_data *wd = ci->home->data;
309 if (!ci->str || !ci->comm2)
311 if (strcmp(ci->str, "orig") == 0)
313 else if (strcmp(ci->str, "before") == 0)
315 else if (strcmp(ci->str, "after") == 0)
319 if (!collect(wt->text, wt->start, wt->end, wt->skip, wt->choose,
323 comm_call(ci->comm2, "cb", ci->focus, 0, NULL, str.body);
328 DEF_CMD(wiggle_set_common)
330 /* Set the attribute 'str' on all common ranges in
331 * 'before' and 'after'
333 struct wiggle_data *wd = ci->home->data;
334 const char *attr = ci->str ?: "render:common";
335 struct stream before, after;
336 struct file bfile, afile;
339 int ignore_blanks = 0 /* IgnoreBlanks */;
341 if (!collect(wd->texts[1].text, wd->texts[1].start, wd->texts[1].end,
342 wd->texts[1].skip, wd->texts[1].choose, &before))
344 if (!collect(wd->texts[2].text, wd->texts[2].start, wd->texts[2].end,
345 wd->texts[2].skip, wd->texts[2].choose, &after)) {
350 bfile = wiggle_split_stream(before, ByWord | ignore_blanks);
351 afile = wiggle_split_stream(after, ByWord | ignore_blanks);
352 csl = wiggle_diff(bfile, afile, 1);
354 add_markup(wd->texts[1].text, wd->texts[1].start,
355 wd->texts[1].skip, wd->texts[1].choose,
356 before, bfile, csl, attr, 0);
357 add_markup(wd->texts[2].text, wd->texts[2].start,
358 wd->texts[2].skip, wd->texts[2].choose,
359 after, afile, csl, attr, 1);
362 if (only_spaces(bfile, csl, 0) &&
363 only_spaces(afile, csl, 1))
364 ret = 2; /* only space differences */
376 static const char *typenames[] = {
378 [Unmatched] = "Unmatched",
379 [Unchanged] = "Unchanged",
380 [Extraneous] = "Extraneous",
381 [Changed] = "Changed",
382 [Conflict] = "Conflict",
383 [AlreadyApplied] = "AlreadyApplied",
386 static bool merge_has_nonspace(struct file f, int pos, int len)
395 endcp = f.list[pos+len-1].start + f.list[pos+len-1].len;
396 cp = f.list[pos].start;
397 return has_nonspace(cp, endcp-cp);
400 static int count_space_conflicts(struct merge *merge safe,
401 struct file a, struct file b, struct file c)
406 for (m = merge; m->type != End; m++) {
408 if (m->type != Conflict)
410 if (!merge_has_nonspace(a, m->a, m->al) &&
411 !merge_has_nonspace(b, m->b, m->bl) &&
412 !merge_has_nonspace(c, m->c, m->cl))
418 static void add_merge_markup(struct pane *p safe,
420 int skip, int choose,
421 struct file f, struct merge *merge safe,
422 const char *attr safe, int which,
434 LOG("which=%d", which);
435 for (m = merge, mergenum=0; m->type != End; m++, mergenum++) {
438 case 0: pos2 = m->a; break;
439 case 1: pos2 = m->b; break;
440 case 2: pos2 = m->c; break;
442 LOG("M:%d %-10s %d %d %d (%d+%d)", mergenum, typenames[m->type], m->al, m->bl, m->cl,
443 f.list[pos2].prefix, f.list[pos2].len);
446 doskip(p, st, NULL, skip, choose);
447 for (m = merge, mergenum=0; m->type != End; m++, mergenum++) {
449 const char *cp, *endcp;
457 case 0: /* orig - no Extraneous */
458 if (m->type == Extraneous)
460 if (pos != m->a) abort();
463 case 1: /* before - no Unmatched */
464 if (m->type == Unmatched)
466 if (pos != m->b) abort();
469 case 2: /* after - no Unmatched */
470 if (m->type == Unmatched)
472 if (pos != m->c) abort();
476 /* From here for 'len' element in f are 'm->type' */
479 cp = f.list[pos].start - f.list[pos].prefix;
480 endcp = f.list[pos+len-1].start + f.list[pos+len-1].len;
481 if (ignore_blanks && endcp) {
482 while (*endcp == ' ' || *endcp == '\t')
490 while ((wch = get_utf8(&cp, endcp)) < WERR) {
496 if (m->type == Conflict && !non_space)
498 snprintf(buf, sizeof(buf), "M %d %s %d%s",
499 chars, typenames[m->type], mergenum, suffix);
500 call("doc:set-attr", p, 0, st, attr, 0, NULL, buf);
502 wint_t ch = doc_next(p, st);
507 doskip(p, st, NULL, skip, choose);
509 if (is_eol(ch) && chars > 0) {
510 snprintf(buf, sizeof(buf), "L %d %s %d%s", chars,
511 typenames[m->type], mergenum, suffix);
512 call("doc:set-attr", p, 0, st, attr,
520 static int copy_words(char *str, struct file *f safe, int start, int len)
525 while (len > 0 && start < f->elcnt) {
526 struct elmnt e = f->list[start];
528 memcpy(str, e.start - e.prefix, e.plen + e.prefix);
529 str += e.plen + e.prefix;
531 ret += e.plen + e.prefix;
538 static char *collect_merge(struct merge *merge safe,
539 struct file of, struct file bf, struct file af)
545 if (!of.list || !bf.list || !af.list)
547 /* First determine size */
549 for (m = merge; m->type != End; m++) {
550 if (m->type == Unmatched ||
551 m->type == AlreadyApplied ||
552 m->type == Conflict ||
553 m->type == Unchanged)
554 l += copy_words(NULL, &of, m->a, m->al);
555 else if (m->type == Changed)
556 l += copy_words(NULL, &af, m->c, m->cl);
559 /* Now copy content in */
561 for (m = merge; m->type != End; m++) {
562 if (m->type == Unmatched ||
563 m->type == AlreadyApplied ||
564 m->type == Conflict ||
565 m->type == Unchanged)
566 l += copy_words(str+l, &of, m->a, m->al);
567 else if (m->type == Changed)
568 l += copy_words(str+l, &af, m->c, m->cl);
574 DEF_CMD(wiggle_set_wiggle)
576 struct wiggle_data *wd = ci->home->data;
577 struct stream ostr, astr, bstr;
578 struct file of, af, bf;
579 struct csl *csl1, *csl2;
581 const char *attr = ci->str ?: "render:wiggle";
582 int ignore_blanks = 0 /*IgnoreBlanks*/;
584 if (!collect(wd->texts[0].text, wd->texts[0].start, wd->texts[0].end,
585 wd->texts[0].skip, wd->texts[0].choose, &ostr))
587 if (!collect(wd->texts[1].text, wd->texts[1].start, wd->texts[1].end,
588 wd->texts[1].skip, wd->texts[1].choose, &bstr)) {
592 if (!collect(wd->texts[2].text, wd->texts[2].start, wd->texts[2].end,
593 wd->texts[2].skip, wd->texts[2].choose, &astr)) {
599 of = wiggle_split_stream(ostr, ByWord | ignore_blanks);
600 bf = wiggle_split_stream(bstr, ByWord | ignore_blanks);
601 af = wiggle_split_stream(astr, ByWord | ignore_blanks);
603 csl1 = wiggle_diff(of, bf, 1);
604 csl2 = wiggle_diff(bf, af, 1);
605 info = wiggle_make_merger(of, bf, af, csl1, csl2, 1, 1, 0);
609 wd->conflicts = info.conflicts;
610 wd->wiggles = info.wiggles;
611 wd->space_conflicts = count_space_conflicts(info.merger,
613 if (info.conflicts == wd->space_conflicts)
614 wd->wiggle = collect_merge(info.merger, of, bf, af);
616 add_merge_markup(ci->focus,
618 wd->texts[0].skip, wd->texts[0].choose,
619 of, info.merger, attr, 0, ignore_blanks);
620 add_merge_markup(ci->focus,
622 wd->texts[1].skip, wd->texts[1].choose,
623 bf, info.merger, attr, 1, ignore_blanks);
624 add_merge_markup(ci->focus,
626 wd->texts[2].skip, wd->texts[2].choose,
627 af, info.merger, attr, 2, ignore_blanks);
640 return info.conflicts + 1;
645 /* Find orig, before or after in 'focus' near 'mark'
646 * str in "orig", "before" or "after"
647 * num is max number of lines to stripe (fuzz)
648 * num2 is max number of lines, defaults to searching whole file.
649 * Returns number of fuzz lines, plus 1
651 struct wiggle_data *wd = ci->home->data;
652 int lines = ci->num2;
653 struct pane *p = ci->focus;
660 if (!ci->mark || !ci->str)
662 if (strcmp(ci->str, "orig") == 0)
664 else if (strcmp(ci->str, "before") == 0)
666 else if (strcmp(ci->str, "after") == 0)
670 if (!collect(wt->text, wt->start, wt->end, wt->skip, wt->choose,
678 struct mark *early, *late;
680 early = mark_dup(ci->mark);
681 call("doc:EOL", p, -1, early);
682 late = mark_dup(ci->mark);
683 call("doc:EOL", p, 1, late, NULL, 1);
684 if (doc_following(p, late) == WEOF) {
689 while (early || late) {
691 ret = call("text-equals", p, 0, early, match);
693 mark_to_mark(ci->mark, early);
696 if (ret != Efalse || doc_prior(p, early) == WEOF) {
700 call("doc:EOL", p, -2, early);
703 ret = call("text-equals", p, 0, late, match);
705 mark_to_mark(ci->mark, late);
708 if (ret != Efalse || doc_following(p, late) == WEOF) {
712 call("doc:EOL", p, 1, late, NULL, 1);
727 match = strchr(match, '\n');
731 end = strrchr(match, '\n');
734 end = strrchr(match, '\n');
739 } while (fuzz < ci->num);
742 return ret > 0 ? fuzz + 1 : Efail;
747 struct wiggle_data *wd = ci->home->data;
749 if (wd->conflicts < 0)
753 if (strcmp(ci->str, "wiggle") == 0) {
755 return comm_call(ci->comm2, "cb", ci->focus, 0, NULL,
760 if (strcmp(ci->str, "space-conflicts") == 0)
761 return wd->space_conflicts + 1;
762 if (strcmp(ci->str, "conflicts") == 0)
763 return wd->conflicts + 1;
764 if (strcmp(ci->str, "wiggles") == 0)
765 return wd->wiggles + 1;
769 DEF_CMD(wiggle_find_best) { return 0; }
771 static struct map *wiggle_map;
772 DEF_LOOKUP_CMD(wiggle_pane, wiggle_map);
776 struct wiggle_data *wd;
780 wiggle_map = key_alloc();
781 key_add(wiggle_map, "Notify:Close", ¬ify_close);
782 key_add(wiggle_map, "Close", &wiggle_close);
783 key_add(wiggle_map, "orig", &wiggle_text);
784 key_add(wiggle_map, "before", &wiggle_text);
785 key_add(wiggle_map, "after", &wiggle_text);
786 key_add(wiggle_map, "extract", &wiggle_extract);
787 key_add(wiggle_map, "set-common", &wiggle_set_common);
788 key_add(wiggle_map, "set-wiggle", &wiggle_set_wiggle);
789 key_add(wiggle_map, "find", &wiggle_find);
790 key_add(wiggle_map, "find-best", &wiggle_find_best);
791 key_add(wiggle_map, "get-result", &wiggle_get);
796 wd->c.free = wiggle_free;
798 p = pane_register(pane_root(ci->focus), 0,
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");