Add the --color-words option to the diff options family

With this option, the changed words are shown inline. For example,
if a file containing "This is foo" is changed to "This is bar", the diff
will now show "This is " in plain text, "foo" in red, and "bar" in green.

Signed-off-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Signed-off-by: Junio C Hamano <junkio@cox.net>
diff --git a/diff.c b/diff.c
index b3b1781..a8710cb 100644
--- a/diff.c
+++ b/diff.c
@@ -358,12 +358,152 @@
 	return 0;
 }
 
+struct diff_words_buffer {
+	mmfile_t text;
+	long alloc;
+	long current; /* output pointer */
+	int suppressed_newline;
+};
+
+static void diff_words_append(char *line, unsigned long len,
+		struct diff_words_buffer *buffer)
+{
+	if (buffer->text.size + len > buffer->alloc) {
+		buffer->alloc = (buffer->text.size + len) * 3 / 2;
+		buffer->text.ptr = xrealloc(buffer->text.ptr, buffer->alloc);
+	}
+	line++;
+	len--;
+	memcpy(buffer->text.ptr + buffer->text.size, line, len);
+	buffer->text.size += len;
+}
+
+struct diff_words_data {
+	struct xdiff_emit_state xm;
+	struct diff_words_buffer minus, plus;
+};
+
+static void print_word(struct diff_words_buffer *buffer, int len, int color,
+		int suppress_newline)
+{
+	const char *ptr;
+	int eol = 0;
+
+	if (len == 0)
+		return;
+
+	ptr  = buffer->text.ptr + buffer->current;
+	buffer->current += len;
+
+	if (ptr[len - 1] == '\n') {
+		eol = 1;
+		len--;
+	}
+
+	fputs(diff_get_color(1, color), stdout);
+	fwrite(ptr, len, 1, stdout);
+	fputs(diff_get_color(1, DIFF_RESET), stdout);
+
+	if (eol) {
+		if (suppress_newline)
+			buffer->suppressed_newline = 1;
+		else
+			putchar('\n');
+	}
+}
+
+static void fn_out_diff_words_aux(void *priv, char *line, unsigned long len)
+{
+	struct diff_words_data *diff_words = priv;
+
+	if (diff_words->minus.suppressed_newline) {
+		if (line[0] != '+')
+			putchar('\n');
+		diff_words->minus.suppressed_newline = 0;
+	}
+
+	len--;
+	switch (line[0]) {
+		case '-':
+			print_word(&diff_words->minus, len, DIFF_FILE_OLD, 1);
+			break;
+		case '+':
+			print_word(&diff_words->plus, len, DIFF_FILE_NEW, 0);
+			break;
+		case ' ':
+			print_word(&diff_words->plus, len, DIFF_PLAIN, 0);
+			diff_words->minus.current += len;
+			break;
+	}
+}
+
+/* this executes the word diff on the accumulated buffers */
+static void diff_words_show(struct diff_words_data *diff_words)
+{
+	xpparam_t xpp;
+	xdemitconf_t xecfg;
+	xdemitcb_t ecb;
+	mmfile_t minus, plus;
+	int i;
+
+	minus.size = diff_words->minus.text.size;
+	minus.ptr = xmalloc(minus.size);
+	memcpy(minus.ptr, diff_words->minus.text.ptr, minus.size);
+	for (i = 0; i < minus.size; i++)
+		if (isspace(minus.ptr[i]))
+			minus.ptr[i] = '\n';
+	diff_words->minus.current = 0;
+
+	plus.size = diff_words->plus.text.size;
+	plus.ptr = xmalloc(plus.size);
+	memcpy(plus.ptr, diff_words->plus.text.ptr, plus.size);
+	for (i = 0; i < plus.size; i++)
+		if (isspace(plus.ptr[i]))
+			plus.ptr[i] = '\n';
+	diff_words->plus.current = 0;
+
+	xpp.flags = XDF_NEED_MINIMAL;
+	xecfg.ctxlen = diff_words->minus.alloc + diff_words->plus.alloc;
+	xecfg.flags = 0;
+	ecb.outf = xdiff_outf;
+	ecb.priv = diff_words;
+	diff_words->xm.consume = fn_out_diff_words_aux;
+	xdl_diff(&minus, &plus, &xpp, &xecfg, &ecb);
+
+	free(minus.ptr);
+	free(plus.ptr);
+	diff_words->minus.text.size = diff_words->plus.text.size = 0;
+
+	if (diff_words->minus.suppressed_newline) {
+		putchar('\n');
+		diff_words->minus.suppressed_newline = 0;
+	}
+}
+
 struct emit_callback {
 	struct xdiff_emit_state xm;
 	int nparents, color_diff;
 	const char **label_path;
+	struct diff_words_data *diff_words;
 };
 
+static void free_diff_words_data(struct emit_callback *ecbdata)
+{
+	if (ecbdata->diff_words) {
+		/* flush buffers */
+		if (ecbdata->diff_words->minus.text.size ||
+				ecbdata->diff_words->plus.text.size)
+			diff_words_show(ecbdata->diff_words);
+
+		if (ecbdata->diff_words->minus.text.ptr)
+			free (ecbdata->diff_words->minus.text.ptr);
+		if (ecbdata->diff_words->plus.text.ptr)
+			free (ecbdata->diff_words->plus.text.ptr);
+		free(ecbdata->diff_words);
+		ecbdata->diff_words = NULL;
+	}
+}
+
 const char *diff_get_color(int diff_use_color, enum color_diff ix)
 {
 	if (diff_use_color)
@@ -398,12 +538,31 @@
 	else {
 		int nparents = ecbdata->nparents;
 		int color = DIFF_PLAIN;
-		for (i = 0; i < nparents && len; i++) {
-			if (line[i] == '-')
-				color = DIFF_FILE_OLD;
-			else if (line[i] == '+')
-				color = DIFF_FILE_NEW;
-		}
+		if (ecbdata->diff_words && nparents != 1)
+			/* fall back to normal diff */
+			free_diff_words_data(ecbdata);
+		if (ecbdata->diff_words) {
+			if (line[0] == '-') {
+				diff_words_append(line, len,
+						&ecbdata->diff_words->minus);
+				return;
+			} else if (line[0] == '+') {
+				diff_words_append(line, len,
+						&ecbdata->diff_words->plus);
+				return;
+			}
+			if (ecbdata->diff_words->minus.text.size ||
+					ecbdata->diff_words->plus.text.size)
+				diff_words_show(ecbdata->diff_words);
+			line++;
+			len--;
+		} else
+			for (i = 0; i < nparents && len; i++) {
+				if (line[i] == '-')
+					color = DIFF_FILE_OLD;
+				else if (line[i] == '+')
+					color = DIFF_FILE_NEW;
+			}
 		set = diff_get_color(ecbdata->color_diff, color);
 	}
 	if (len > 0 && line[len-1] == '\n')
@@ -836,7 +995,12 @@
 		ecb.outf = xdiff_outf;
 		ecb.priv = &ecbdata;
 		ecbdata.xm.consume = fn_out_consume;
+		if (o->color_diff_words)
+			ecbdata.diff_words =
+				xcalloc(1, sizeof(struct diff_words_data));
 		xdl_diff(&mf1, &mf2, &xpp, &xecfg, &ecb);
+		if (o->color_diff_words)
+			free_diff_words_data(&ecbdata);
 	}
 
  free_ab_and_return:
@@ -1697,6 +1861,8 @@
 		options->xdl_opts |= XDF_IGNORE_WHITESPACE;
 	else if (!strcmp(arg, "-b") || !strcmp(arg, "--ignore-space-change"))
 		options->xdl_opts |= XDF_IGNORE_WHITESPACE_CHANGE;
+	else if (!strcmp(arg, "--color-words"))
+		options->color_diff = options->color_diff_words = 1;
 	else if (!strcmp(arg, "--no-renames"))
 		options->detect_rename = 0;
 	else