formatter: handle tabs

Transform these into whitespace so we can sensibly align the ^^ markers
with the text.

Change-Id: I151c0e55bc0a02c1cff6e381cb9839c9f9abdaf6
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/48694
Commit-Queue: Ben Clayton <bclayton@google.com>
Auto-Submit: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
diff --git a/src/diagnostic/formatter.cc b/src/diagnostic/formatter.cc
index 6f5a9ec..7461ba9 100644
--- a/src/diagnostic/formatter.cc
+++ b/src/diagnostic/formatter.cc
@@ -211,33 +211,52 @@
     state.newline();
     state.set_style({Color::kDefault, false});
 
-    for (size_t line = rng.begin.line; line <= rng.end.line; line++) {
-      if (line < src.file_content->lines.size() + 1) {
-        auto len = src.file_content->lines[line - 1].size();
+    for (size_t line_num = rng.begin.line;
+         (line_num <= rng.end.line) && (src.file_content->lines.size() + 1);
+         line_num++) {
+      auto& line = src.file_content->lines[line_num - 1];
+      auto line_len = line.size();
 
-        state << src.file_content->lines[line - 1];
-
-        state.newline();
-        state.set_style({Color::kCyan, false});
-
-        if (line == rng.begin.line && line == rng.end.line) {
-          // Single line
-          state.repeat(' ', rng.begin.column - 1);
-          state.repeat('^',
-                       std::max<size_t>(rng.end.column - rng.begin.column, 1));
-        } else if (line == rng.begin.line) {
-          // Start of multi-line
-          state.repeat(' ', rng.begin.column - 1);
-          state.repeat('^', len - (rng.begin.column - 1));
-        } else if (line == rng.end.line) {
-          // End of multi-line
-          state.repeat('^', rng.end.column - 1);
+      for (auto c : line) {
+        if (c == '\t') {
+          state.repeat(' ', style_.tab_width);
         } else {
-          // Middle of multi-line
-          state.repeat('^', len);
+          state << c;
         }
-        state.newline();
       }
+
+      state.newline();
+      state.set_style({Color::kCyan, false});
+
+      // Count the number of glyphs in the line span.
+      // start and end use 1-based indexing .
+      auto num_glyphs = [&](size_t start, size_t end) {
+        size_t count = 0;
+        start = (start > 0) ? (start - 1) : 0;
+        end = (end > 0) ? (end - 1) : 0;
+        for (size_t i = start; (i < end) && (i < line_len); i++) {
+          count += (line[i] == '\t') ? style_.tab_width : 1;
+        }
+        return count;
+      };
+
+      if (line_num == rng.begin.line && line_num == rng.end.line) {
+        // Single line
+        state.repeat(' ', num_glyphs(1, rng.begin.column));
+        state.repeat('^', std::max<size_t>(
+                              num_glyphs(rng.begin.column, rng.end.column), 1));
+      } else if (line_num == rng.begin.line) {
+        // Start of multi-line
+        state.repeat(' ', num_glyphs(1, rng.begin.column));
+        state.repeat('^', num_glyphs(rng.begin.column, line_len + 1));
+      } else if (line_num == rng.end.line) {
+        // End of multi-line
+        state.repeat('^', num_glyphs(1, rng.end.column));
+      } else {
+        // Middle of multi-line
+        state.repeat('^', num_glyphs(1, line_len + 1));
+      }
+      state.newline();
     }
 
     state.set_style({});
diff --git a/src/diagnostic/formatter.h b/src/diagnostic/formatter.h
index 7e037cb..dbb2856 100644
--- a/src/diagnostic/formatter.h
+++ b/src/diagnostic/formatter.h
@@ -37,6 +37,8 @@
     bool print_line = true;
     /// print a newline at the end of a diagnostic list
     bool print_newline_at_end = true;
+    /// width of a tab character
+    size_t tab_width = 2u;
   };
 
   /// Constructor for the formatter using a default style.
diff --git a/src/diagnostic/formatter_test.cc b/src/diagnostic/formatter_test.cc
index 0863030..8495dc9 100644
--- a/src/diagnostic/formatter_test.cc
+++ b/src/diagnostic/formatter_test.cc
@@ -21,11 +21,11 @@
 namespace diag {
 namespace {
 
-constexpr const char* content =
-    R"(the cat says meow
-the dog says woof
-the snake says quack
-the snail says ???
+constexpr const char* content =  // Note: words are tab-delimited
+    R"(the	cat	says	meow
+the	dog	says	woof
+the	snake	says	quack
+the	snail	says	???
 )";
 
 class DiagFormatterTest : public testing::Test {
@@ -96,16 +96,16 @@
   Formatter fmt{{false, false, true, false}};
   auto got = fmt.format(List{diag_note, diag_warn, diag_err});
   auto* expect = R"(1:14: purr
-the cat says meow
-             ^
+the  cat  says  meow
+                ^
 
 2:14: grrr
-the dog says woof
-             ^^^^
+the  dog  says  woof
+                ^^^^
 
 3:16 abc123: hiss
-the snake says quack
-               ^^^^^
+the  snake  says  quack
+                  ^^^^^
 )";
   ASSERT_EQ(expect, got);
 }
@@ -114,16 +114,16 @@
   Formatter fmt{{true, true, true, false}};
   auto got = fmt.format(List{diag_note, diag_warn, diag_err});
   auto* expect = R"(file.name:1:14 note: purr
-the cat says meow
-             ^
+the  cat  says  meow
+                ^
 
 file.name:2:14 warning: grrr
-the dog says woof
-             ^^^^
+the  dog  says  woof
+                ^^^^
 
 file.name:3:16 error abc123: hiss
-the snake says quack
-               ^^^^^
+the  snake  says  quack
+                  ^^^^^
 )";
   ASSERT_EQ(expect, got);
 }
@@ -135,12 +135,47 @@
   Formatter fmt{{false, false, true, false}};
   auto got = fmt.format(List{multiline});
   auto* expect = R"(2:9: multiline
-the dog says woof
-        ^^^^^^^^^
-the snake says quack
+the  dog  says  woof
+          ^^^^^^^^^^
+the  snake  says  quack
+^^^^^^^^^^^^^^^^^^^^^^^
+the  snail  says  ???
+^^^^^^^^^^^^^^^^
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, BasicWithFileSeverityLineTab4) {
+  Formatter fmt{{true, true, true, false, 4u}};
+  auto got = fmt.format(List{diag_note, diag_warn, diag_err});
+  auto* expect = R"(file.name:1:14 note: purr
+the    cat    says    meow
+                      ^
+
+file.name:2:14 warning: grrr
+the    dog    says    woof
+                      ^^^^
+
+file.name:3:16 error abc123: hiss
+the    snake    says    quack
+                        ^^^^^
+)";
+  ASSERT_EQ(expect, got);
+}
+
+TEST_F(DiagFormatterTest, BasicWithMultiLineTab4) {
+  Diagnostic multiline{Severity::Warning,
+                       Source{Source::Range{{2, 9}, {4, 15}}, &file},
+                       "multiline"};
+  Formatter fmt{{false, false, true, false, 4u}};
+  auto got = fmt.format(List{multiline});
+  auto* expect = R"(2:9: multiline
+the    dog    says    woof
+              ^^^^^^^^^^^^
+the    snake    says    quack
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+the    snail    says    ???
 ^^^^^^^^^^^^^^^^^^^^
-the snail says ???
-^^^^^^^^^^^^^^
 )";
   ASSERT_EQ(expect, got);
 }
@@ -149,8 +184,8 @@
   Formatter fmt{{}};
   auto got = fmt.format(List{diag_ice});
   auto* expect = R"(file.name:4:16 internal compiler error: unreachable
-the snail says ???
-               ^^^
+the  snail  says  ???
+                  ^^^
 
 ********************************************************************
 *  The tint shader compiler has encountered an unexpected error.   *
@@ -167,8 +202,8 @@
   Formatter fmt{{}};
   auto got = fmt.format(List{diag_fatal});
   auto* expect = R"(file.name:4:16 fatal: nothing
-the snail says ???
-               ^^^
+the  snail  says  ???
+                  ^^^
 
 ********************************************************************
 *  The tint shader compiler has encountered an unexpected error.   *