[tintd] Handle multi-line node spans

Bug: tint:2127
Change-Id: I6d065f91aca4f069cd8b4aa0a366a9503c896d11
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/180485
Reviewed-by: David Neto <dneto@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/lang/wgsl/ls/file.h b/src/tint/lang/wgsl/ls/file.h
index 7e6c367..7f31ad9 100644
--- a/src/tint/lang/wgsl/ls/file.h
+++ b/src/tint/lang/wgsl/ls/file.h
@@ -28,6 +28,8 @@
 #ifndef SRC_TINT_LANG_WGSL_LS_FILE_H_
 #define SRC_TINT_LANG_WGSL_LS_FILE_H_
 
+#include <cstddef>
+#include <cstdint>
 #include <limits>
 #include <memory>
 #include <string>
@@ -98,17 +100,16 @@
     template <typename T = sem::Node, UnwrapMode UNWRAP_MODE = DefaultUnwrapMode<T>>
     const T* NodeAt(Source::Location l) const {
         // TODO(bclayton): This is a brute-force search. Optimize.
-        size_t best_len = std::numeric_limits<uint32_t>::max();
+        size_t best_len = std::numeric_limits<size_t>::max();
         const T* best_node = nullptr;
         for (auto* node : nodes) {
-            if (node->source.range.begin.line == node->source.range.end.line &&
-                node->source.range.begin <= l && node->source.range.end >= l) {
+            if (node->source.range.begin <= l && node->source.range.end >= l) {
                 auto* sem = program.Sem().Get(node);
                 if constexpr (UNWRAP_MODE == UnwrapMode::kUnwrap) {
                     sem = Unwrap(sem);
                 }
                 if (auto* cast = As<T, CastFlags::kDontErrorOnImpossibleCast>(sem)) {
-                    size_t len = node->source.range.end.column - node->source.range.begin.column;
+                    size_t len = node->source.range.Length(source->content);
                     if (len < best_len) {
                         best_len = len;
                         best_node = cast;
diff --git a/src/tint/lang/wgsl/ls/signature_help.cc b/src/tint/lang/wgsl/ls/signature_help.cc
index 401e621..9e0de3e 100644
--- a/src/tint/lang/wgsl/ls/signature_help.cc
+++ b/src/tint/lang/wgsl/ls/signature_help.cc
@@ -27,6 +27,7 @@
 
 #include "src/tint/lang/wgsl/ls/server.h"
 
+#include "langsvr/lsp/comparators.h"
 #include "src/tint/lang/core/intrinsic/table.h"
 #include "src/tint/lang/wgsl/intrinsic/dialect.h"
 #include "src/tint/lang/wgsl/ls/utils.h"
@@ -56,18 +57,22 @@
     return params;
 }
 
-size_t CalcParamIndex(const Source& call_source, const Source::Location& carat) {
+/// @returns the zero-based index of the parameter at with the cursor at @p position, for a call
+/// with the source @p call_source.
+size_t CalcParamIndex(const Source& call_source, const Source::Location& position) {
     size_t index = 0;
     int depth = 0;
 
-    auto start = call_source.range.begin;
-    auto end = std::min(call_source.range.end, carat);
+    auto range = Conv(call_source.range);
+    auto start = range.start;
+    auto end = std::min(range.end, Conv(position));
     auto& lines = call_source.file->content.lines;
 
-    for (auto line = start.line; line <= end.line; line++) {
-        auto start_column = line == start.line ? start.column : 0;
-        auto end_column = line == end.line ? end.column : 0;
-        auto text = lines[line - 1].substr(start_column - 1, end_column - start_column);
+    for (auto line_idx = start.line; line_idx <= end.line; line_idx++) {
+        auto& line = lines[line_idx];
+        auto start_character = (line_idx == start.line) ? start.character : 0;
+        auto end_character = (line_idx == end.line) ? end.character : line.size();
+        auto text = line.substr(start_character, end_character - start_character);
         for (char c : text) {
             switch (c) {
                 case '(':
diff --git a/src/tint/lang/wgsl/ls/signature_help_test.cc b/src/tint/lang/wgsl/ls/signature_help_test.cc
index ceaf52a..31fcaba 100644
--- a/src/tint/lang/wgsl/ls/signature_help_test.cc
+++ b/src/tint/lang/wgsl/ls/signature_help_test.cc
@@ -271,6 +271,28 @@
                              },  // =========================================
                              {
                                  R"(
+const C = max(1
+              ⧘
+              ,
+              2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1
+              ,
+              ⧘
+              2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
 const C = max(1, 2) ⧘;
 )",
                                  {},
diff --git a/src/tint/utils/diagnostic/source.cc b/src/tint/utils/diagnostic/source.cc
index 3dc439b..b183f8a 100644
--- a/src/tint/utils/diagnostic/source.cc
+++ b/src/tint/utils/diagnostic/source.cc
@@ -31,6 +31,7 @@
 #include <string_view>
 #include <utility>
 
+#include "src/tint/utils/ice/ice.h"
 #include "src/tint/utils/text/string_stream.h"
 #include "src/tint/utils/text/unicode.h"
 
@@ -190,4 +191,25 @@
     return out.str();
 }
 
+size_t Source::Range::Length(const FileContent& content) const {
+    TINT_ASSERT_OR_RETURN_VALUE(begin <= end, 0);
+    TINT_ASSERT_OR_RETURN_VALUE(begin.column > 0, 0);
+    TINT_ASSERT_OR_RETURN_VALUE(begin.line > 0, 0);
+    TINT_ASSERT_OR_RETURN_VALUE(end.line <= 1 + content.lines.size(), 0);
+    TINT_ASSERT_OR_RETURN_VALUE(end.column <= 1 + content.lines[end.line - 1].size(), 0);
+
+    if (end.line == begin.line) {
+        return end.column - begin.column;
+    }
+
+    size_t len = (content.lines[begin.line - 1].size() + 1 - begin.column) +  // first line
+                 (end.column - 1) +                                           // last line
+                 end.line - begin.line;                                       // newlines
+
+    for (size_t line = begin.line + 1; line < end.line; line++) {
+        len += content.lines[line - 1].size();  // whole-lines
+    }
+    return len;
+}
+
 }  // namespace tint
diff --git a/src/tint/utils/diagnostic/source.h b/src/tint/utils/diagnostic/source.h
index afef58c..e644122 100644
--- a/src/tint/utils/diagnostic/source.h
+++ b/src/tint/utils/diagnostic/source.h
@@ -42,7 +42,7 @@
 /// Source describes a range of characters within a source file.
 class Source {
   public:
-    /// FileContent describes the content of a source file encoded using utf-8.
+    /// FileContent describes the content of a source file encoded using UTF-8.
     class FileContent {
       public:
         /// Constructs the FileContent with the given file content.
@@ -151,7 +151,7 @@
         inline constexpr Range(const Location& b, const Location& e) : begin(b), end(e) {}
 
         /// Return a column-shifted Range
-        /// @param n the number of characters to shift by
+        /// @param n the number of UTF-8 codepoint to shift by
         /// @returns a Range with a #begin and #end column shifted by `n`
         inline Range operator+(uint32_t n) const {
             return Range{{begin.line, begin.column + n}, {end.line, end.column + n}};
@@ -169,9 +169,16 @@
         /// @returns true if `this` == `rhs`
         inline bool operator!=(const Range& rhs) const { return !(*this == rhs); }
 
-        /// The location of the first character in the range.
+        /// @param content the file content that this range belongs to
+        /// @returns the length of the range in UTF-8 codepoints, treating all line-break sequences
+        /// as a single code-point.
+        /// @see https://www.w3.org/TR/WGSL/#blankspace-and-line-breaks for the definition of a line
+        /// break.
+        size_t Length(const FileContent& content) const;
+
+        /// The location of the first UTF-8 codepoint in the range.
         Location begin;
-        /// The location of one-past the last character in the range.
+        /// The location of one-past the last UTF-8 codepoint in the range.
         Location end;
     };
 
diff --git a/src/tint/utils/diagnostic/source_test.cc b/src/tint/utils/diagnostic/source_test.cc
index cb50053..42b4e01 100644
--- a/src/tint/utils/diagnostic/source_test.cc
+++ b/src/tint/utils/diagnostic/source_test.cc
@@ -28,6 +28,7 @@
 #include "src/tint/utils/diagnostic/source.h"
 
 #include <memory>
+#include <string_view>
 #include <utility>
 
 #include "gtest/gtest.h"
@@ -35,7 +36,7 @@
 namespace tint {
 namespace {
 
-static constexpr const char* kSource = R"(line one
+static constexpr std::string_view kSource = R"(line one
 line two
 line three)";
 
@@ -81,7 +82,7 @@
 #define kLS "\xE2\x80\xA8"
 #define kPS "\xE2\x80\xA9"
 
-using LineBreakTest = testing::TestWithParam<const char*>;
+using LineBreakTest = testing::TestWithParam<std::string_view>;
 TEST_P(LineBreakTest, Single) {
     std::string src = "line one";
     src += GetParam();
@@ -108,5 +109,50 @@
                          LineBreakTest,
                          testing::Values(kVTab, kFF, kNL, kLS, kPS, kLF, kCR, kCR kLF));
 
+using RangeLengthTest = testing::TestWithParam<std::pair<Source::Range, size_t>>;
+TEST_P(RangeLengthTest, Test) {
+    Source::FileContent fc("X" kLF       // 1
+                           "XX" kCR kLF  // 2
+                           "X" kCR       // 3
+                               kLS       // 4
+                           "XX"          // 5
+    );
+    auto& range = GetParam().first;
+    auto expected_length = GetParam().second;
+    EXPECT_EQ(range.Length(fc), expected_length);
+}
+
+INSTANTIATE_TEST_SUITE_P(SingleLine,
+                         RangeLengthTest,
+                         testing::Values(  //
+
+                             std::make_pair(Source::Range{{1, 1}, {1, 1}}, 0),
+                             std::make_pair(Source::Range{{2, 1}, {2, 1}}, 0),
+                             std::make_pair(Source::Range{{3, 1}, {3, 1}}, 0),
+                             std::make_pair(Source::Range{{4, 1}, {4, 1}}, 0),
+                             std::make_pair(Source::Range{{5, 1}, {5, 1}}, 0),
+
+                             std::make_pair(Source::Range{{1, 1}, {1, 2}}, 1),
+                             std::make_pair(Source::Range{{2, 1}, {2, 3}}, 2),
+                             std::make_pair(Source::Range{{3, 1}, {3, 2}}, 1),
+                             std::make_pair(Source::Range{{5, 1}, {5, 3}}, 2),
+
+                             std::make_pair(Source::Range{{1, 2}, {1, 2}}, 0),
+                             std::make_pair(Source::Range{{2, 2}, {2, 3}}, 1),
+                             std::make_pair(Source::Range{{3, 2}, {3, 2}}, 0),
+                             std::make_pair(Source::Range{{5, 2}, {5, 3}}, 1)));
+
+INSTANTIATE_TEST_SUITE_P(MultiLine,
+                         RangeLengthTest,
+                         testing::Values(  //
+
+                             std::make_pair(Source::Range{{1, 1}, {2, 1}}, 2),
+                             std::make_pair(Source::Range{{2, 1}, {3, 1}}, 3),
+                             std::make_pair(Source::Range{{3, 1}, {4, 1}}, 2),
+                             std::make_pair(Source::Range{{4, 1}, {5, 1}}, 1),
+
+                             std::make_pair(Source::Range{{1, 1}, {5, 3}}, 10),
+                             std::make_pair(Source::Range{{2, 2}, {5, 2}}, 6)));
+
 }  // namespace
 }  // namespace tint