diff --git a/contrib/unicode/gen-box-drawing-chars.py b/contrib/unicode/gen-box-drawing-chars.py
new file mode 100755
index 000000000000..9a55266ab843
--- /dev/null
+++ b/contrib/unicode/gen-box-drawing-chars.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+#
+# Script to generate gcc/text-art/box-drawing-chars.inc
+#
+# This file is part of GCC.
+#
+# GCC is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 3, or (at your option) any later
+# version.
+#
+# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3. If not see
+# . */
+
+import unicodedata
+
+def get_box_drawing_char_name(up: bool,
+ down: bool,
+ left: bool,
+ right: bool) -> str:
+ if 0:
+ print(f'{locals()=}')
+ if up and down:
+ vertical = True
+ up = False
+ down = False
+ else:
+ vertical = False
+
+ if left and right:
+ horizontal = True
+ left = False
+ right = False
+ else:
+ horizontal = False
+
+ weights = []
+ heavy = []
+ light = []
+ dirs = []
+ for dir_name in ('up', 'down', 'vertical', 'left', 'right', 'horizontal'):
+ val = locals()[dir_name]
+ if val:
+ dirs.append(dir_name.upper())
+
+ if not dirs:
+ return 'SPACE'
+
+ name = 'BOX DRAWINGS'
+ #print(f'{light=} {heavy=}')
+
+ if 0:
+ print(dirs)
+
+ def weights_frag(weight: str, dirs: list, prefix: bool):
+ """
+ Generate a fragment where all directions share the same weight, e.g.:
+ 'HEAVY HORIZONTAL'
+ 'DOWN LIGHT'
+ 'LEFT DOWN HEAVY'
+ 'HEAVY DOWN AND RIGHT'
+ """
+ assert len(dirs) >= 1
+ assert len(dirs) <= 2
+ if prefix:
+ return f' {weight} ' + (' AND '.join(dirs))
+ else:
+ return ' ' + (' '.join(dirs)) + f' {weight}'
+
+ assert(len(dirs) >= 1 and len(dirs) <= 2)
+ name += weights_frag('LIGHT', dirs, True)
+
+ return name
+
+print('/* Generated by contrib/unicode/gen-box-drawing-chars.py. */')
+print()
+for i in range(16):
+ up = (i & 8)
+ down = (i & 4)
+ left = (i & 2)
+ right = (i & 1)
+ name = get_box_drawing_char_name(up, down, left, right)
+ if i < 15:
+ trailing_comma = ','
+ else:
+ trailing_comma = ' '
+ unichar = unicodedata.lookup(name)
+ print(f'0x{ord(unichar):04X}{trailing_comma} /* "{unichar}": U+{ord(unichar):04X}: {name} */')
diff --git a/contrib/unicode/gen-combining-chars.py b/contrib/unicode/gen-combining-chars.py
new file mode 100755
index 000000000000..fb5ef50ba4ca
--- /dev/null
+++ b/contrib/unicode/gen-combining-chars.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+#
+# Script to generate libcpp/combining-chars.inc
+#
+# This file is part of GCC.
+#
+# GCC is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 3, or (at your option) any later
+# version.
+#
+# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3. If not see
+# . */
+
+from pprint import pprint
+import unicodedata
+
+def is_combining_char(code_point) -> bool:
+ return unicodedata.combining(chr(code_point)) != 0
+
+class Range:
+ def __init__(self, start, end, value):
+ self.start = start
+ self.end = end
+ self.value = value
+
+ def __repr__(self):
+ return f'Range({self.start:x}, {self.end:x}, {self.value})'
+
+def make_ranges(value_callback):
+ ranges = []
+ for code_point in range(0x10FFFF):
+ value = is_combining_char(code_point)
+ if 0:
+ print(f'{code_point=:x} {value=}')
+ if ranges and ranges[-1].value == value:
+ # Extend current range
+ ranges[-1].end = code_point
+ else:
+ # Start a new range
+ ranges.append(Range(code_point, code_point, value))
+ return ranges
+
+ranges = make_ranges(is_combining_char)
+if 0:
+ pprint(ranges)
+
+print(f"/* Generated by contrib/unicode/gen-combining-chars.py")
+print(f" using version {unicodedata.unidata_version}"
+ " of the Unicode standard. */")
+print("\nstatic const cppchar_t combining_range_ends[] = {", end="")
+for i, r in enumerate(ranges):
+ if i % 8:
+ print(" ", end="")
+ else:
+ print("\n ", end="")
+ print("0x%x," % r.end, end="")
+print("\n};\n")
+print("static const bool is_combining[] = {", end="")
+for i, r in enumerate(ranges):
+ if i % 24:
+ print(" ", end="")
+ else:
+ print("\n ", end="")
+ if r.value:
+ print("1,", end="")
+ else:
+ print("0,", end="")
+print("\n};")
diff --git a/contrib/unicode/gen-printable-chars.py b/contrib/unicode/gen-printable-chars.py
new file mode 100755
index 000000000000..7684c086638b
--- /dev/null
+++ b/contrib/unicode/gen-printable-chars.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+#
+# Script to generate libcpp/printable-chars.inc
+#
+# This file is part of GCC.
+#
+# GCC is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 3, or (at your option) any later
+# version.
+#
+# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3. If not see
+# . */
+
+from pprint import pprint
+import unicodedata
+
+def is_printable_char(code_point) -> bool:
+ category = unicodedata.category(chr(code_point))
+ # "Cc" is "control" and "Cf" is "format"
+ return category[0] != 'C'
+
+class Range:
+ def __init__(self, start, end, value):
+ self.start = start
+ self.end = end
+ self.value = value
+
+ def __repr__(self):
+ return f'Range({self.start:x}, {self.end:x}, {self.value})'
+
+def make_ranges(value_callback):
+ ranges = []
+ for code_point in range(0x10FFFF):
+ value = is_printable_char(code_point)
+ if 0:
+ print(f'{code_point=:x} {value=}')
+ if ranges and ranges[-1].value == value:
+ # Extend current range
+ ranges[-1].end = code_point
+ else:
+ # Start a new range
+ ranges.append(Range(code_point, code_point, value))
+ return ranges
+
+ranges = make_ranges(is_printable_char)
+if 0:
+ pprint(ranges)
+
+print(f"/* Generated by contrib/unicode/gen-printable-chars.py")
+print(f" using version {unicodedata.unidata_version}"
+ " of the Unicode standard. */")
+print("\nstatic const cppchar_t printable_range_ends[] = {", end="")
+for i, r in enumerate(ranges):
+ if i % 8:
+ print(" ", end="")
+ else:
+ print("\n ", end="")
+ print("0x%x," % r.end, end="")
+print("\n};\n")
+print("static const bool is_printable[] = {", end="")
+for i, r in enumerate(ranges):
+ if i % 24:
+ print(" ", end="")
+ else:
+ print("\n ", end="")
+ if r.value:
+ print("1,", end="")
+ else:
+ print("0,", end="")
+print("\n};")
diff --git a/gcc/Makefile.in b/gcc/Makefile.in
index 669a2a001d58..8a7dbf71491d 100644
--- a/gcc/Makefile.in
+++ b/gcc/Makefile.in
@@ -1788,7 +1788,16 @@ OBJS-libcommon = diagnostic-spec.o diagnostic.o diagnostic-color.o \
json.o \
sbitmap.o \
vec.o input.o hash-table.o ggc-none.o memory-block.o \
- selftest.o selftest-diagnostic.o sort.o
+ selftest.o selftest-diagnostic.o sort.o \
+ text-art/box-drawing.o \
+ text-art/canvas.o \
+ text-art/ruler.o \
+ text-art/selftests.o \
+ text-art/style.o \
+ text-art/styled-string.o \
+ text-art/table.o \
+ text-art/theme.o \
+ text-art/widget.o
# Objects in libcommon-target.a, used by drivers and by the core
# compiler and containing target-dependent code.
diff --git a/gcc/color-macros.h b/gcc/color-macros.h
index fcd79d09c018..9688f92110ac 100644
--- a/gcc/color-macros.h
+++ b/gcc/color-macros.h
@@ -92,6 +92,14 @@ along with GCC; see the file COPYING3. If not see
#define COLOR_FG_MAGENTA "35"
#define COLOR_FG_CYAN "36"
#define COLOR_FG_WHITE "37"
+#define COLOR_FG_BRIGHT_BLACK "90"
+#define COLOR_FG_BRIGHT_RED "91"
+#define COLOR_FG_BRIGHT_GREEN "92"
+#define COLOR_FG_BRIGHT_YELLOW "93"
+#define COLOR_FG_BRIGHT_BLUE "94"
+#define COLOR_FG_BRIGHT_MAGENTA "95"
+#define COLOR_FG_BRIGHT_CYAN "96"
+#define COLOR_FG_BRIGHT_WHITE "97"
#define COLOR_BG_BLACK "40"
#define COLOR_BG_RED "41"
#define COLOR_BG_GREEN "42"
@@ -100,6 +108,14 @@ along with GCC; see the file COPYING3. If not see
#define COLOR_BG_MAGENTA "45"
#define COLOR_BG_CYAN "46"
#define COLOR_BG_WHITE "47"
+#define COLOR_BG_BRIGHT_BLACK "100"
+#define COLOR_BG_BRIGHT_RED "101"
+#define COLOR_BG_BRIGHT_GREEN "102"
+#define COLOR_BG_BRIGHT_YELLOW "103"
+#define COLOR_BG_BRIGHT_BLUE "104"
+#define COLOR_BG_BRIGHT_MAGENTA "105"
+#define COLOR_BG_BRIGHT_CYAN "106"
+#define COLOR_BG_BRIGHT_WHITE "107"
#define SGR_START "\33["
#define SGR_END "m\33[K"
#define SGR_SEQ(str) SGR_START str SGR_END
diff --git a/gcc/common.opt b/gcc/common.opt
index 3daec85aef9b..25f650e2dae4 100644
--- a/gcc/common.opt
+++ b/gcc/common.opt
@@ -1502,6 +1502,29 @@ fdiagnostics-show-path-depths
Common Var(flag_diagnostics_show_path_depths) Init(0)
Show stack depths of events in paths.
+fdiagnostics-text-art-charset=
+Driver Common Joined RejectNegative Var(flag_diagnostics_text_art_charset) Enum(diagnostic_text_art_charset) Init(DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI)
+-fdiagnostics-text-art-charset=[none|ascii|unicode|emoji] Determine which characters to use in text arg diagrams.
+
+; Required for these enum values.
+SourceInclude
+diagnostic-text-art.h
+
+Enum
+Name(diagnostic_text_art_charset) Type(int)
+
+EnumValue
+Enum(diagnostic_text_art_charset) String(none) Value(DIAGNOSTICS_TEXT_ART_CHARSET_NONE)
+
+EnumValue
+Enum(diagnostic_text_art_charset) String(ascii) Value(DIAGNOSTICS_TEXT_ART_CHARSET_ASCII)
+
+EnumValue
+Enum(diagnostic_text_art_charset) String(unicode) Value(DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE)
+
+EnumValue
+Enum(diagnostic_text_art_charset) String(emoji) Value(DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI)
+
fdiagnostics-minimum-margin-width=
Common Joined UInteger Var(diagnostics_minimum_margin_width) Init(6)
Set minimum width of left margin of source code when showing source.
diff --git a/gcc/configure b/gcc/configure
index f7b4b283ca2f..c99105fb5fd0 100755
--- a/gcc/configure
+++ b/gcc/configure
@@ -34009,7 +34009,7 @@ $as_echo "$as_me: executing $ac_file commands" >&6;}
"depdir":C) $SHELL $ac_aux_dir/mkinstalldirs $DEPDIR ;;
"gccdepdir":C)
${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs build/$DEPDIR
- for lang in $subdirs c-family common analyzer rtl-ssa
+ for lang in $subdirs c-family common analyzer text-art rtl-ssa
do
${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs $lang/$DEPDIR
done ;;
diff --git a/gcc/configure.ac b/gcc/configure.ac
index 9c680ec4ed94..0428ee41b9dc 100644
--- a/gcc/configure.ac
+++ b/gcc/configure.ac
@@ -1382,7 +1382,7 @@ AC_CHECK_HEADERS(ext/hash_map)
ZW_CREATE_DEPDIR
AC_CONFIG_COMMANDS([gccdepdir],[
${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs build/$DEPDIR
- for lang in $subdirs c-family common analyzer rtl-ssa
+ for lang in $subdirs c-family common analyzer text-art rtl-ssa
do
${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs $lang/$DEPDIR
done], [subdirs="$subdirs" ac_aux_dir=$ac_aux_dir DEPDIR=$DEPDIR])
diff --git a/gcc/diagnostic-diagram.h b/gcc/diagnostic-diagram.h
new file mode 100644
index 000000000000..fc923c512ed2
--- /dev/null
+++ b/gcc/diagnostic-diagram.h
@@ -0,0 +1,51 @@
+/* Support for diagrams within diagnostics.
+ Copyright (C) 2023 Free Software Foundation, Inc.
+ Contributed by David Malcolm
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+. */
+
+#ifndef GCC_DIAGNOSTIC_DIAGRAM_H
+#define GCC_DIAGNOSTIC_DIAGRAM_H
+
+namespace text_art
+{
+ class canvas;
+} // namespace text_art
+
+/* A text art diagram, along with an "alternative text" string
+ describing it. */
+
+class diagnostic_diagram
+{
+ public:
+ diagnostic_diagram (const text_art::canvas &canvas,
+ const char *alt_text)
+ : m_canvas (canvas),
+ m_alt_text (alt_text)
+ {
+ gcc_assert (alt_text);
+ }
+
+ const text_art::canvas &get_canvas () const { return m_canvas; }
+ const char *get_alt_text () const { return m_alt_text; }
+
+ private:
+ const text_art::canvas &m_canvas;
+ const char *const m_alt_text;
+};
+
+#endif /* ! GCC_DIAGNOSTIC_DIAGRAM_H */
diff --git a/gcc/diagnostic-format-json.cc b/gcc/diagnostic-format-json.cc
index 694dddca9e8c..539b98b5e74f 100644
--- a/gcc/diagnostic-format-json.cc
+++ b/gcc/diagnostic-format-json.cc
@@ -324,6 +324,15 @@ json_file_final_cb (diagnostic_context *)
free (filename);
}
+/* Callback for diagnostic_context::m_diagrams.m_emission_cb. */
+
+static void
+json_emit_diagram (diagnostic_context *,
+ const diagnostic_diagram &)
+{
+ /* No-op. */
+}
+
/* Populate CONTEXT in preparation for JSON output (either to stderr, or
to a file). */
@@ -340,6 +349,7 @@ diagnostic_output_format_init_json (diagnostic_context *context)
context->begin_group_cb = json_begin_group;
context->end_group_cb = json_end_group;
context->print_path = NULL; /* handled in json_end_diagnostic. */
+ context->m_diagrams.m_emission_cb = json_emit_diagram;
/* The metadata is handled in JSON format, rather than as text. */
context->show_cwe = false;
diff --git a/gcc/diagnostic-format-sarif.cc b/gcc/diagnostic-format-sarif.cc
index fd29ac2ca3b4..ac2f5b844e39 100644
--- a/gcc/diagnostic-format-sarif.cc
+++ b/gcc/diagnostic-format-sarif.cc
@@ -29,6 +29,8 @@ along with GCC; see the file COPYING3. If not see
#include "cpplib.h"
#include "logical-location.h"
#include "diagnostic-client-data-hooks.h"
+#include "diagnostic-diagram.h"
+#include "text-art/canvas.h"
class sarif_builder;
@@ -66,8 +68,13 @@ public:
diagnostic_info *diagnostic,
diagnostic_t orig_diag_kind,
sarif_builder *builder);
+ void on_diagram (diagnostic_context *context,
+ const diagnostic_diagram &diagram,
+ sarif_builder *builder);
private:
+ void add_related_location (json::object *location_obj);
+
json::array *m_related_locations_arr;
};
@@ -135,7 +142,8 @@ public:
void end_diagnostic (diagnostic_context *context, diagnostic_info *diagnostic,
diagnostic_t orig_diag_kind);
-
+ void emit_diagram (diagnostic_context *context,
+ const diagnostic_diagram &diagram);
void end_group ();
void flush_to_file (FILE *outf);
@@ -144,6 +152,9 @@ public:
json::object *make_location_object (const rich_location &rich_loc,
const logical_location *logical_loc);
json::object *make_message_object (const char *msg) const;
+ json::object *
+ make_message_object_for_diagram (diagnostic_context *context,
+ const diagnostic_diagram &diagram);
private:
sarif_result *make_result_object (diagnostic_context *context,
@@ -261,12 +272,6 @@ sarif_result::on_nested_diagnostic (diagnostic_context *context,
diagnostic_t /*orig_diag_kind*/,
sarif_builder *builder)
{
- if (!m_related_locations_arr)
- {
- m_related_locations_arr = new json::array ();
- set ("relatedLocations", m_related_locations_arr);
- }
-
/* We don't yet generate meaningful logical locations for notes;
sometimes these will related to current_function_decl, but
often they won't. */
@@ -277,6 +282,39 @@ sarif_result::on_nested_diagnostic (diagnostic_context *context,
pp_clear_output_area (context->printer);
location_obj->set ("message", message_obj);
+ add_related_location (location_obj);
+}
+
+/* Handle diagrams that occur within a diagnostic group.
+ The closest thing in SARIF seems to be to add a location to the
+ "releatedLocations" property (SARIF v2.1.0 section 3.27.22),
+ and to put the diagram into the "message" property of that location
+ (SARIF v2.1.0 section 3.28.5). */
+
+void
+sarif_result::on_diagram (diagnostic_context *context,
+ const diagnostic_diagram &diagram,
+ sarif_builder *builder)
+{
+ json::object *location_obj = new json::object ();
+ json::object *message_obj
+ = builder->make_message_object_for_diagram (context, diagram);
+ location_obj->set ("message", message_obj);
+
+ add_related_location (location_obj);
+}
+
+/* Add LOCATION_OBJ to this result's "relatedLocations" array,
+ creating it if it doesn't yet exist. */
+
+void
+sarif_result::add_related_location (json::object *location_obj)
+{
+ if (!m_related_locations_arr)
+ {
+ m_related_locations_arr = new json::array ();
+ set ("relatedLocations", m_related_locations_arr);
+ }
m_related_locations_arr->append (location_obj);
}
@@ -348,6 +386,18 @@ sarif_builder::end_diagnostic (diagnostic_context *context,
}
}
+/* Implementation of diagnostic_context::m_diagrams.m_emission_cb
+ for SARIF output. */
+
+void
+sarif_builder::emit_diagram (diagnostic_context *context,
+ const diagnostic_diagram &diagram)
+{
+ /* We must be within the emission of a top-level diagnostic. */
+ gcc_assert (m_cur_group_result);
+ m_cur_group_result->on_diagram (context, diagram, this);
+}
+
/* Implementation of "end_group_cb" for SARIF output. */
void
@@ -1115,6 +1165,37 @@ sarif_builder::make_message_object (const char *msg) const
return message_obj;
}
+/* Make a message object (SARIF v2.1.0 section 3.11) for DIAGRAM.
+ We emit the diagram as a code block within the Markdown part
+ of the message. */
+
+json::object *
+sarif_builder::make_message_object_for_diagram (diagnostic_context *context,
+ const diagnostic_diagram &diagram)
+{
+ json::object *message_obj = new json::object ();
+
+ /* "text" property (SARIF v2.1.0 section 3.11.8). */
+ message_obj->set ("text", new json::string (diagram.get_alt_text ()));
+
+ char *saved_prefix = pp_take_prefix (context->printer);
+ pp_set_prefix (context->printer, NULL);
+
+ /* "To produce a code block in Markdown, simply indent every line of
+ the block by at least 4 spaces or 1 tab."
+ Here we use 4 spaces. */
+ diagram.get_canvas ().print_to_pp (context->printer, " ");
+ pp_set_prefix (context->printer, saved_prefix);
+
+ /* "markdown" property (SARIF v2.1.0 section 3.11.9). */
+ message_obj->set ("markdown",
+ new json::string (pp_formatted_text (context->printer)));
+
+ pp_clear_output_area (context->printer);
+
+ return message_obj;
+}
+
/* Make a multiformatMessageString object (SARIF v2.1.0 section 3.12)
for MSG. */
@@ -1630,6 +1711,16 @@ sarif_ice_handler (diagnostic_context *context)
fnotice (stderr, "Internal compiler error:\n");
}
+/* Callback for diagnostic_context::m_diagrams.m_emission_cb. */
+
+static void
+sarif_emit_diagram (diagnostic_context *context,
+ const diagnostic_diagram &diagram)
+{
+ gcc_assert (the_builder);
+ the_builder->emit_diagram (context, diagram);
+}
+
/* Populate CONTEXT in preparation for SARIF output (either to stderr, or
to a file). */
@@ -1645,6 +1736,7 @@ diagnostic_output_format_init_sarif (diagnostic_context *context)
context->end_group_cb = sarif_end_group;
context->print_path = NULL; /* handled in sarif_end_diagnostic. */
context->ice_handler_cb = sarif_ice_handler;
+ context->m_diagrams.m_emission_cb = sarif_emit_diagram;
/* The metadata is handled in SARIF format, rather than as text. */
context->show_cwe = false;
diff --git a/gcc/diagnostic-text-art.h b/gcc/diagnostic-text-art.h
new file mode 100644
index 000000000000..a0d8a78f52a1
--- /dev/null
+++ b/gcc/diagnostic-text-art.h
@@ -0,0 +1,49 @@
+/* Copyright (C) 2023 Free Software Foundation, Inc.
+ Contributed by David Malcolm .
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+. */
+
+#ifndef GCC_DIAGNOSTIC_TEXT_ART_H
+#define GCC_DIAGNOSTIC_TEXT_ART_H
+
+/* Values for -fdiagnostics-text-art-charset=. */
+
+enum diagnostic_text_art_charset
+{
+ /* No text art diagrams shall be emitted. */
+ DIAGNOSTICS_TEXT_ART_CHARSET_NONE,
+
+ /* Use pure ASCII for text art diagrams. */
+ DIAGNOSTICS_TEXT_ART_CHARSET_ASCII,
+
+ /* Use ASCII + conservative use of other unicode characters
+ in text art diagrams. */
+ DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE,
+
+ /* Use Emoji. */
+ DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI
+};
+
+const enum diagnostic_text_art_charset DIAGNOSTICS_TEXT_ART_CHARSET_DEFAULT
+ = DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI;
+
+extern void
+diagnostics_text_art_charset_init (diagnostic_context *context,
+ enum diagnostic_text_art_charset charset);
+
+
+#endif /* ! GCC_DIAGNOSTIC_TEXT_ART_H */
diff --git a/gcc/diagnostic.cc b/gcc/diagnostic.cc
index 0f093081161a..7c2289f06342 100644
--- a/gcc/diagnostic.cc
+++ b/gcc/diagnostic.cc
@@ -35,11 +35,14 @@ along with GCC; see the file COPYING3. If not see
#include "diagnostic-metadata.h"
#include "diagnostic-path.h"
#include "diagnostic-client-data-hooks.h"
+#include "diagnostic-text-art.h"
+#include "diagnostic-diagram.h"
#include "edit-context.h"
#include "selftest.h"
#include "selftest-diagnostic.h"
#include "opts.h"
#include "cpplib.h"
+#include "text-art/theme.h"
#ifdef HAVE_TERMIOS_H
# include
@@ -244,6 +247,10 @@ diagnostic_initialize (diagnostic_context *context, int n_opts)
context->ice_handler_cb = NULL;
context->includes_seen = NULL;
context->m_client_data_hooks = NULL;
+ context->m_diagrams.m_theme = NULL;
+ context->m_diagrams.m_emission_cb = NULL;
+ diagnostics_text_art_charset_init (context,
+ DIAGNOSTICS_TEXT_ART_CHARSET_DEFAULT);
}
/* Maybe initialize the color support. We require clients to do this
@@ -320,6 +327,12 @@ diagnostic_finish (diagnostic_context *context)
if (context->final_cb)
context->final_cb (context);
+ if (context->m_diagrams.m_theme)
+ {
+ delete context->m_diagrams.m_theme;
+ context->m_diagrams.m_theme = NULL;
+ }
+
diagnostic_file_cache_fini ();
XDELETEVEC (context->classify_diagnostic);
@@ -2174,6 +2187,33 @@ internal_error_no_backtrace (const char *gmsgid, ...)
gcc_unreachable ();
}
+
+/* Emit DIAGRAM to CONTEXT, respecting the output format. */
+
+void
+diagnostic_emit_diagram (diagnostic_context *context,
+ const diagnostic_diagram &diagram)
+{
+ if (context->m_diagrams.m_theme == nullptr)
+ return;
+
+ if (context->m_diagrams.m_emission_cb)
+ {
+ context->m_diagrams.m_emission_cb (context, diagram);
+ return;
+ }
+
+ /* Default implementation. */
+ char *saved_prefix = pp_take_prefix (context->printer);
+ pp_set_prefix (context->printer, NULL);
+ /* Use a newline before and after and a two-space indent
+ to make the diagram stand out a little from the wall of text. */
+ pp_newline (context->printer);
+ diagram.get_canvas ().print_to_pp (context->printer, " ");
+ pp_newline (context->printer);
+ pp_set_prefix (context->printer, saved_prefix);
+ pp_flush (context->printer);
+}
/* Special case error functions. Most are implemented in terms of the
above, or should be. */
@@ -2316,6 +2356,38 @@ diagnostic_output_format_init (diagnostic_context *context,
}
}
+/* Initialize CONTEXT->m_diagrams based on CHARSET.
+ Specifically, make a text_art::theme object for m_diagrams.m_theme,
+ (or NULL for "no diagrams"). */
+
+void
+diagnostics_text_art_charset_init (diagnostic_context *context,
+ enum diagnostic_text_art_charset charset)
+{
+ delete context->m_diagrams.m_theme;
+ switch (charset)
+ {
+ default:
+ gcc_unreachable ();
+
+ case DIAGNOSTICS_TEXT_ART_CHARSET_NONE:
+ context->m_diagrams.m_theme = NULL;
+ break;
+
+ case DIAGNOSTICS_TEXT_ART_CHARSET_ASCII:
+ context->m_diagrams.m_theme = new text_art::ascii_theme ();
+ break;
+
+ case DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE:
+ context->m_diagrams.m_theme = new text_art::unicode_theme ();
+ break;
+
+ case DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI:
+ context->m_diagrams.m_theme = new text_art::emoji_theme ();
+ break;
+ }
+}
+
/* Implementation of diagnostic_path::num_events vfunc for
simple_diagnostic_path: simply get the number of events in the vec. */
diff --git a/gcc/diagnostic.h b/gcc/diagnostic.h
index 9a51097f1465..00b828f230d0 100644
--- a/gcc/diagnostic.h
+++ b/gcc/diagnostic.h
@@ -24,6 +24,11 @@ along with GCC; see the file COPYING3. If not see
#include "pretty-print.h"
#include "diagnostic-core.h"
+namespace text_art
+{
+ class theme;
+} // namespace text_art
+
/* An enum for controlling what units to use for the column number
when diagnostics are output, used by the -fdiagnostics-column-unit option.
Tabs will be expanded or not according to the value of -ftabstop. The origin
@@ -170,6 +175,7 @@ class edit_context;
namespace json { class value; }
class diagnostic_client_data_hooks;
class logical_location;
+class diagnostic_diagram;
/* This data structure bundles altogether any information relevant to
the context of a diagnostic message. */
@@ -417,6 +423,18 @@ struct diagnostic_context
Used by SARIF output to give metadata about the client that's
producing diagnostics. */
diagnostic_client_data_hooks *m_client_data_hooks;
+
+ /* Support for diagrams. */
+ struct
+ {
+ /* Theme to use when generating diagrams.
+ Can be NULL (if text art is disabled). */
+ text_art::theme *m_theme;
+
+ /* Callback for emitting diagrams. */
+ void (*m_emission_cb) (diagnostic_context *context,
+ const diagnostic_diagram &diagram);
+ } m_diagrams;
};
inline void
@@ -619,4 +637,7 @@ extern bool warning_enabled_at (location_t, int);
extern char *get_cwe_url (int cwe);
+extern void diagnostic_emit_diagram (diagnostic_context *context,
+ const diagnostic_diagram &diagram);
+
#endif /* ! GCC_DIAGNOSTIC_H */
diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
index 8c17a81c7d90..7f76337161e8 100644
--- a/gcc/doc/invoke.texi
+++ b/gcc/doc/invoke.texi
@@ -317,7 +317,8 @@ Objective-C and Objective-C++ Dialects}.
-fno-show-column
-fdiagnostics-column-unit=@r{[}display@r{|}byte@r{]}
-fdiagnostics-column-origin=@var{origin}
--fdiagnostics-escape-format=@r{[}unicode@r{|}bytes@r{]}}
+-fdiagnostics-escape-format=@r{[}unicode@r{|}bytes@r{]}
+-fdiagnostics-text-art-charset=@r{[}none@r{|}ascii@r{|}unicode@r{|}emoji@r{]}}
@item Warning Options
@xref{Warning Options,,Options to Request or Suppress Warnings}.
@@ -5078,7 +5079,8 @@ options:
-fno-diagnostics-show-line-numbers
-fdiagnostics-color=never
-fdiagnostics-urls=never
--fdiagnostics-path-format=separate-events}
+-fdiagnostics-path-format=separate-events
+-fdiagnostics-text-art-charset=none}
In the future, if GCC changes the default appearance of its diagnostics, the
corresponding option to disable the new behavior will be added to this list.
@@ -5604,6 +5606,25 @@ Unicode characters. For the example above, the following will be printed:
before<80>after
@end smallexample
+@opindex fdiagnostics-text-art-charset
+@item -fdiagnostics-text-art-charset=@var{CHARSET}
+Some diagnostics can contain ``text art'' diagrams: visualizations created
+from text, intended to be viewed in a monospaced font.
+
+This option selects which characters should be used for printing such
+diagrams, if any. @var{CHARSET} is @samp{none}, @samp{ascii}, @samp{unicode},
+or @samp{emoji}.
+
+The @samp{none} value suppresses the printing of such diagrams.
+The @samp{ascii} value will ensure that such diagrams are pure ASCII
+(``ASCII art''). The @samp{unicode} value will allow for conservative use of
+unicode drawing characters (such as box-drawing characters). The @samp{emoji}
+value further adds the possibility of emoji in the output (such as emitting
+U+26A0 WARNING SIGN followed by U+FE0F VARIATION SELECTOR-16 to select the
+emoji variant of the character).
+
+The default is @samp{emoji}.
+
@opindex fdiagnostics-format
@item -fdiagnostics-format=@var{FORMAT}
Select a different format for printing diagnostics.
diff --git a/gcc/gcc.cc b/gcc/gcc.cc
index 08bdf28b0ada..fdfac0b4fe4b 100644
--- a/gcc/gcc.cc
+++ b/gcc/gcc.cc
@@ -46,6 +46,7 @@ compilation is specified by a string called a "spec". */
#include "spellcheck.h"
#include "opts-jobserver.h"
#include "common/common-target.h"
+#include "diagnostic-text-art.h"
#ifndef MATH_LIBRARY
#define MATH_LIBRARY "m"
@@ -4344,6 +4345,11 @@ driver_handle_option (struct gcc_options *opts,
break;
}
+ case OPT_fdiagnostics_text_art_charset_:
+ diagnostics_text_art_charset_init (dc,
+ (enum diagnostic_text_art_charset)value);
+ break;
+
case OPT_Wa_:
{
int prev, j;
diff --git a/gcc/opts-common.cc b/gcc/opts-common.cc
index 23ddcaa3b55d..f0c5f4836652 100644
--- a/gcc/opts-common.cc
+++ b/gcc/opts-common.cc
@@ -1068,6 +1068,7 @@ decode_cmdline_options_to_array (unsigned int argc, const char **argv,
"-fdiagnostics-color=never",
"-fdiagnostics-urls=never",
"-fdiagnostics-path-format=separate-events",
+ "-fdiagnostics-text-art-charset=none"
};
const int num_expanded = ARRAY_SIZE (expanded_args);
opt_array_len += num_expanded - 1;
diff --git a/gcc/opts.cc b/gcc/opts.cc
index 86b94d62b588..3087bdac2c6c 100644
--- a/gcc/opts.cc
+++ b/gcc/opts.cc
@@ -35,6 +35,7 @@ along with GCC; see the file COPYING3. If not see
#include "version.h"
#include "selftest.h"
#include "file-prefix-map.h"
+#include "diagnostic-text-art.h"
/* In this file all option sets are explicit. */
#undef OPTION_SET_P
@@ -2887,6 +2888,11 @@ common_handle_option (struct gcc_options *opts,
break;
}
+ case OPT_fdiagnostics_text_art_charset_:
+ diagnostics_text_art_charset_init (dc,
+ (enum diagnostic_text_art_charset)value);
+ break;
+
case OPT_fdiagnostics_parseable_fixits:
dc->extra_output_kind = (value
? EXTRA_DIAGNOSTIC_OUTPUT_fixits_v1
diff --git a/gcc/pretty-print.cc b/gcc/pretty-print.cc
index 7d294717f50c..3d789a238121 100644
--- a/gcc/pretty-print.cc
+++ b/gcc/pretty-print.cc
@@ -1828,6 +1828,35 @@ pp_string (pretty_printer *pp, const char *str)
pp_maybe_wrap_text (pp, str, str + strlen (str));
}
+/* Append code point C to the output area of PRETTY-PRINTER, encoding it
+ as UTF-8. */
+
+void
+pp_unicode_character (pretty_printer *pp, unsigned c)
+{
+ static const uchar masks[6] = { 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC };
+ static const uchar limits[6] = { 0x80, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE };
+ size_t nbytes;
+ uchar buf[6], *p = &buf[6];
+
+ nbytes = 1;
+ if (c < 0x80)
+ *--p = c;
+ else
+ {
+ do
+ {
+ *--p = ((c & 0x3F) | 0x80);
+ c >>= 6;
+ nbytes++;
+ }
+ while (c >= 0x3F || (c & limits[nbytes-1]));
+ *--p = (c | masks[nbytes-1]);
+ }
+
+ pp_append_r (pp, (const char *)p, nbytes);
+}
+
/* Append the leading N characters of STRING to the output area of
PRETTY-PRINTER, quoting in hexadecimal non-printable characters.
Setting N = -1 is as if N were set to strlen (STRING). The STRING
diff --git a/gcc/pretty-print.h b/gcc/pretty-print.h
index 0230a289df53..369be6e7ba76 100644
--- a/gcc/pretty-print.h
+++ b/gcc/pretty-print.h
@@ -401,6 +401,7 @@ extern void pp_indent (pretty_printer *);
extern void pp_newline (pretty_printer *);
extern void pp_character (pretty_printer *, int);
extern void pp_string (pretty_printer *, const char *);
+extern void pp_unicode_character (pretty_printer *, unsigned);
extern void pp_write_text_to_stream (pretty_printer *);
extern void pp_write_text_as_dot_label_to_stream (pretty_printer *, bool);
diff --git a/gcc/selftest-run-tests.cc b/gcc/selftest-run-tests.cc
index 915f2129702b..e2fc8f84b1b4 100644
--- a/gcc/selftest-run-tests.cc
+++ b/gcc/selftest-run-tests.cc
@@ -28,6 +28,7 @@ along with GCC; see the file COPYING3. If not see
#include "stringpool.h"
#include "attribs.h"
#include "analyzer/analyzer-selftests.h"
+#include "text-art/selftests.h"
/* This function needed to be split out from selftest.cc as it references
tests from the whole source tree, and so is within
@@ -118,6 +119,8 @@ selftest::run_tests ()
/* Run any lang-specific selftests. */
lang_hooks.run_lang_selftests ();
+ text_art_tests ();
+
/* Run the analyzer selftests (if enabled). */
ana::selftest::run_analyzer_selftests ();
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c
new file mode 100644
index 000000000000..e4239aab0326
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c
@@ -0,0 +1,57 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=ascii -fdiagnostics-color=never" } */
+
+int non_empty;
+
+/* { dg-begin-multiline-output "" }
+
+ A
+ B
+ C
+
+ { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+ ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
+ ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
+
+
+
+
+ ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
+ ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
+
+ { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+ +--+
+ |🙂|
+ +--+
+
+ { dg-end-multiline-output "" } */
+/* { dg-begin-multiline-output "" }
+
+ +-------+-----+---------------+---------------------+-----------------------+-----------------------+
+ |Offsets|Octet| 0 | 1 | 2 | 3 |
+ +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ | Octet | Bit |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|
+ +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ | 0 | 0 |Version| IHL | DSCP | ECN | Total Length |
+ +-------+-----+-------+-------+---------------+-----+--------+--------------------------------------+
+ | 4 | 32 | Identification | Flags | Fragment Offset |
+ +-------+-----+---------------+---------------------+--------+--------------------------------------+
+ | 8 | 64 | Time To Live | Protocol | Header Checksum |
+ +-------+-----+---------------+---------------------+-----------------------------------------------+
+ | 12 | 96 | Source IP Address |
+ +-------+-----+-------------------------------------------------------------------------------------+
+ | 16 | 128 | Destination IP Address |
+ +-------+-----+-------------------------------------------------------------------------------------+
+ | 20 | 160 | |
+ +-------+-----+ |
+ | ... | ... | Options |
+ +-------+-----+ |
+ | 56 | 448 | |
+ +-------+-----+-------------------------------------------------------------------------------------+
+
+ { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c
new file mode 100644
index 000000000000..0650428b1ce4
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c
@@ -0,0 +1,58 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=ascii -fdiagnostics-color=always" } */
+
+int non_empty;
+
+/* { dg-begin-multiline-output "" }
+
+ A
+ B
+ C
+
+ { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+ [38;2;0;0;0;48;2;240;217;181m[K♜ [38;2;0;0;0;48;2;181;136;99m[K♞ [38;2;0;0;0;48;2;240;217;181m[K♝ [38;2;0;0;0;48;2;181;136;99m[K♛ [38;2;0;0;0;48;2;240;217;181m[K♚ [38;2;0;0;0;48;2;181;136;99m[K♝ [38;2;0;0;0;48;2;240;217;181m[K♞ [38;2;0;0;0;48;2;181;136;99m[K♜ [m[K
+ [38;2;0;0;0;48;2;181;136;99m[K♟ [38;2;0;0;0;48;2;240;217;181m[K♟ [38;2;0;0;0;48;2;181;136;99m[K♟ [38;2;0;0;0;48;2;240;217;181m[K♟ [38;2;0;0;0;48;2;181;136;99m[K♟ [38;2;0;0;0;48;2;240;217;181m[K♟ [38;2;0;0;0;48;2;181;136;99m[K♟ [38;2;0;0;0;48;2;240;217;181m[K♟ [m[K
+ [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [m[K
+ [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [m[K
+ [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [m[K
+ [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [m[K
+ [38;2;255;255;255;48;2;240;217;181m[K♙ [38;2;255;255;255;48;2;181;136;99m[K♙ [38;2;255;255;255;48;2;240;217;181m[K♙ [38;2;255;255;255;48;2;181;136;99m[K♙ [38;2;255;255;255;48;2;240;217;181m[K♙ [38;2;255;255;255;48;2;181;136;99m[K♙ [38;2;255;255;255;48;2;240;217;181m[K♙ [38;2;255;255;255;48;2;181;136;99m[K♙ [m[K
+ [38;2;255;255;255;48;2;181;136;99m[K♖ [38;2;255;255;255;48;2;240;217;181m[K♘ [38;2;255;255;255;48;2;181;136;99m[K♗ [38;2;255;255;255;48;2;240;217;181m[K♕ [38;2;255;255;255;48;2;181;136;99m[K♔ [38;2;255;255;255;48;2;240;217;181m[K♗ [38;2;255;255;255;48;2;181;136;99m[K♘ [38;2;255;255;255;48;2;240;217;181m[K♖ [m[K
+
+ { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+ +--+
+ |🙂|
+ +--+
+
+ { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+ +-------+-----+---------------+---------------------+-----------------------+-----------------------+
+ |Offsets|Octet| 0 | 1 | 2 | 3 |
+ +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ | Octet | Bit |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|
+ +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+ | 0 | 0 |Version| IHL | DSCP | ECN | Total Length |
+ +-------+-----+-------+-------+---------------+-----+--------+--------------------------------------+
+ | 4 | 32 | Identification | Flags | Fragment Offset |
+ +-------+-----+---------------+---------------------+--------+--------------------------------------+
+ | 8 | 64 | Time To Live | Protocol | Header Checksum |
+ +-------+-----+---------------+---------------------+-----------------------------------------------+
+ | 12 | 96 | Source IP Address |
+ +-------+-----+-------------------------------------------------------------------------------------+
+ | 16 | 128 | Destination IP Address |
+ +-------+-----+-------------------------------------------------------------------------------------+
+ | 20 | 160 | |
+ +-------+-----+ |
+ | ... | ... | Options |
+ +-------+-----+ |
+ | 56 | 448 | |
+ +-------+-----+-------------------------------------------------------------------------------------+
+
+ { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c
new file mode 100644
index 000000000000..c8118b467599
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c
@@ -0,0 +1,5 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=none" } */
+
+int non_empty;
+
+/* We expect no output. */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c
new file mode 100644
index 000000000000..c9f5b36571a8
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c
@@ -0,0 +1,58 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode -fdiagnostics-color=never" } */
+
+int non_empty;
+
+/* { dg-begin-multiline-output "" }
+
+ A
+ B
+ C
+
+ { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+ ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
+ ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
+
+
+
+
+ ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
+ ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
+
+ { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+ ┌──┐
+ │🙂│
+ └──┘
+
+ { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+ ┌───────┬─────┬───────────────┬─────────────────────┬───────────────────────┬───────────────────────┐
+ │Offsets│Octet│ 0 │ 1 │ 2 │ 3 │
+ ├───────┼─────┼─┬─┬─┬─┬─┬─┬─┬─┼─┬─┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┤
+ │ Octet │ Bit │0│1│2│3│4│5│6│7│8│9│10│11│12│13│14│15│16│17│18│19│20│21│22│23│24│25│26│27│28│29│30│31│
+ ├───────┼─────┼─┴─┴─┴─┼─┴─┴─┴─┼─┴─┴──┴──┴──┴──┼──┴──┼──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┤
+ │ 0 │ 0 │Version│ IHL │ DSCP │ ECN │ Total Length │
+ ├───────┼─────┼───────┴───────┴───────────────┴─────┼────────┬──────────────────────────────────────┤
+ │ 4 │ 32 │ Identification │ Flags │ Fragment Offset │
+ ├───────┼─────┼───────────────┬─────────────────────┼────────┴──────────────────────────────────────┤
+ │ 8 │ 64 │ Time To Live │ Protocol │ Header Checksum │
+ ├───────┼─────┼───────────────┴─────────────────────┴───────────────────────────────────────────────┤
+ │ 12 │ 96 │ Source IP Address │
+ ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
+ │ 16 │ 128 │ Destination IP Address │
+ ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
+ │ 20 │ 160 │ │
+ ├───────┼─────┤ │
+ │ ... │ ... │ Options │
+ ├───────┼─────┤ │
+ │ 56 │ 448 │ │
+ └───────┴─────┴─────────────────────────────────────────────────────────────────────────────────────┘
+
+ { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c
new file mode 100644
index 000000000000..f402836f8895
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c
@@ -0,0 +1,59 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode -fdiagnostics-color=always" } */
+
+int non_empty;
+
+
+/* { dg-begin-multiline-output "" }
+
+ A
+ B
+ C
+
+ { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+ [38;2;0;0;0;48;2;240;217;181m[K♜ [38;2;0;0;0;48;2;181;136;99m[K♞ [38;2;0;0;0;48;2;240;217;181m[K♝ [38;2;0;0;0;48;2;181;136;99m[K♛ [38;2;0;0;0;48;2;240;217;181m[K♚ [38;2;0;0;0;48;2;181;136;99m[K♝ [38;2;0;0;0;48;2;240;217;181m[K♞ [38;2;0;0;0;48;2;181;136;99m[K♜ [m[K
+ [38;2;0;0;0;48;2;181;136;99m[K♟ [38;2;0;0;0;48;2;240;217;181m[K♟ [38;2;0;0;0;48;2;181;136;99m[K♟ [38;2;0;0;0;48;2;240;217;181m[K♟ [38;2;0;0;0;48;2;181;136;99m[K♟ [38;2;0;0;0;48;2;240;217;181m[K♟ [38;2;0;0;0;48;2;181;136;99m[K♟ [38;2;0;0;0;48;2;240;217;181m[K♟ [m[K
+ [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [m[K
+ [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [m[K
+ [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [m[K
+ [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [48;2;181;136;99m[K [48;2;240;217;181m[K [m[K
+ [38;2;255;255;255;48;2;240;217;181m[K♙ [38;2;255;255;255;48;2;181;136;99m[K♙ [38;2;255;255;255;48;2;240;217;181m[K♙ [38;2;255;255;255;48;2;181;136;99m[K♙ [38;2;255;255;255;48;2;240;217;181m[K♙ [38;2;255;255;255;48;2;181;136;99m[K♙ [38;2;255;255;255;48;2;240;217;181m[K♙ [38;2;255;255;255;48;2;181;136;99m[K♙ [m[K
+ [38;2;255;255;255;48;2;181;136;99m[K♖ [38;2;255;255;255;48;2;240;217;181m[K♘ [38;2;255;255;255;48;2;181;136;99m[K♗ [38;2;255;255;255;48;2;240;217;181m[K♕ [38;2;255;255;255;48;2;181;136;99m[K♔ [38;2;255;255;255;48;2;240;217;181m[K♗ [38;2;255;255;255;48;2;181;136;99m[K♘ [38;2;255;255;255;48;2;240;217;181m[K♖ [m[K
+
+ { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+ ┌──┐
+ │🙂│
+ └──┘
+
+ { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+ ┌───────┬─────┬───────────────┬─────────────────────┬───────────────────────┬───────────────────────┐
+ │Offsets│Octet│ 0 │ 1 │ 2 │ 3 │
+ ├───────┼─────┼─┬─┬─┬─┬─┬─┬─┬─┼─┬─┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┤
+ │ Octet │ Bit │0│1│2│3│4│5│6│7│8│9│10│11│12│13│14│15│16│17│18│19│20│21│22│23│24│25│26│27│28│29│30│31│
+ ├───────┼─────┼─┴─┴─┴─┼─┴─┴─┴─┼─┴─┴──┴──┴──┴──┼──┴──┼──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┤
+ │ 0 │ 0 │Version│ IHL │ DSCP │ ECN │ Total Length │
+ ├───────┼─────┼───────┴───────┴───────────────┴─────┼────────┬──────────────────────────────────────┤
+ │ 4 │ 32 │ Identification │ Flags │ Fragment Offset │
+ ├───────┼─────┼───────────────┬─────────────────────┼────────┴──────────────────────────────────────┤
+ │ 8 │ 64 │ Time To Live │ Protocol │ Header Checksum │
+ ├───────┼─────┼───────────────┴─────────────────────┴───────────────────────────────────────────────┤
+ │ 12 │ 96 │ Source IP Address │
+ ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
+ │ 16 │ 128 │ Destination IP Address │
+ ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
+ │ 20 │ 160 │ │
+ ├───────┼─────┤ │
+ │ ... │ ... │ Options │
+ ├───────┼─────┤ │
+ │ 56 │ 448 │ │
+ └───────┴─────┴─────────────────────────────────────────────────────────────────────────────────────┘
+
+ { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c b/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c
new file mode 100644
index 000000000000..27c341b9f2f5
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c
@@ -0,0 +1,257 @@
+/* { dg-options "-O" } */
+
+/* This plugin exercises the text_art code. */
+
+#include "gcc-plugin.h"
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "plugin-version.h"
+#include "diagnostic.h"
+#include "diagnostic-diagram.h"
+#include "text-art/canvas.h"
+#include "text-art/table.h"
+
+int plugin_is_GPL_compatible;
+
+using namespace text_art;
+
+/* Canvas tests. */
+
+static void
+emit_canvas (const canvas &c, const char *alt_text)
+{
+ diagnostic_diagram diagram (c, alt_text);
+ diagnostic_emit_diagram (global_dc, diagram);
+}
+
+static void
+test_abc ()
+{
+ style_manager sm;
+ canvas c (canvas::size_t (3, 3), sm);
+ c.paint (canvas::coord_t (0, 0), styled_unichar ('A'));
+ c.paint (canvas::coord_t (1, 1), styled_unichar ('B'));
+ c.paint (canvas::coord_t (2, 2), styled_unichar ('C'));
+ emit_canvas (c, "test_abc");
+}
+
+/* Test of procedural art using 24-bit color: chess starting position. */
+
+static void
+test_chessboard ()
+{
+ /* With the exception of NONE, these are in order of the chess symbols
+ in the Unicode Miscellaneous Symbols block. */
+ enum class piece { KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN, NONE };
+ enum class color { BLACK, WHITE, NONE };
+
+ style_manager sm;
+
+ /* We assume double-column chars for the pieces, so allow two canvas
+ columns per square. */
+ canvas canvas (canvas::size_t (16, 8), sm);
+
+ for (int x = 0; x < 8; x++)
+ for (int y = 0; y < 8; y++)
+ {
+ enum piece piece_kind;
+ enum color piece_color;
+ switch (y)
+ {
+ case 0:
+ case 7:
+ switch (x)
+ {
+ default:
+ gcc_unreachable ();
+ case 0:
+ piece_kind = piece::ROOK;
+ break;
+ case 1:
+ piece_kind = piece::KNIGHT;
+ break;
+ case 2:
+ piece_kind = piece::BISHOP;
+ break;
+ case 3:
+ piece_kind = piece::QUEEN;
+ break;
+ case 4:
+ piece_kind = piece::KING;
+ break;
+ case 5:
+ piece_kind = piece::BISHOP;
+ break;
+ case 6:
+ piece_kind = piece::KNIGHT;
+ break;
+ case 7:
+ piece_kind = piece::ROOK;
+ break;
+ }
+ piece_color = (y == 0) ? color::BLACK : color::WHITE;
+ break;
+ case 1:
+ case 6:
+ piece_kind = piece::PAWN;
+ piece_color = (y == 1) ? color::BLACK : color::WHITE;
+ break;
+ default:
+ piece_kind = piece::NONE;
+ piece_color = color::NONE;
+ break;
+ }
+
+ style s;
+ const bool white_square = (x + y) % 2 == 0;
+ if (white_square)
+ s.m_bg_color = style::color (0xf0, 0xd9, 0xb5);
+ else
+ s.m_bg_color = style::color (0xb5, 0x88, 0x63);
+ switch (piece_color)
+ {
+ default:
+ gcc_unreachable ();
+ case color::WHITE:
+ s.m_fg_color = style::color (0xff, 0xff, 0xff);
+ break;
+ case color::BLACK:
+ s.m_fg_color = style::color (0x00, 0x00, 0x00);
+ break;
+ case color::NONE:
+ break;
+ }
+ style::id_t style_id = sm.get_or_create_id (s);
+
+ cppchar_t ch;
+ if (piece_kind == piece::NONE)
+ ch = ' ';
+ else
+ {
+ const cppchar_t WHITE_KING = 0x2654;
+ const cppchar_t BLACK_KING = 0x265A;
+ cppchar_t base ((piece_color == color::WHITE)
+ ? WHITE_KING : BLACK_KING);
+ ch = base + ((int)piece_kind - (int)piece::KING);
+ }
+ canvas.paint (canvas::coord_t (x * 2, y),
+ canvas::cell_t (ch, false, style_id));
+ canvas.paint (canvas::coord_t (x * 2 + 1, y),
+ canvas::cell_t (' ', false, style_id));
+ }
+ emit_canvas (canvas, "test_chessboard");
+}
+
+/* Table tests. */
+
+static void
+emit_table (const table &table, const style_manager &sm, const char *alt_text)
+{
+ const text_art::theme *theme = global_dc->m_diagrams.m_theme;
+ if (!theme)
+ return;
+ canvas c (table.to_canvas (*theme, sm));
+ emit_canvas (c, alt_text);
+}
+
+static void
+test_double_width_chars ()
+{
+ style_manager sm;
+ table table (table::size_t (1, 1));
+ table.set_cell (table::coord_t (0,0),
+ styled_string ((cppchar_t)0x1f642));
+
+ emit_table (table, sm, "test_double_width_chars");
+}
+
+static void
+test_ipv4_header ()
+{
+ style_manager sm;
+ table table (table::size_t (34, 10));
+ table.set_cell (table::coord_t (0, 0), styled_string (sm, "Offsets"));
+ table.set_cell (table::coord_t (1, 0), styled_string (sm, "Octet"));
+ table.set_cell (table::coord_t (0, 1), styled_string (sm, "Octet"));
+ for (int octet = 0; octet < 4; octet++)
+ table.set_cell_span (table::rect_t (table::coord_t (2 + (octet * 8), 0),
+ table::size_t (8, 1)),
+ styled_string::from_fmt (sm, nullptr, "%i", octet));
+ table.set_cell (table::coord_t (1, 1), styled_string (sm, "Bit"));
+ for (int bit = 0; bit < 32; bit++)
+ table.set_cell (table::coord_t (bit + 2, 1),
+ styled_string::from_fmt (sm, nullptr, "%i", bit));
+ for (int word = 0; word < 6; word++)
+ {
+ table.set_cell (table::coord_t (0, word + 2),
+ styled_string::from_fmt (sm, nullptr, "%i", word * 4));
+ table.set_cell (table::coord_t (1, word + 2),
+ styled_string::from_fmt (sm, nullptr, "%i", word * 32));
+ }
+
+ table.set_cell (table::coord_t (0, 8), styled_string (sm, "..."));
+ table.set_cell (table::coord_t (1, 8), styled_string (sm, "..."));
+ table.set_cell (table::coord_t (0, 9), styled_string (sm, "56"));
+ table.set_cell (table::coord_t (1, 9), styled_string (sm, "448"));
+
+#define SET_BITS(FIRST, LAST, NAME) \
+ do { \
+ const int first = (FIRST); \
+ const int last = (LAST); \
+ const char *name = (NAME); \
+ const int row = first / 32; \
+ gcc_assert (last / 32 == row); \
+ table::rect_t rect (table::coord_t ((first % 32) + 2, row + 2), \
+ table::size_t (last + 1 - first , 1)); \
+ table.set_cell_span (rect, styled_string (sm, name)); \
+ } while (0)
+
+ SET_BITS (0, 3, "Version");
+ SET_BITS (4, 7, "IHL");
+ SET_BITS (8, 13, "DSCP");
+ SET_BITS (14, 15, "ECN");
+ SET_BITS (16, 31, "Total Length");
+
+ SET_BITS (32 + 0, 32 + 15, "Identification");
+ SET_BITS (32 + 16, 32 + 18, "Flags");
+ SET_BITS (32 + 19, 32 + 31, "Fragment Offset");
+
+ SET_BITS (64 + 0, 64 + 7, "Time To Live");
+ SET_BITS (64 + 8, 64 + 15, "Protocol");
+ SET_BITS (64 + 16, 64 + 31, "Header Checksum");
+
+ SET_BITS (96 + 0, 96 + 31, "Source IP Address");
+ SET_BITS (128 + 0, 128 + 31, "Destination IP Address");
+
+ table.set_cell_span(table::rect_t (table::coord_t (2, 7),
+ table::size_t (32, 3)),
+ styled_string (sm, "Options"));
+
+ emit_table (table, sm, "test_ipv4_header");
+}
+
+static void
+show_diagrams ()
+{
+ test_abc ();
+ test_chessboard ();
+ test_double_width_chars ();
+ test_ipv4_header ();
+}
+
+int
+plugin_init (struct plugin_name_args *plugin_info,
+ struct plugin_gcc_version *version)
+{
+ const char *plugin_name = plugin_info->base_name;
+ int argc = plugin_info->argc;
+ struct plugin_argument *argv = plugin_info->argv;
+
+ if (!plugin_default_version_check (version, &gcc_version))
+ return 1;
+
+ show_diagrams ();
+
+ return 0;
+}
diff --git a/gcc/testsuite/gcc.dg/plugin/plugin.exp b/gcc/testsuite/gcc.dg/plugin/plugin.exp
index 4d6304cd1007..60723a20eda9 100644
--- a/gcc/testsuite/gcc.dg/plugin/plugin.exp
+++ b/gcc/testsuite/gcc.dg/plugin/plugin.exp
@@ -114,6 +114,12 @@ set plugin_test_list [list \
diagnostic-path-format-inline-events-1.c \
diagnostic-path-format-inline-events-2.c \
diagnostic-path-format-inline-events-3.c } \
+ { diagnostic_plugin_test_text_art.c \
+ diagnostic-test-text-art-none.c \
+ diagnostic-test-text-art-ascii-bw.c \
+ diagnostic-test-text-art-ascii-color.c \
+ diagnostic-test-text-art-unicode-bw.c \
+ diagnostic-test-text-art-unicode-color.c } \
{ location_overflow_plugin.c \
location-overflow-test-1.c \
location-overflow-test-2.c \
diff --git a/gcc/text-art/box-drawing-chars.inc b/gcc/text-art/box-drawing-chars.inc
new file mode 100644
index 000000000000..a370255d56d1
--- /dev/null
+++ b/gcc/text-art/box-drawing-chars.inc
@@ -0,0 +1,18 @@
+/* Generated by contrib/unicode/gen-box-drawing-chars.py. */
+
+0x0020, /* " ": U+0020: SPACE */
+0x2576, /* "╶": U+2576: BOX DRAWINGS LIGHT RIGHT */
+0x2574, /* "╴": U+2574: BOX DRAWINGS LIGHT LEFT */
+0x2500, /* "─": U+2500: BOX DRAWINGS LIGHT HORIZONTAL */
+0x2577, /* "╷": U+2577: BOX DRAWINGS LIGHT DOWN */
+0x250C, /* "┌": U+250C: BOX DRAWINGS LIGHT DOWN AND RIGHT */
+0x2510, /* "┐": U+2510: BOX DRAWINGS LIGHT DOWN AND LEFT */
+0x252C, /* "┬": U+252C: BOX DRAWINGS LIGHT DOWN AND HORIZONTAL */
+0x2575, /* "╵": U+2575: BOX DRAWINGS LIGHT UP */
+0x2514, /* "└": U+2514: BOX DRAWINGS LIGHT UP AND RIGHT */
+0x2518, /* "┘": U+2518: BOX DRAWINGS LIGHT UP AND LEFT */
+0x2534, /* "┴": U+2534: BOX DRAWINGS LIGHT UP AND HORIZONTAL */
+0x2502, /* "│": U+2502: BOX DRAWINGS LIGHT VERTICAL */
+0x251C, /* "├": U+251C: BOX DRAWINGS LIGHT VERTICAL AND RIGHT */
+0x2524, /* "┤": U+2524: BOX DRAWINGS LIGHT VERTICAL AND LEFT */
+0x253C /* "┼": U+253C: BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL */
diff --git a/gcc/text-art/box-drawing.cc b/gcc/text-art/box-drawing.cc
new file mode 100644
index 000000000000..981d0b095cf8
--- /dev/null
+++ b/gcc/text-art/box-drawing.cc
@@ -0,0 +1,72 @@
+/* Procedural lookup of box drawing characters.
+ Copyright (C) 2023 Free Software Foundation, Inc.
+ Contributed by David Malcolm .
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+. */
+
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "text-art/box-drawing.h"
+#include "selftest.h"
+#include "text-art/selftests.h"
+
+
+/* According to
+ https://en.wikipedia.org/wiki/Box-drawing_character#Character_code
+ "DOS line- and box-drawing characters are not ordered in any programmatic
+ manner, so calculating a particular character shape needs to use a look-up
+ table. "
+ Hence this array. */
+static const cppchar_t box_drawing_chars[] = {
+#include "text-art/box-drawing-chars.inc"
+};
+
+cppchar_t
+text_art::get_box_drawing_char (directions line_dirs)
+{
+ const size_t idx = line_dirs.as_index ();
+ gcc_assert (idx < 16);
+ return box_drawing_chars[idx];
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* Run all selftests in this file. */
+
+void
+text_art_box_drawing_cc_tests ()
+{
+ ASSERT_EQ (text_art::get_box_drawing_char
+ (text_art::directions (false, false, false, false)),
+ ' ');
+ ASSERT_EQ (text_art::get_box_drawing_char
+ (text_art::directions (false, false, true, true)),
+ 0x2500); /* BOX DRAWINGS LIGHT HORIZONTAL */
+ ASSERT_EQ (text_art::get_box_drawing_char
+ (text_art::directions (true, true, false, false)),
+ 0x2502); /* BOX DRAWINGS LIGHT VERTICAL */
+ ASSERT_EQ (text_art::get_box_drawing_char
+ (text_art::directions (true, false, true, false)),
+ 0x2518); /* BOX DRAWINGS LIGHT UP AND LEFT */
+}
+
+} // namespace selftest
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/text-art/box-drawing.h b/gcc/text-art/box-drawing.h
new file mode 100644
index 000000000000..29f4d9921b34
--- /dev/null
+++ b/gcc/text-art/box-drawing.h
@@ -0,0 +1,32 @@
+/* Procedural lookup of box drawing characters.
+ Copyright (C) 2023 Free Software Foundation, Inc.
+ Contributed by David Malcolm .
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3, or (at your option)
+any later version.
+
+GCC is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+. */
+
+#ifndef GCC_TEXT_ART_BOX_DRAWING_H
+#define GCC_TEXT_ART_BOX_DRAWING_H
+
+#include "text-art/types.h"
+
+namespace text_art {
+
+extern cppchar_t get_box_drawing_char (directions line_dirs);
+
+} // namespace text_art
+
+#endif /* GCC_TEXT_ART_BOX_DRAWING_H */
diff --git a/gcc/text-art/canvas.cc b/gcc/text-art/canvas.cc
new file mode 100644
index 000000000000..f229612c9191
--- /dev/null
+++ b/gcc/text-art/canvas.cc
@@ -0,0 +1,437 @@
+/* Canvas for random-access procedural text art.
+ Copyright (C) 2023 Free Software Foundation, Inc.
+ Contributed by David Malcolm .
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+. */
+
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "pretty-print.h"
+#include "selftest.h"
+#include "text-art/selftests.h"
+#include "text-art/canvas.h"
+
+using namespace text_art;
+
+canvas::canvas (size_t size, const style_manager &style_mgr)
+: m_cells (size_t (size.w, size.h)),
+ m_style_mgr (style_mgr)
+{
+ m_cells.fill (cell_t (' '));
+}
+
+void
+canvas::paint (coord_t coord, styled_unichar ch)
+{
+ m_cells.set (coord, std::move (ch));
+}
+
+void
+canvas::paint_text (coord_t coord, const styled_string &text)
+{
+ for (auto ch : text)
+ {
+ paint (coord, ch);
+ if (ch.double_width_p ())
+ coord.x += 2;
+ else
+ coord.x++;
+ }
+}
+
+void
+canvas::fill (rect_t rect, cell_t c)
+{
+ for (int y = rect.get_min_y (); y < rect.get_next_y (); y++)
+ for (int x = rect.get_min_x (); x < rect.get_next_x (); x++)
+ paint(coord_t (x, y), c);
+}
+
+void
+canvas::debug_fill ()
+{
+ fill (rect_t (coord_t (0, 0), get_size ()), cell_t ('*'));
+}
+
+void
+canvas::print_to_pp (pretty_printer *pp,
+ const char *per_line_prefix) const
+{
+ for (int y = 0; y < m_cells.get_size ().h; y++)
+ {
+ style::id_t curr_style_id = 0;
+ if (per_line_prefix)
+ pp_string (pp, per_line_prefix);
+
+ pretty_printer line_pp;
+ line_pp.show_color = pp->show_color;
+ line_pp.url_format = pp->url_format;
+ const int final_x_in_row = get_final_x_in_row (y);
+ for (int x = 0; x <= final_x_in_row; x++)
+ {
+ if (x > 0)
+ {
+ const cell_t prev_cell = m_cells.get (coord_t (x - 1, y));
+ if (prev_cell.double_width_p ())
+ /* This cell is just a placeholder for the
+ 2nd column of a double width cell; skip it. */
+ continue;
+ }
+ const cell_t cell = m_cells.get (coord_t (x, y));
+ if (cell.get_style_id () != curr_style_id)
+ {
+ m_style_mgr.print_any_style_changes (&line_pp,
+ curr_style_id,
+ cell.get_style_id ());
+ curr_style_id = cell.get_style_id ();
+ }
+ pp_unicode_character (&line_pp, cell.get_code ());
+ if (cell.emoji_variant_p ())
+ /* Append U+FE0F VARIATION SELECTOR-16 to select the emoji
+ variation of the char. */
+ pp_unicode_character (&line_pp, 0xFE0F);
+ }
+ /* Reset the style at the end of each line. */
+ m_style_mgr.print_any_style_changes (&line_pp, curr_style_id, 0);
+
+ /* Print from line_pp to pp, stripping trailing whitespace from
+ the line. */
+ const char *line_buf = pp_formatted_text (&line_pp);
+ ::size_t len = strlen (line_buf);
+ while (len > 0)
+ {
+ if (line_buf[len - 1] == ' ')
+ len--;
+ else
+ break;
+ }
+ pp_append_text (pp, line_buf, line_buf + len);
+ pp_newline (pp);
+ }
+}
+
+DEBUG_FUNCTION void
+canvas::debug (bool styled) const
+{
+ pretty_printer pp;
+ if (styled)
+ {
+ pp_show_color (&pp) = true;
+ pp.url_format = determine_url_format (DIAGNOSTICS_URL_AUTO);
+ }
+ print_to_pp (&pp);
+ fprintf (stderr, "%s\n", pp_formatted_text (&pp));
+}
+
+/* Find right-most non-default cell in this row,
+ or -1 if all are default. */
+
+int
+canvas::get_final_x_in_row (int y) const
+{
+ for (int x = m_cells.get_size ().w - 1; x >= 0; x--)
+ {
+ cell_t cell = m_cells.get (coord_t (x, y));
+ if (cell.get_code () != ' '
+ || cell.get_style_id () != style::id_plain)
+ return x;
+ }
+ return -1;
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+static void
+test_blank ()
+{
+ style_manager sm;
+ canvas c (canvas::size_t (5, 5), sm);
+ ASSERT_CANVAS_STREQ (c, false,
+ ("\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"));
+}
+
+static void
+test_abc ()
+{
+ style_manager sm;
+ canvas c (canvas::size_t (3, 3), sm);
+ c.paint (canvas::coord_t (0, 0), styled_unichar ('A'));
+ c.paint (canvas::coord_t (1, 1), styled_unichar ('B'));
+ c.paint (canvas::coord_t (2, 2), styled_unichar ('C'));
+
+ ASSERT_CANVAS_STREQ (c, false,
+ "A\n B\n C\n");
+}
+
+static void
+test_debug_fill ()
+{
+ style_manager sm;
+ canvas c (canvas::size_t (5, 3), sm);
+ c.debug_fill();
+ ASSERT_CANVAS_STREQ (c, false,
+ ("*****\n"
+ "*****\n"
+ "*****\n"));
+}
+
+static void
+test_text ()
+{
+ style_manager sm;
+ canvas c (canvas::size_t (6, 1), sm);
+ c.paint_text (canvas::coord_t (0, 0), styled_string (sm, "012345"));
+ ASSERT_CANVAS_STREQ (c, false,
+ ("012345\n"));
+
+ /* Paint an emoji character that should occupy two canvas columns when
+ printed. */
+ c.paint_text (canvas::coord_t (2, 0), styled_string ((cppchar_t)0x1f642));
+ ASSERT_CANVAS_STREQ (c, false,
+ ("01🙂45\n"));
+}
+
+static void
+test_circle ()
+{
+ canvas::size_t sz (30, 30);
+ style_manager sm;
+ canvas canvas (sz, sm);
+ canvas::coord_t center (sz.w / 2, sz.h / 2);
+ const int radius = 12;
+ const int radius_squared = radius * radius;
+ for (int x = 0; x < sz.w; x++)
+ for (int y = 0; y < sz.h; y++)
+ {
+ int dx = x - center.x;
+ int dy = y - center.y;
+ char ch = "AB"[(x + y) % 2];
+ if (dx * dx + dy * dy < radius_squared)
+ canvas.paint (canvas::coord_t (x, y), styled_unichar (ch));
+ }
+ ASSERT_CANVAS_STREQ
+ (canvas, false,
+ ("\n"
+ "\n"
+ "\n"
+ "\n"
+ " BABABABAB\n"
+ " ABABABABABABA\n"
+ " ABABABABABABABA\n"
+ " ABABABABABABABABA\n"
+ " ABABABABABABABABABA\n"
+ " ABABABABABABABABABABA\n"
+ " BABABABABABABABABABAB\n"
+ " BABABABABABABABABABABAB\n"
+ " ABABABABABABABABABABABA\n"
+ " BABABABABABABABABABABAB\n"
+ " ABABABABABABABABABABABA\n"
+ " BABABABABABABABABABABAB\n"
+ " ABABABABABABABABABABABA\n"
+ " BABABABABABABABABABABAB\n"
+ " ABABABABABABABABABABABA\n"
+ " BABABABABABABABABABABAB\n"
+ " BABABABABABABABABABAB\n"
+ " ABABABABABABABABABABA\n"
+ " ABABABABABABABABABA\n"
+ " ABABABABABABABABA\n"
+ " ABABABABABABABA\n"
+ " ABABABABABABA\n"
+ " BABABABAB\n"
+ "\n"
+ "\n"
+ "\n"));
+}
+
+static void
+test_color_circle ()
+{
+ const canvas::size_t sz (10, 10);
+ const canvas::coord_t center (sz.w / 2, sz.h / 2);
+ const int outer_r2 = 25;
+ const int inner_r2 = 10;
+ style_manager sm;
+ canvas c (sz, sm);
+ for (int x = 0; x < sz.w; x++)
+ for (int y = 0; y < sz.h; y++)
+ {
+ const int dist_from_center_squared
+ = ((x - center.x) * (x - center.x) + (y - center.y) * (y - center.y));
+ if (dist_from_center_squared < outer_r2)
+ {
+ style s;
+ if (dist_from_center_squared < inner_r2)
+ s.m_fg_color = style::named_color::RED;
+ else
+ s.m_fg_color = style::named_color::GREEN;
+ c.paint (canvas::coord_t (x, y),
+ styled_unichar ('*', false, sm.get_or_create_id (s)));
+ }
+ }
+ ASSERT_EQ (sm.get_num_styles (), 3);
+ ASSERT_CANVAS_STREQ
+ (c, false,
+ ("\n"
+ " *****\n"
+ " *******\n"
+ " *********\n"
+ " *********\n"
+ " *********\n"
+ " *********\n"
+ " *********\n"
+ " *******\n"
+ " *****\n"));
+ ASSERT_CANVAS_STREQ
+ (c, true,
+ ("\n"
+ " [32m[K*****[m[K\n"
+ " [32m[K***[31m[K*[32m[K***[m[K\n"
+ " [32m[K**[31m[K*****[32m[K**[m[K\n"
+ " [32m[K**[31m[K*****[32m[K**[m[K\n"
+ " [32m[K*[31m[K*******[32m[K*[m[K\n"
+ " [32m[K**[31m[K*****[32m[K**[m[K\n"
+ " [32m[K**[31m[K*****[32m[K**[m[K\n"
+ " [32m[K***[31m[K*[32m[K***[m[K\n"
+ " [32m[K*****[m[K\n"));
+}
+
+static void
+test_bold ()
+{
+ auto_fix_quotes fix_quotes;
+ style_manager sm;
+ styled_string s (styled_string::from_fmt (sm, nullptr,
+ "before %qs after", "foo"));
+ canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
+ c.paint_text (canvas::coord_t (0, 0), s);
+ ASSERT_CANVAS_STREQ (c, false,
+ "before `foo' after\n");
+ ASSERT_CANVAS_STREQ (c, true,
+ "before `[00;01m[Kfoo[00m[K' after\n");
+}
+
+static void
+test_emoji ()
+{
+ style_manager sm;
+ styled_string s (0x26A0, /* U+26A0 WARNING SIGN. */
+ true);
+ canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
+ c.paint_text (canvas::coord_t (0, 0), s);
+ ASSERT_CANVAS_STREQ (c, false, "⚠️\n");
+ ASSERT_CANVAS_STREQ (c, true, "⚠️\n");
+}
+
+static void
+test_emoji_2 ()
+{
+ style_manager sm;
+ styled_string s;
+ s.append (styled_string (0x26A0, /* U+26A0 WARNING SIGN. */
+ true));
+ s.append (styled_string (sm, "test"));
+ ASSERT_EQ (s.size (), 5);
+ ASSERT_EQ (s.calc_canvas_width (), 5);
+ canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
+ c.paint_text (canvas::coord_t (0, 0), s);
+ ASSERT_CANVAS_STREQ (c, false,
+ /* U+26A0 WARNING SIGN, as UTF-8: 0xE2 0x9A 0xA0. */
+ "\xE2\x9A\xA0"
+ /* U+FE0F VARIATION SELECTOR-16, as UTF-8: 0xEF 0xB8 0x8F. */
+ "\xEF\xB8\x8F"
+ "test\n");
+}
+
+static void
+test_canvas_urls ()
+{
+ style_manager sm;
+ canvas canvas (canvas::size_t (9, 3), sm);
+ styled_string foo_ss (sm, "foo");
+ foo_ss.set_url (sm, "https://www.example.com/foo");
+ styled_string bar_ss (sm, "bar");
+ bar_ss.set_url (sm, "https://www.example.com/bar");
+ canvas.paint_text(canvas::coord_t (1, 1), foo_ss);
+ canvas.paint_text(canvas::coord_t (5, 1), bar_ss);
+
+ ASSERT_CANVAS_STREQ (canvas, false,
+ ("\n"
+ " foo bar\n"
+ "\n"));
+ {
+ pretty_printer pp;
+ pp_show_color (&pp) = true;
+ pp.url_format = URL_FORMAT_ST;
+ assert_canvas_streq (SELFTEST_LOCATION, canvas, &pp,
+ (/* Line 1. */
+ "\n"
+ /* Line 2. */
+ " "
+ "\33]8;;https://www.example.com/foo\33\\foo\33]8;;\33\\"
+ " "
+ "\33]8;;https://www.example.com/bar\33\\bar\33]8;;\33\\"
+ "\n"
+ /* Line 3. */
+ "\n"));
+ }
+
+ {
+ pretty_printer pp;
+ pp_show_color (&pp) = true;
+ pp.url_format = URL_FORMAT_BEL;
+ assert_canvas_streq (SELFTEST_LOCATION, canvas, &pp,
+ (/* Line 1. */
+ "\n"
+ /* Line 2. */
+ " "
+ "\33]8;;https://www.example.com/foo\afoo\33]8;;\a"
+ " "
+ "\33]8;;https://www.example.com/bar\abar\33]8;;\a"
+ "\n"
+ /* Line 3. */
+ "\n"));
+ }
+}
+
+/* Run all selftests in this file. */
+
+void
+text_art_canvas_cc_tests ()
+{
+ test_blank ();
+ test_abc ();
+ test_debug_fill ();
+ test_text ();
+ test_circle ();
+ test_color_circle ();
+ test_bold ();
+ test_emoji ();
+ test_emoji_2 ();
+ test_canvas_urls ();
+}
+
+} // namespace selftest
+
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/text-art/canvas.h b/gcc/text-art/canvas.h
new file mode 100644
index 000000000000..495497754f5e
--- /dev/null
+++ b/gcc/text-art/canvas.h
@@ -0,0 +1,74 @@
+/* Canvas for random-access procedural text art.
+ Copyright (C) 2023 Free Software Foundation, Inc.
+ Contributed by David Malcolm .
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3, or (at your option)
+any later version.
+
+GCC is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+. */
+
+#ifndef GCC_TEXT_ART_CANVAS_H
+#define GCC_TEXT_ART_CANVAS_H
+
+#include "text-art/types.h"
+
+namespace text_art {
+
+class canvas;
+
+/* A 2 dimensional grid of text cells (a "canvas"), which
+ can be written to ("painted") via random access, and then
+ written out to a pretty_printer once the picture is complete.
+
+ Each text cell can be styled independently (colorization,
+ URLs, etc). */
+
+class canvas
+{
+ public:
+ typedef styled_unichar cell_t;
+ typedef size size_t;
+ typedef coord coord_t;
+ typedef range range_t;
+ typedef rect rect_t;
+
+ canvas (size_t size, const style_manager &style_mgr);
+
+ size_t get_size () const { return m_cells.get_size (); }
+
+ void paint (coord_t coord, cell_t c);
+ void paint_text (coord_t coord, const styled_string &text);
+
+ void fill (rect_t rect, cell_t c);
+ void debug_fill ();
+
+ void print_to_pp (pretty_printer *pp,
+ const char *per_line_prefix = NULL) const;
+ void debug (bool styled) const;
+
+ const cell_t &get (coord_t coord) const
+ {
+ return m_cells.get (coord);
+ }
+
+ private:
+ int get_final_x_in_row (int y) const;
+
+ array2 m_cells;
+ const style_manager &m_style_mgr;
+};
+
+} // namespace text_art
+
+#endif /* GCC_TEXT_ART_CANVAS_H */
diff --git a/gcc/text-art/ruler.cc b/gcc/text-art/ruler.cc
new file mode 100644
index 000000000000..80c623f77ba7
--- /dev/null
+++ b/gcc/text-art/ruler.cc
@@ -0,0 +1,723 @@
+/* Classes for printing labelled rulers.
+ Copyright (C) 2023 Free Software Foundation, Inc.
+ Contributed by David Malcolm .
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+. */
+
+#include "config.h"
+#define INCLUDE_ALGORITHM
+#include "system.h"
+#include "coretypes.h"
+#include "pretty-print.h"
+#include "selftest.h"
+#include "text-art/selftests.h"
+#include "text-art/ruler.h"
+#include "text-art/theme.h"
+
+using namespace text_art;
+
+void
+x_ruler::add_label (const canvas::range_t &r,
+ styled_string text,
+ style::id_t style_id,
+ label_kind kind)
+{
+ m_labels.push_back (label (r, std::move (text), style_id, kind));
+ m_has_layout = false;
+}
+
+int
+x_ruler::get_canvas_y (int rel_y) const
+{
+ gcc_assert (rel_y >= 0);
+ gcc_assert (rel_y < m_size.h);
+ switch (m_label_dir)
+ {
+ default:
+ gcc_unreachable ();
+ case label_dir::ABOVE:
+ return m_size.h - (rel_y + 1);
+ case label_dir::BELOW:
+ return rel_y;
+ }
+}
+
+void
+x_ruler::paint_to_canvas (canvas &canvas,
+ canvas::coord_t offset,
+ const theme &theme)
+{
+ ensure_layout ();
+
+ if (0)
+ canvas.fill (canvas::rect_t (offset, m_size),
+ canvas::cell_t ('*'));
+
+ for (size_t idx = 0; idx < m_labels.size (); idx++)
+ {
+ const label &iter_label = m_labels[idx];
+
+ /* Paint the ruler itself. */
+ const int ruler_rel_y = get_canvas_y (0);
+ for (int rel_x = iter_label.m_range.start;
+ rel_x < iter_label.m_range.next;
+ rel_x++)
+ {
+ enum theme::cell_kind kind = theme::cell_kind::X_RULER_MIDDLE;
+
+ if (rel_x == iter_label.m_range.start)
+ {
+ kind = theme::cell_kind::X_RULER_LEFT_EDGE;
+ if (idx > 0)
+ {
+ const label &prev_label = m_labels[idx - 1];
+ if (prev_label.m_range.get_max () == iter_label.m_range.start)
+ kind = theme::cell_kind::X_RULER_INTERNAL_EDGE;
+ }
+ }
+ else if (rel_x == iter_label.m_range.get_max ())
+ kind = theme::cell_kind::X_RULER_RIGHT_EDGE;
+ else if (rel_x == iter_label.m_connector_x)
+ {
+ switch (m_label_dir)
+ {
+ default:
+ gcc_unreachable ();
+ case label_dir::ABOVE:
+ kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_ABOVE;
+ break;
+ case label_dir::BELOW:
+ kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_BELOW;
+ break;
+ }
+ }
+ canvas.paint (canvas::coord_t (rel_x, ruler_rel_y) + offset,
+ theme.get_cell (kind, iter_label.m_style_id));
+ }
+
+ /* Paint the connector to the text. */
+ for (int connector_rel_y = 1;
+ connector_rel_y < iter_label.m_text_rect.get_min_y ();
+ connector_rel_y++)
+ {
+ canvas.paint
+ ((canvas::coord_t (iter_label.m_connector_x,
+ get_canvas_y (connector_rel_y))
+ + offset),
+ theme.get_cell (theme::cell_kind::X_RULER_VERTICAL_CONNECTOR,
+ iter_label.m_style_id));
+ }
+
+ /* Paint the text. */
+ switch (iter_label.m_kind)
+ {
+ default:
+ gcc_unreachable ();
+ case x_ruler::label_kind::TEXT:
+ canvas.paint_text
+ ((canvas::coord_t (iter_label.m_text_rect.get_min_x (),
+ get_canvas_y (iter_label.m_text_rect.get_min_y ()))
+ + offset),
+ iter_label.m_text);
+ break;
+
+ case x_ruler::label_kind::TEXT_WITH_BORDER:
+ {
+ const canvas::range_t rel_x_range
+ (iter_label.m_text_rect.get_x_range ());
+
+ enum theme::cell_kind inner_left_kind;
+ enum theme::cell_kind inner_connector_kind;
+ enum theme::cell_kind inner_right_kind;
+ enum theme::cell_kind outer_left_kind;
+ enum theme::cell_kind outer_right_kind;
+
+ switch (m_label_dir)
+ {
+ default:
+ gcc_unreachable ();
+ case label_dir::ABOVE:
+ outer_left_kind = theme::cell_kind::TEXT_BORDER_TOP_LEFT;
+ outer_right_kind = theme::cell_kind::TEXT_BORDER_TOP_RIGHT;
+ inner_left_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_LEFT;
+ inner_connector_kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_BELOW;
+ inner_right_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_RIGHT;
+ break;
+ case label_dir::BELOW:
+ inner_left_kind = theme::cell_kind::TEXT_BORDER_TOP_LEFT;
+ inner_connector_kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_ABOVE;
+ inner_right_kind = theme::cell_kind::TEXT_BORDER_TOP_RIGHT;
+ outer_left_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_LEFT;
+ outer_right_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_RIGHT;
+ break;
+ }
+ /* Inner border. */
+ {
+ const int rel_canvas_y
+ = get_canvas_y (iter_label.m_text_rect.get_min_y ());
+ /* Left corner. */
+ canvas.paint ((canvas::coord_t (rel_x_range.get_min (),
+ rel_canvas_y)
+ + offset),
+ theme.get_cell (inner_left_kind,
+ iter_label.m_style_id));
+ /* Edge. */
+ const canvas::cell_t edge_border_cell
+ = theme.get_cell (theme::cell_kind::TEXT_BORDER_HORIZONTAL,
+ iter_label.m_style_id);
+ const canvas::cell_t connector_border_cell
+ = theme.get_cell (inner_connector_kind,
+ iter_label.m_style_id);
+ for (int rel_x = rel_x_range.get_min () + 1;
+ rel_x < rel_x_range.get_max ();
+ rel_x++)
+ if (rel_x == iter_label.m_connector_x)
+ canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y)
+ + offset),
+ connector_border_cell);
+ else
+ canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y)
+ + offset),
+ edge_border_cell);
+
+ /* Right corner. */
+ canvas.paint ((canvas::coord_t (rel_x_range.get_max (),
+ rel_canvas_y)
+ + offset),
+ theme.get_cell (inner_right_kind,
+ iter_label.m_style_id));
+ }
+
+ {
+ const int rel_canvas_y
+ = get_canvas_y (iter_label.m_text_rect.get_min_y () + 1);
+ const canvas::cell_t border_cell
+ = theme.get_cell (theme::cell_kind::TEXT_BORDER_VERTICAL,
+ iter_label.m_style_id);
+
+ /* Left border. */
+ canvas.paint ((canvas::coord_t (rel_x_range.get_min (),
+ rel_canvas_y)
+ + offset),
+ border_cell);
+ /* Text. */
+ canvas.paint_text ((canvas::coord_t (rel_x_range.get_min () + 1,
+ rel_canvas_y)
+ + offset),
+ iter_label.m_text);
+ /* Right border. */
+ canvas.paint ((canvas::coord_t (rel_x_range.get_max (),
+ rel_canvas_y)
+ + offset),
+ border_cell);
+ }
+
+ /* Outer border. */
+ {
+ const int rel_canvas_y
+ = get_canvas_y (iter_label.m_text_rect.get_max_y ());
+ /* Left corner. */
+ canvas.paint ((canvas::coord_t (rel_x_range.get_min (),
+ rel_canvas_y)
+ + offset),
+ theme.get_cell (outer_left_kind,
+ iter_label.m_style_id));
+ /* Edge. */
+ const canvas::cell_t border_cell
+ = theme.get_cell (theme::cell_kind::TEXT_BORDER_HORIZONTAL,
+ iter_label.m_style_id);
+ for (int rel_x = rel_x_range.get_min () + 1;
+ rel_x < rel_x_range.get_max ();
+ rel_x++)
+ canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y)
+ + offset),
+ border_cell);
+
+ /* Right corner. */
+ canvas.paint ((canvas::coord_t (rel_x_range.get_max (),
+ rel_canvas_y)
+ + offset),
+ theme.get_cell (outer_right_kind,
+ iter_label.m_style_id));
+ }
+ }
+ break;
+ }
+ }
+}
+
+DEBUG_FUNCTION void
+x_ruler::debug (const style_manager &sm)
+{
+ canvas c (get_size (), sm);
+ paint_to_canvas (c, canvas::coord_t (0, 0), unicode_theme ());
+ c.debug (true);
+}
+
+x_ruler::label::label (const canvas::range_t &range,
+ styled_string text,
+ style::id_t style_id,
+ label_kind kind)
+: m_range (range),
+ m_text (std::move (text)),
+ m_style_id (style_id),
+ m_kind (kind),
+ m_text_rect (canvas::coord_t (0, 0),
+ canvas::size_t (m_text.calc_canvas_width (), 1)),
+ m_connector_x ((m_range.get_min () + m_range.get_max ()) / 2)
+{
+ if (kind == label_kind::TEXT_WITH_BORDER)
+ {
+ m_text_rect.m_size.w += 2;
+ m_text_rect.m_size.h += 2;
+ }
+}
+
+bool
+x_ruler::label::operator< (const label &other) const
+{
+ int cmp = m_range.start - other.m_range.start;
+ if (cmp)
+ return cmp < 0;
+ return m_range.next < other.m_range.next;
+}
+
+void
+x_ruler::ensure_layout ()
+{
+ if (m_has_layout)
+ return;
+ update_layout ();
+ m_has_layout = true;
+}
+
+void
+x_ruler::update_layout ()
+{
+ if (m_labels.empty ())
+ return;
+
+ std::sort (m_labels.begin (), m_labels.end ());
+
+ /* Place labels. */
+ int ruler_width = m_labels.back ().m_range.get_next ();
+ int width_with_labels = ruler_width;
+
+ /* Get x coordinates of text parts of each label
+ (m_text_rect.m_top_left.x for each label). */
+ for (size_t idx = 0; idx < m_labels.size (); idx++)
+ {
+ label &iter_label = m_labels[idx];
+ /* Attempt to center the text label. */
+ int min_x;
+ if (idx > 0)
+ {
+ /* ...but don't overlap with the connector to the left. */
+ int left_neighbor_connector_x = m_labels[idx - 1].m_connector_x;
+ min_x = left_neighbor_connector_x + 1;
+ }
+ else
+ {
+ /* ...or go beyond the leftmost column. */
+ min_x = 0;
+ }
+ int connector_x = iter_label.m_connector_x;
+ int centered_x
+ = connector_x - ((int)iter_label.m_text_rect.get_width () / 2);
+ int text_x = std::max (min_x, centered_x);
+ iter_label.m_text_rect.m_top_left.x = text_x;
+ }
+
+ /* Now walk backwards trying to place them vertically,
+ setting m_text_rect.m_top_left.y for each label,
+ consolidating the rows where possible.
+ The y cooordinates are stored with respect to label_dir::BELOW. */
+ int label_y = 2;
+ for (int idx = m_labels.size () - 1; idx >= 0; idx--)
+ {
+ label &iter_label = m_labels[idx];
+ /* Does it fit on the same row as the text label to the right? */
+ size_t text_len = iter_label.m_text_rect.get_width ();
+ /* Get the x-coord of immediately beyond iter_label's text. */
+ int next_x = iter_label.m_text_rect.get_min_x () + text_len;
+ if (idx < (int)m_labels.size () - 1)
+ {
+ if (next_x >= m_labels[idx + 1].m_text_rect.get_min_x ())
+ {
+ /* If not, start a new row. */
+ label_y += m_labels[idx + 1].m_text_rect.get_height ();
+ }
+ }
+ iter_label.m_text_rect.m_top_left.y = label_y;
+ width_with_labels = std::max (width_with_labels, next_x);
+ }
+
+ m_size = canvas::size_t (width_with_labels,
+ label_y + m_labels[0].m_text_rect.get_height ());
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+static void
+assert_x_ruler_streq (const location &loc,
+ x_ruler &ruler,
+ const theme &theme,
+ const style_manager &sm,
+ bool styled,
+ const char *expected_str)
+{
+ canvas c (ruler.get_size (), sm);
+ ruler.paint_to_canvas (c, canvas::coord_t (0, 0), theme);
+ if (0)
+ c.debug (styled);
+ assert_canvas_streq (loc, c, styled, expected_str);
+}
+
+#define ASSERT_X_RULER_STREQ(RULER, THEME, SM, STYLED, EXPECTED_STR) \
+ SELFTEST_BEGIN_STMT \
+ assert_x_ruler_streq ((SELFTEST_LOCATION), \
+ (RULER), \
+ (THEME), \
+ (SM), \
+ (STYLED), \
+ (EXPECTED_STR)); \
+ SELFTEST_END_STMT
+
+static void
+test_single ()
+{
+ style_manager sm;
+ x_ruler r (x_ruler::label_dir::BELOW);
+ r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"),
+ style::id_plain, x_ruler::label_kind::TEXT);
+ ASSERT_X_RULER_STREQ
+ (r, ascii_theme (), sm, true,
+ ("|~~~~+~~~~|\n"
+ " |\n"
+ " foo\n"));
+ ASSERT_X_RULER_STREQ
+ (r, unicode_theme (), sm, true,
+ ("├────┬────┤\n"
+ " │\n"
+ " foo\n"));
+}
+
+static void
+test_single_above ()
+{
+ style_manager sm;
+ x_ruler r (x_ruler::label_dir::ABOVE);
+ r.add_label (canvas::range_t (0, 11), styled_string (sm, "hello world"),
+ style::id_plain);
+ ASSERT_X_RULER_STREQ
+ (r, ascii_theme (), sm, true,
+ ("hello world\n"
+ " |\n"
+ "|~~~~+~~~~|\n"));
+ ASSERT_X_RULER_STREQ
+ (r, unicode_theme (), sm, true,
+ ("hello world\n"
+ " │\n"
+ "├────┴────┤\n"));
+}
+
+static void
+test_multiple_contiguous ()
+{
+ style_manager sm;
+ x_ruler r (x_ruler::label_dir::BELOW);
+ r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"),
+ style::id_plain);
+ r.add_label (canvas::range_t (10, 16), styled_string (sm, "bar"),
+ style::id_plain);
+ ASSERT_X_RULER_STREQ
+ (r, ascii_theme (), sm, true,
+ ("|~~~~+~~~~|~+~~|\n"
+ " | |\n"
+ " foo bar\n"));
+ ASSERT_X_RULER_STREQ
+ (r, unicode_theme (), sm, true,
+ ("├────┬────┼─┬──┤\n"
+ " │ │\n"
+ " foo bar\n"));
+}
+
+static void
+test_multiple_contiguous_above ()
+{
+ style_manager sm;
+ x_ruler r (x_ruler::label_dir::ABOVE);
+ r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"),
+ style::id_plain);
+ r.add_label (canvas::range_t (10, 16), styled_string (sm, "bar"),
+ style::id_plain);
+ ASSERT_X_RULER_STREQ
+ (r, ascii_theme (), sm, true,
+ (" foo bar\n"
+ " | |\n"
+ "|~~~~+~~~~|~+~~|\n"));
+ ASSERT_X_RULER_STREQ
+ (r, unicode_theme (), sm, true,
+ (" foo bar\n"
+ " │ │\n"
+ "├────┴────┼─┴──┤\n"));
+}
+
+static void
+test_multiple_contiguous_abutting_labels ()
+{
+ style_manager sm;
+ x_ruler r (x_ruler::label_dir::BELOW);
+ r.add_label (canvas::range_t (0, 11), styled_string (sm, "12345678"),
+ style::id_plain);
+ r.add_label (canvas::range_t (10, 16), styled_string (sm, "1234678"),
+ style::id_plain);
+ ASSERT_X_RULER_STREQ
+ (r, unicode_theme (), sm, true,
+ ("├────┬────┼─┬──┤\n"
+ " │ │\n"
+ " │ 1234678\n"
+ " 12345678\n"));
+}
+
+static void
+test_multiple_contiguous_overlapping_labels ()
+{
+ style_manager sm;
+ x_ruler r (x_ruler::label_dir::BELOW);
+ r.add_label (canvas::range_t (0, 11), styled_string (sm, "123456789"),
+ style::id_plain);
+ r.add_label (canvas::range_t (10, 16), styled_string (sm, "12346789"),
+ style::id_plain);
+ ASSERT_X_RULER_STREQ
+ (r, unicode_theme (), sm, true,
+ ("├────┬────┼─┬──┤\n"
+ " │ │\n"
+ " │ 12346789\n"
+ " 123456789\n"));
+}
+static void
+test_abutting_left_border ()
+{
+ style_manager sm;
+ x_ruler r (x_ruler::label_dir::BELOW);
+ r.add_label (canvas::range_t (0, 6),
+ styled_string (sm, "this is a long label"),
+ style::id_plain);
+ ASSERT_X_RULER_STREQ
+ (r, unicode_theme (), sm, true,
+ ("├─┬──┤\n"
+ " │\n"
+ "this is a long label\n"));
+}
+
+static void
+test_too_long_to_consolidate_vertically ()
+{
+ style_manager sm;
+ x_ruler r (x_ruler::label_dir::BELOW);
+ r.add_label (canvas::range_t (0, 11),
+ styled_string (sm, "long string A"),
+ style::id_plain);
+ r.add_label (canvas::range_t (10, 16),
+ styled_string (sm, "long string B"),
+ style::id_plain);
+ ASSERT_X_RULER_STREQ
+ (r, unicode_theme (), sm, true,
+ ("├────┬────┼─┬──┤\n"
+ " │ │\n"
+ " │long string B\n"
+ "long string A\n"));
+}
+
+static void
+test_abutting_neighbor ()
+{
+ style_manager sm;
+ x_ruler r (x_ruler::label_dir::BELOW);
+ r.add_label (canvas::range_t (0, 11),
+ styled_string (sm, "very long string A"),
+ style::id_plain);
+ r.add_label (canvas::range_t (10, 16),
+ styled_string (sm, "very long string B"),
+ style::id_plain);
+ ASSERT_X_RULER_STREQ
+ (r, unicode_theme (), sm, true,
+ ("├────┬────┼─┬──┤\n"
+ " │ │\n"
+ " │very long string B\n"
+ "very long string A\n"));
+}
+
+static void
+test_gaps ()
+{
+ style_manager sm;
+ x_ruler r (x_ruler::label_dir::BELOW);
+ r.add_label (canvas::range_t (0, 5),
+ styled_string (sm, "foo"),
+ style::id_plain);
+ r.add_label (canvas::range_t (10, 15),
+ styled_string (sm, "bar"),
+ style::id_plain);
+ ASSERT_X_RULER_STREQ
+ (r, ascii_theme (), sm, true,
+ ("|~+~| |~+~|\n"
+ " | |\n"
+ " foo bar\n"));
+}
+
+static void
+test_styled ()
+{
+ style_manager sm;
+ style s1, s2;
+ s1.m_bold = true;
+ s1.m_fg_color = style::named_color::YELLOW;
+ s2.m_bold = true;
+ s2.m_fg_color = style::named_color::BLUE;
+ style::id_t sid1 = sm.get_or_create_id (s1);
+ style::id_t sid2 = sm.get_or_create_id (s2);
+
+ x_ruler r (x_ruler::label_dir::BELOW);
+ r.add_label (canvas::range_t (0, 5), styled_string (sm, "foo"), sid1);
+ r.add_label (canvas::range_t (10, 15), styled_string (sm, "bar"), sid2);
+ ASSERT_X_RULER_STREQ
+ (r, ascii_theme (), sm, true,
+ ("[00;01;33m[K|~+~|[00m[K [00;01;34m[K|~+~|[00m[K\n"
+ " [00;01;33m[K|[00m[K [00;01;34m[K|[00m[K\n"
+ " foo bar\n"));
+}
+
+static void
+test_borders ()
+{
+ style_manager sm;
+ {
+ x_ruler r (x_ruler::label_dir::BELOW);
+ r.add_label (canvas::range_t (0, 5),
+ styled_string (sm, "label 1"),
+ style::id_plain,
+ x_ruler::label_kind::TEXT_WITH_BORDER);
+ r.add_label (canvas::range_t (10, 15),
+ styled_string (sm, "label 2"),
+ style::id_plain);
+ r.add_label (canvas::range_t (20, 25),
+ styled_string (sm, "label 3"),
+ style::id_plain,
+ x_ruler::label_kind::TEXT_WITH_BORDER);
+ ASSERT_X_RULER_STREQ
+ (r, ascii_theme (), sm, true,
+ "|~+~| |~+~| |~+~|\n"
+ " | | |\n"
+ " | label 2 +---+---+\n"
+ "+-+-----+ |label 3|\n"
+ "|label 1| +-------+\n"
+ "+-------+\n");
+ ASSERT_X_RULER_STREQ
+ (r, unicode_theme (), sm, true,
+ "├─┬─┤ ├─┬─┤ ├─┬─┤\n"
+ " │ │ │\n"
+ " │ label 2 ╭───┴───╮\n"
+ "╭─┴─────╮ │label 3│\n"
+ "│label 1│ ╰───────╯\n"
+ "╰───────╯\n");
+ }
+ {
+ x_ruler r (x_ruler::label_dir::ABOVE);
+ r.add_label (canvas::range_t (0, 5),
+ styled_string (sm, "label 1"),
+ style::id_plain,
+ x_ruler::label_kind::TEXT_WITH_BORDER);
+ r.add_label (canvas::range_t (10, 15),
+ styled_string (sm, "label 2"),
+ style::id_plain);
+ r.add_label (canvas::range_t (20, 25),
+ styled_string (sm, "label 3"),
+ style::id_plain,
+ x_ruler::label_kind::TEXT_WITH_BORDER);
+ ASSERT_X_RULER_STREQ
+ (r, ascii_theme (), sm, true,
+ "+-------+\n"
+ "|label 1| +-------+\n"
+ "+-+-----+ |label 3|\n"
+ " | label 2 +---+---+\n"
+ " | | |\n"
+ "|~+~| |~+~| |~+~|\n");
+ ASSERT_X_RULER_STREQ
+ (r, unicode_theme (), sm, true,
+ "╭───────╮\n"
+ "│label 1│ ╭───────╮\n"
+ "╰─┬─────╯ │label 3│\n"
+ " │ label 2 ╰───┬───╯\n"
+ " │ │ │\n"
+ "├─┴─┤ ├─┴─┤ ├─┴─┤\n");
+ }
+}
+
+static void
+test_emoji ()
+{
+ style_manager sm;
+
+ styled_string s;
+ s.append (styled_string (0x26A0, /* U+26A0 WARNING SIGN. */
+ true));
+ s.append (styled_string (sm, " "));
+ s.append (styled_string (sm, "this is a warning"));
+
+ x_ruler r (x_ruler::label_dir::BELOW);
+ r.add_label (canvas::range_t (0, 5),
+ std::move (s),
+ style::id_plain,
+ x_ruler::label_kind::TEXT_WITH_BORDER);
+
+ ASSERT_X_RULER_STREQ
+ (r, ascii_theme (), sm, true,
+ "|~+~|\n"
+ " |\n"
+ "+-+------------------+\n"
+ "|⚠️ this is a warning|\n"
+ "+--------------------+\n");
+}
+
+/* Run all selftests in this file. */
+
+void
+text_art_ruler_cc_tests ()
+{
+ test_single ();
+ test_single_above ();
+ test_multiple_contiguous ();
+ test_multiple_contiguous_above ();
+ test_multiple_contiguous_abutting_labels ();
+ test_multiple_contiguous_overlapping_labels ();
+ test_abutting_left_border ();
+ test_too_long_to_consolidate_vertically ();
+ test_abutting_neighbor ();
+ test_gaps ();
+ test_styled ();
+ test_borders ();
+ test_emoji ();
+}
+
+} // namespace selftest
+
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/text-art/ruler.h b/gcc/text-art/ruler.h
new file mode 100644
index 000000000000..31f535498365
--- /dev/null
+++ b/gcc/text-art/ruler.h
@@ -0,0 +1,125 @@
+/* Classes for printing labelled rulers.
+ Copyright (C) 2023 Free Software Foundation, Inc.
+ Contributed by David Malcolm .
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3, or (at your option)
+any later version.
+
+GCC is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+. */
+
+#ifndef GCC_TEXT_ART_RULER_H
+#define GCC_TEXT_ART_RULER_H
+
+#include "text-art/canvas.h"
+
+namespace text_art {
+
+/* A way to annotate a series of ranges of canvas coordinates
+ with text labels either above or, in this example, below:
+ ├───────┬───────┼───────┬───────┼───────┬───────┤
+ │ │ │
+ label A label B label C
+ with logic to ensure that the text labels don't overlap
+ when printed. */
+
+class x_ruler
+{
+ public:
+ enum class label_dir { ABOVE, BELOW };
+ enum class label_kind
+ {
+ TEXT,
+ TEXT_WITH_BORDER
+ };
+
+ x_ruler (label_dir dir)
+ : m_label_dir (dir),
+ m_size (canvas::size_t (0, 0)),
+ m_has_layout (false)
+ {}
+
+ void add_label (const canvas::range_t &r,
+ styled_string text,
+ style::id_t style_id,
+ label_kind kind = label_kind::TEXT);
+
+ canvas::size_t get_size ()
+ {
+ ensure_layout ();
+ return m_size;
+ }
+
+ void paint_to_canvas (canvas &canvas,
+ canvas::coord_t offset,
+ const theme &theme);
+
+ void debug (const style_manager &sm);
+
+ private:
+ /* A particular label within an x_ruler.
+ Consider e.g.:
+
+ # x: 01234567890123456789012345678901234567890123456789
+ # y: 0: ├───────┬───────┼───────┬───────┼───────┬───────┤
+ # 1: │ │ │
+ # 2: label A label B label C
+ #
+
+ Then "label A" is:
+
+ # m_connector_x == 8
+ # V
+ # x: 0123456789012
+ # y: 0: ┬
+ # 1: │
+ # 2: label A
+ # x: 0123456789012
+ # ^
+ # m_text_coord.x == 6
+
+ and m_text_coord is (2, 6).
+ The y cooordinates are stored with respect to label_dir::BELOW;
+ for label_dir::ABOVE we flip them when painting the ruler. */
+ class label
+ {
+ friend class x_ruler;
+ public:
+ label (const canvas::range_t &range, styled_string text, style::id_t style_id,
+ label_kind kind);
+
+ bool operator< (const label &other) const;
+
+ private:
+ canvas::range_t m_range;
+ styled_string m_text;
+ style::id_t m_style_id;
+ label_kind m_kind;
+ canvas::rect_t m_text_rect; // includes any border
+ int m_connector_x;
+ };
+
+ void ensure_layout ();
+ void update_layout ();
+ int get_canvas_y (int rel_y) const;
+
+ label_dir m_label_dir;
+ std::vector