From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by sourceware.org (Postfix) with ESMTPS id 4E21A3858CD1 for ; Wed, 31 May 2023 18:06:35 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 4E21A3858CD1 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=redhat.com Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=redhat.com DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1685556395; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=xvkEBLmjQAL71LeEQZNRLJn7H8xLz3r3TexadnNW8Mc=; b=AetRnrdyXrFm1f1Mb8qf0clcUe4o4z+cHFBen46XEGtDD2jZXYpN76Mh85ZvE/F5aHFnEc utckeURw/IzyIR9pVjRd8dhaqa9ISPJm/H4yyuDxaouPA3ftoKvltYbgwn+QvjMiDC7xjS XVCV4xdl8vRAoPOIsnFoft4mCMw8Dkc= Received: from mimecast-mx02.redhat.com (mimecast-mx02.redhat.com [66.187.233.88]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id us-mta-632--9bFyA9aPzetwy4LCSpbLA-1; Wed, 31 May 2023 14:06:33 -0400 X-MC-Unique: -9bFyA9aPzetwy4LCSpbLA-1 Received: from smtp.corp.redhat.com (int-mx01.intmail.prod.int.rdu2.redhat.com [10.11.54.1]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mimecast-mx02.redhat.com (Postfix) with ESMTPS id DF9D6811E7F for ; Wed, 31 May 2023 18:06:32 +0000 (UTC) Received: from t14s.localdomain.com (unknown [10.22.17.56]) by smtp.corp.redhat.com (Postfix) with ESMTP id 8D96C40CFD45; Wed, 31 May 2023 18:06:32 +0000 (UTC) From: David Malcolm To: gcc-patches@gcc.gnu.org Cc: David Malcolm Subject: [PATCH 2/3] diagnostics: add support for "text art" diagrams Date: Wed, 31 May 2023 14:06:29 -0400 Message-Id: <20230531180630.3127108-3-dmalcolm@redhat.com> In-Reply-To: <20230531180630.3127108-1-dmalcolm@redhat.com> References: <20230531180630.3127108-1-dmalcolm@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.1 on 10.11.54.1 X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Spam-Status: No, score=-24.1 required=5.0 tests=BAYES_00,DKIM_INVALID,DKIM_SIGNED,GIT_PATCH_0,KAM_ASCII_DIVIDERS,KAM_DMARC_NONE,KAM_DMARC_STATUS,KAM_SHORT,RCVD_IN_DNSWL_NONE,SPF_HELO_NONE,SPF_NONE,TXREP,T_SCC_BODY_TEXT_LINE autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on server2.sourceware.org List-Id: Existing text output in GCC has to be implemented by writing sequentially to a pretty_printer instance. This makes it hard to implement some kinds of diagnostic output (see e.g. diagnostic-show-locus.cc). This patch adds more flexible ways of creating text output: - a canvas class, which can be "painted" to via random-access (rather that sequentially) - a table class for 2D grid layout, supporting items that span multiple rows/columns - a widget class for organizing diagrams hierarchically. The patch also expands GCC's diagnostics subsystem so that diagnostics can have "text art" diagrams - think ASCII art, but potentially including some Unicode characters, such as box-drawing chars. The new code is in a new "gcc/text-art" subdirectory and "text_art" namespace. The patch adds a new "-fdiagnostics-text-art-charset=VAL" option, with values: - "none": don't emit diagrams (added to -fdiagnostics-plain-output) - "ascii": use pure ASCII in diagrams - "unicode": allow for conservative use of unicode drawing characters (such as box-drawing characters). - "emoji" (the default): as "unicode", but potentially allow for conservative use of emoji in the output (such as U+26A0 WARNING SIGN). I made it possible to disable emoji separately from unicode as I believe there's a generation gap in acceptance of these characters (some older programmers have a visceral reaction against them, whereas younger programmers may have no problem with them). Diagrams are emitted to stderr by default. With SARIF output they are captured as a location in "relatedLocations", with the diagram as a code block in Markdown within a "markdown" property of a message. This patch doesn't add any such diagram usage to GCC, saving that for followups, apart from adding a plugin to the test suite to exercise the functionality. One issue is that in various places in the seltftests I've embedded UTF-8 encoded characters in the source code. Is that acceptable, or do I need to e.g. move those tests to DejaGnu? contrib/ChangeLog: * unicode/gen-box-drawing-chars.py: New file. * unicode/gen-combining-chars.py: New file. * unicode/gen-printable-chars.py: New file. gcc/ChangeLog: * Makefile.in (OBJS-libcommon): Add 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, and text-art/widget.o. * color-macros.h (COLOR_FG_BRIGHT_BLACK): New. (COLOR_FG_BRIGHT_RED): New. (COLOR_FG_BRIGHT_GREEN): New. (COLOR_FG_BRIGHT_YELLOW): New. (COLOR_FG_BRIGHT_BLUE): New. (COLOR_FG_BRIGHT_MAGENTA): New. (COLOR_FG_BRIGHT_CYAN): New. (COLOR_FG_BRIGHT_WHITE): New. (COLOR_BG_BRIGHT_BLACK): New. (COLOR_BG_BRIGHT_RED): New. (COLOR_BG_BRIGHT_GREEN): New. (COLOR_BG_BRIGHT_YELLOW): New. (COLOR_BG_BRIGHT_BLUE): New. (COLOR_BG_BRIGHT_MAGENTA): New. (COLOR_BG_BRIGHT_CYAN): New. (COLOR_BG_BRIGHT_WHITE): New. * common.opt (fdiagnostics-text-art-charset=): New option. (diagnostic-text-art.h): New SourceInclude. (diagnostic_text_art_charset) New Enum and EnumValues. * configure: Regenerate. * configure.ac (gccdepdir): Add text-art to loop. * diagnostic-diagram.h: New file. * diagnostic-format-json.cc (json_emit_diagram): New. (diagnostic_output_format_init_json): Wire it up to context->m_diagrams.m_emission_cb. * diagnostic-format-sarif.cc: Include "diagnostic-diagram.h" and "text-art/canvas.h". (sarif_result::on_nested_diagnostic): Move code to... (sarif_result::add_related_location): ...this new function. (sarif_result::on_diagram): New. (sarif_builder::emit_diagram): New. (sarif_builder::make_message_object_for_diagram): New. (sarif_emit_diagram): New. (diagnostic_output_format_init_sarif): Set context->m_diagrams.m_emission_cb to sarif_emit_diagram. * diagnostic-text-art.h: New file. * diagnostic.cc: Include "diagnostic-text-art.h", "diagnostic-diagram.h", and "text-art/theme.h". (diagnostic_initialize): Initialize context->m_diagrams and call diagnostics_text_art_charset_init. (diagnostic_finish): Clean up context->m_diagrams.m_theme. (diagnostic_emit_diagram): New. (diagnostics_text_art_charset_init): New. * diagnostic.h (text_art::theme): New forward decl. (class diagnostic_diagram): Likewise. (diagnostic_context::m_diagrams): New field. (diagnostic_emit_diagram): New decl. * doc/invoke.texi (Diagnostic Message Formatting Options): Add -fdiagnostics-text-art-charset=. (-fdiagnostics-plain-output): Add -fdiagnostics-text-art-charset=none. * gcc.cc: Include "diagnostic-text-art.h". (driver_handle_option): Handle OPT_fdiagnostics_text_art_charset_. * opts-common.cc (decode_cmdline_options_to_array): Add "-fdiagnostics-text-art-charset=none" to expanded_args for -fdiagnostics-plain-output. * opts.cc: Include "diagnostic-text-art.h". (common_handle_option): Handle OPT_fdiagnostics_text_art_charset_. * pretty-print.cc (pp_unicode_character): New. * pretty-print.h (pp_unicode_character): New decl. * selftest-run-tests.cc: Include "text-art/selftests.h". (selftest::run_tests): Call text_art_tests. * text-art/box-drawing-chars.inc: New file, generated by contrib/unicode/gen-box-drawing-chars.py. * text-art/box-drawing.cc: New file. * text-art/box-drawing.h: New file. * text-art/canvas.cc: New file. * text-art/canvas.h: New file. * text-art/ruler.cc: New file. * text-art/ruler.h: New file. * text-art/selftests.cc: New file. * text-art/selftests.h: New file. * text-art/style.cc: New file. * text-art/styled-string.cc: New file. * text-art/table.cc: New file. * text-art/table.h: New file. * text-art/theme.cc: New file. * text-art/theme.h: New file. * text-art/types.h: New file. * text-art/widget.cc: New file. * text-art/widget.h: New file. gcc/testsuite/ChangeLog: * gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c: New test. * gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c: New test. * gcc.dg/plugin/diagnostic-test-text-art-none.c: New test. * gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c: New test. * gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c: New test. * gcc.dg/plugin/diagnostic_plugin_test_text_art.c: New test plugin. * gcc.dg/plugin/plugin.exp (plugin_test_list): Add them. libcpp/ChangeLog: * charset.cc (get_cppchar_property): New function template, based on... (cpp_wcwidth): ...this function. Rework to use the above. Include "combining-chars.inc". (cpp_is_combining_char): New function Include "printable-chars.inc". (cpp_is_printable_char): New function * combining-chars.inc: New file, generated by contrib/unicode/gen-combining-chars.py. * include/cpplib.h (cpp_is_combining_char): New function decl. (cpp_is_printable_char): New function decl. * printable-chars.inc: New file, generated by contrib/unicode/gen-printable-chars.py. --- contrib/unicode/gen-box-drawing-chars.py | 94 ++ contrib/unicode/gen-combining-chars.py | 75 + contrib/unicode/gen-printable-chars.py | 77 + gcc/Makefile.in | 11 +- gcc/color-macros.h | 16 + gcc/common.opt | 23 + gcc/configure | 2 +- gcc/configure.ac | 2 +- gcc/diagnostic-diagram.h | 51 + gcc/diagnostic-format-json.cc | 10 + gcc/diagnostic-format-sarif.cc | 106 +- gcc/diagnostic-text-art.h | 49 + gcc/diagnostic.cc | 72 + gcc/diagnostic.h | 21 + gcc/doc/invoke.texi | 25 +- gcc/gcc.cc | 6 + gcc/opts-common.cc | 1 + gcc/opts.cc | 6 + gcc/pretty-print.cc | 29 + gcc/pretty-print.h | 1 + gcc/selftest-run-tests.cc | 3 + .../diagnostic-test-text-art-ascii-bw.c | 57 + .../diagnostic-test-text-art-ascii-color.c | 58 + .../plugin/diagnostic-test-text-art-none.c | 5 + .../diagnostic-test-text-art-unicode-bw.c | 58 + .../diagnostic-test-text-art-unicode-color.c | 59 + .../plugin/diagnostic_plugin_test_text_art.c | 257 ++++ gcc/testsuite/gcc.dg/plugin/plugin.exp | 6 + gcc/text-art/box-drawing-chars.inc | 18 + gcc/text-art/box-drawing.cc | 72 + gcc/text-art/box-drawing.h | 32 + gcc/text-art/canvas.cc | 437 ++++++ gcc/text-art/canvas.h | 74 + gcc/text-art/ruler.cc | 723 ++++++++++ gcc/text-art/ruler.h | 125 ++ gcc/text-art/selftests.cc | 77 + gcc/text-art/selftests.h | 60 + gcc/text-art/style.cc | 632 ++++++++ gcc/text-art/styled-string.cc | 1107 ++++++++++++++ gcc/text-art/table.cc | 1272 +++++++++++++++++ gcc/text-art/table.h | 262 ++++ gcc/text-art/theme.cc | 183 +++ gcc/text-art/theme.h | 123 ++ gcc/text-art/types.h | 504 +++++++ gcc/text-art/widget.cc | 275 ++++ gcc/text-art/widget.h | 246 ++++ libcpp/charset.cc | 89 +- libcpp/combining-chars.inc | 68 + libcpp/include/cpplib.h | 3 + libcpp/printable-chars.inc | 231 +++ 50 files changed, 7760 insertions(+), 33 deletions(-) create mode 100755 contrib/unicode/gen-box-drawing-chars.py create mode 100755 contrib/unicode/gen-combining-chars.py create mode 100755 contrib/unicode/gen-printable-chars.py create mode 100644 gcc/diagnostic-diagram.h create mode 100644 gcc/diagnostic-text-art.h create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c create mode 100644 gcc/text-art/box-drawing-chars.inc create mode 100644 gcc/text-art/box-drawing.cc create mode 100644 gcc/text-art/box-drawing.h create mode 100644 gcc/text-art/canvas.cc create mode 100644 gcc/text-art/canvas.h create mode 100644 gcc/text-art/ruler.cc create mode 100644 gcc/text-art/ruler.h create mode 100644 gcc/text-art/selftests.cc create mode 100644 gcc/text-art/selftests.h create mode 100644 gcc/text-art/style.cc create mode 100644 gcc/text-art/styled-string.cc create mode 100644 gcc/text-art/table.cc create mode 100644 gcc/text-art/table.h create mode 100644 gcc/text-art/theme.cc create mode 100644 gcc/text-art/theme.h create mode 100644 gcc/text-art/types.h create mode 100644 gcc/text-art/widget.cc create mode 100644 gcc/text-art/widget.h create mode 100644 libcpp/combining-chars.inc create mode 100644 libcpp/printable-chars.inc diff --git a/contrib/unicode/gen-box-drawing-chars.py b/contrib/unicode/gen-box-drawing-chars.py new file mode 100755 index 00000000000..9a55266ab84 --- /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 00000000000..fb5ef50ba4c --- /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 00000000000..7684c086638 --- /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 1d39e6dd3f8..c1e7257ed24 100644 --- a/gcc/Makefile.in +++ b/gcc/Makefile.in @@ -1781,7 +1781,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 fcd79d09c01..9688f92110a 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 a28ca13385a..b3c82b8607c 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 5f67808b774..e061d2b1949 100755 --- a/gcc/configure +++ b/gcc/configure @@ -33995,7 +33995,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 cc8dd9e20bf..350d245c89f 100644 --- a/gcc/configure.ac +++ b/gcc/configure.ac @@ -1384,7 +1384,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 00000000000..fc923c512ed --- /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 694dddca9e8..539b98b5e74 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 fd29ac2ca3b..ac2f5b844e3 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 00000000000..a0d8a78f52a --- /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 0f093081161..7c2289f0634 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 9a51097f146..00b828f230d 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 898a88ce33e..023a56a647e 100644 --- a/gcc/doc/invoke.texi +++ b/gcc/doc/invoke.texi @@ -316,7 +316,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}. @@ -5066,7 +5067,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. @@ -5592,6 +5594,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 2ccca00d603..f9f0a7eaad4 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" @@ -4299,6 +4300,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 23ddcaa3b55..f0c5f483665 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 86b94d62b58..3087bdac2c6 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 7d294717f50..3d789a23812 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 0230a289df5..369be6e7ba7 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 915f2129702..e2fc8f84b1b 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 00000000000..e4239aab032 --- /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 00000000000..0650428b1ce --- /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 "" } + + ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜  + ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟  +          +          +          +          + ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙  + ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖  + + { 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 00000000000..c8118b46759 --- /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 00000000000..c9f5b36571a --- /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 00000000000..f402836f889 --- /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 "" } + + ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜  + ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟  +          +          +          +          + ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙  + ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖  + + { 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 00000000000..27c341b9f2f --- /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 4d6304cd100..60723a20eda 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 00000000000..a370255d56d --- /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 00000000000..981d0b095cf --- /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 00000000000..29f4d9921b3 --- /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 00000000000..f229612c919 --- /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" + " *****\n" + " *******\n" + " *********\n" + " *********\n" + " *********\n" + " *********\n" + " *********\n" + " *******\n" + " *****\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 `foo' 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 00000000000..495497754f5 --- /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 00000000000..80c623f77ba --- /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, + ("|~+~| |~+~|\n" + " | |\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 00000000000..31f53549836 --- /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