[tintd] Implement lsp::TextDocumentSignatureHelpRequest

Bug: tint:2127
Change-Id: I60865e10fcee1824bc4c6d41274aebac7a44d3e3
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/180483
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Ben Clayton <bclayton@google.com>
Reviewed-by: David Neto <dneto@google.com>
diff --git a/src/tint/lang/wgsl/ls/BUILD.bazel b/src/tint/lang/wgsl/ls/BUILD.bazel
index 53c566d..25ddd43 100644
--- a/src/tint/lang/wgsl/ls/BUILD.bazel
+++ b/src/tint/lang/wgsl/ls/BUILD.bazel
@@ -54,6 +54,7 @@
     "serve.cc",
     "server.cc",
     "set_trace.cc",
+    "signature_help.cc",
     "symbols.cc",
   ],
   hdrs = [
@@ -67,12 +68,14 @@
     "//src/tint/api/common",
     "//src/tint/lang/core",
     "//src/tint/lang/core/constant",
+    "//src/tint/lang/core/intrinsic",
     "//src/tint/lang/core/ir",
     "//src/tint/lang/core/type",
     "//src/tint/lang/wgsl",
     "//src/tint/lang/wgsl/ast",
     "//src/tint/lang/wgsl/common",
     "//src/tint/lang/wgsl/features",
+    "//src/tint/lang/wgsl/intrinsic",
     "//src/tint/lang/wgsl/program",
     "//src/tint/lang/wgsl/sem",
     "//src/tint/utils/containers",
@@ -116,6 +119,7 @@
     "references_test.cc",
     "rename_test.cc",
     "sem_tokens_test.cc",
+    "signature_help_test.cc",
     "symbols_test.cc",
   ],
   deps = [
diff --git a/src/tint/lang/wgsl/ls/BUILD.cmake b/src/tint/lang/wgsl/ls/BUILD.cmake
index 456469f..611c64d 100644
--- a/src/tint/lang/wgsl/ls/BUILD.cmake
+++ b/src/tint/lang/wgsl/ls/BUILD.cmake
@@ -60,6 +60,7 @@
   lang/wgsl/ls/server.cc
   lang/wgsl/ls/server.h
   lang/wgsl/ls/set_trace.cc
+  lang/wgsl/ls/signature_help.cc
   lang/wgsl/ls/symbols.cc
   lang/wgsl/ls/utils.h
 )
@@ -68,12 +69,14 @@
   tint_api_common
   tint_lang_core
   tint_lang_core_constant
+  tint_lang_core_intrinsic
   tint_lang_core_ir
   tint_lang_core_type
   tint_lang_wgsl
   tint_lang_wgsl_ast
   tint_lang_wgsl_common
   tint_lang_wgsl_features
+  tint_lang_wgsl_intrinsic
   tint_lang_wgsl_program
   tint_lang_wgsl_sem
   tint_utils_containers
@@ -124,6 +127,7 @@
   lang/wgsl/ls/references_test.cc
   lang/wgsl/ls/rename_test.cc
   lang/wgsl/ls/sem_tokens_test.cc
+  lang/wgsl/ls/signature_help_test.cc
   lang/wgsl/ls/symbols_test.cc
 )
 
diff --git a/src/tint/lang/wgsl/ls/BUILD.gn b/src/tint/lang/wgsl/ls/BUILD.gn
index dc692f8..6dde581 100644
--- a/src/tint/lang/wgsl/ls/BUILD.gn
+++ b/src/tint/lang/wgsl/ls/BUILD.gn
@@ -63,6 +63,7 @@
       "server.cc",
       "server.h",
       "set_trace.cc",
+      "signature_help.cc",
       "symbols.cc",
       "utils.h",
     ]
@@ -71,12 +72,14 @@
       "${tint_src_dir}/api/common",
       "${tint_src_dir}/lang/core",
       "${tint_src_dir}/lang/core/constant",
+      "${tint_src_dir}/lang/core/intrinsic",
       "${tint_src_dir}/lang/core/ir",
       "${tint_src_dir}/lang/core/type",
       "${tint_src_dir}/lang/wgsl",
       "${tint_src_dir}/lang/wgsl/ast",
       "${tint_src_dir}/lang/wgsl/common",
       "${tint_src_dir}/lang/wgsl/features",
+      "${tint_src_dir}/lang/wgsl/intrinsic",
       "${tint_src_dir}/lang/wgsl/program",
       "${tint_src_dir}/lang/wgsl/sem",
       "${tint_src_dir}/utils/containers",
@@ -116,6 +119,7 @@
         "references_test.cc",
         "rename_test.cc",
         "sem_tokens_test.cc",
+        "signature_help_test.cc",
         "symbols_test.cc",
       ]
       deps = [
diff --git a/src/tint/lang/wgsl/ls/file.h b/src/tint/lang/wgsl/ls/file.h
index 51f2004..7e6c367 100644
--- a/src/tint/lang/wgsl/ls/file.h
+++ b/src/tint/lang/wgsl/ls/file.h
@@ -31,10 +31,15 @@
 #include <limits>
 #include <memory>
 #include <string>
+#include <type_traits>
 #include <vector>
 
 #include "src/tint/lang/wgsl/ast/node.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
 #include "src/tint/lang/wgsl/program/program.h"
+#include "src/tint/lang/wgsl/sem/expression.h"
+#include "src/tint/lang/wgsl/sem/load.h"
+#include "src/tint/lang/wgsl/sem/materialize.h"
 #include "src/tint/utils/diagnostic/source.h"
 
 namespace tint::wgsl::ls {
@@ -73,9 +78,24 @@
     /// @returns the definition of the symbol at the location @p l in the file.
     std::optional<DefinitionResult> Definition(Source::Location l);
 
+    /// Behaviour of NodeAt()
+    enum class UnwrapMode {
+        /// NodeAt will Unwrap() the semantic node when searching for the template type.
+        kNoUnwrap,
+        /// NodeAt will not Unwrap() the semantic node when searching for the template type.
+        kUnwrap
+    };
+
+    // Default UnwrapMode is to unwrap, unless searching for a sem::Materialize or sem::Load
+    template <typename T>
+    static constexpr UnwrapMode DefaultUnwrapMode =
+        (std::is_same_v<T, sem::Materialize> || std::is_same_v<T, sem::Load>)
+            ? UnwrapMode::kNoUnwrap
+            : UnwrapMode::kUnwrap;
+
     /// @returns the inner-most semantic node at the location @p l in the file.
     /// @tparam T the type or subtype of the node to scan for.
-    template <typename T = sem::Node>
+    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();
@@ -83,12 +103,15 @@
         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 (auto* sem =
-                        As<T, CastFlags::kDontErrorOnImpossibleCast>(program.Sem().Get(node))) {
+                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;
                     if (len < best_len) {
                         best_len = len;
-                        best_node = sem;
+                        best_node = cast;
                     }
                 }
             }
diff --git a/src/tint/lang/wgsl/ls/hover.cc b/src/tint/lang/wgsl/ls/hover.cc
index ae8b2f3..5ffbe47 100644
--- a/src/tint/lang/wgsl/ls/hover.cc
+++ b/src/tint/lang/wgsl/ls/hover.cc
@@ -180,7 +180,7 @@
         return lsp::Null{};
     }
 
-    auto* node = (*file)->NodeAt<CastableBase>(Conv(r.position));
+    auto* node = (*file)->NodeAt<CastableBase, File::UnwrapMode::kNoUnwrap>(Conv(r.position));
     if (!node) {
         return lsp::Null{};
     }
diff --git a/src/tint/lang/wgsl/ls/server.cc b/src/tint/lang/wgsl/ls/server.cc
index f5aa72f..6b526de 100644
--- a/src/tint/lang/wgsl/ls/server.cc
+++ b/src/tint/lang/wgsl/ls/server.cc
@@ -69,6 +69,10 @@
             }
             return opts;
         }();
+        result.capabilities.signature_help_provider = [] {
+            lsp::SignatureHelpOptions opts;
+            return opts;
+        }();
         return result;
     });
 
@@ -97,6 +101,7 @@
     session.Register([&](const lsp::TextDocumentRenameRequest& r) { return Handle(r); });
     session.Register(
         [&](const lsp::TextDocumentSemanticTokensFullRequest& r) { return Handle(r); });
+    session.Register([&](const lsp::TextDocumentSignatureHelpRequest& r) { return Handle(r); });
 }
 
 Server::~Server() = default;
diff --git a/src/tint/lang/wgsl/ls/server.h b/src/tint/lang/wgsl/ls/server.h
index 5fa616c..e0df2f3 100644
--- a/src/tint/lang/wgsl/ls/server.h
+++ b/src/tint/lang/wgsl/ls/server.h
@@ -103,6 +103,10 @@
     langsvr::Result<langsvr::SuccessType>  //
     Handle(const langsvr::lsp::SetTraceNotification&);
 
+    /// Handler for langsvr::lsp::TextDocumentSignatureHelpRequest
+    typename langsvr::lsp::TextDocumentSignatureHelpRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentSignatureHelpRequest&);
+
     /// Handler for langsvr::lsp::TextDocumentDidOpenNotification
     langsvr::Result<langsvr::SuccessType>  //
     Handle(const langsvr::lsp::TextDocumentDidOpenNotification&);
diff --git a/src/tint/lang/wgsl/ls/signature_help.cc b/src/tint/lang/wgsl/ls/signature_help.cc
new file mode 100644
index 0000000..401e621
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/signature_help.cc
@@ -0,0 +1,213 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/ls/server.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"
+#include "src/tint/lang/wgsl/sem/call.h"
+#include "src/tint/utils/rtti/switch.h"
+#include "src/tint/utils/text/string_stream.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+namespace {
+
+std::vector<lsp::ParameterInformation> Params(const core::intrinsic::TableData& data,
+                                              const core::intrinsic::OverloadInfo& overload) {
+    std::vector<lsp::ParameterInformation> params;
+    for (size_t i = 0; i < overload.num_parameters; i++) {
+        lsp::ParameterInformation param_out;
+        auto& param_in = data[overload.parameters + i];
+        if (param_in.usage != core::ParameterUsage::kNone) {
+            param_out.label = std::string(core::ToString(param_in.usage));
+        } else {
+            param_out.label = "param-" + std::to_string(i);
+        }
+        params.push_back(std::move(param_out));
+    }
+    return params;
+}
+
+size_t CalcParamIndex(const Source& call_source, const Source::Location& carat) {
+    size_t index = 0;
+    int depth = 0;
+
+    auto start = call_source.range.begin;
+    auto end = std::min(call_source.range.end, carat);
+    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 (char c : text) {
+            switch (c) {
+                case '(':
+                case '[':
+                    depth++;
+                    break;
+                case ')':
+                case ']':
+                    depth--;
+                    break;
+                case ',':
+                    if (depth == 1) {
+                        index++;
+                    }
+            }
+        }
+    }
+    return index;
+}
+
+void PrintOverload(StyledText& ss,
+                   core::intrinsic::Context& context,
+                   const core::intrinsic::OverloadInfo& overload,
+                   std::string_view intrinsic_name) {
+    // Restore old style before returning.
+    auto prev_style = ss.Style();
+    TINT_DEFER(ss << prev_style);
+
+    core::intrinsic::TemplateState templates;
+
+    auto earliest_eval_stage = core::EvaluationStage::kConstant;
+
+    ss << style::Code << style::Function(intrinsic_name);
+
+    if (overload.num_explicit_templates > 0) {
+        ss << "<";
+        for (size_t i = 0; i < overload.num_explicit_templates; i++) {
+            const auto& tmpl = context.data[overload.templates + i];
+            if (i > 0) {
+                ss << ", ";
+            }
+            ss << style::Type(tmpl.name) << " ";
+        }
+        ss << ">";
+    }
+
+    ss << "(";
+    for (size_t i = 0; i < overload.num_parameters; i++) {
+        const auto& parameter = context.data[overload.parameters + i];
+        auto* matcher_indices = context.data[parameter.matcher_indices];
+
+        if (i > 0) {
+            ss << ", ";
+        }
+
+        if (parameter.usage != core::ParameterUsage::kNone) {
+            ss << style::Variable(parameter.usage, ": ");
+        }
+        context.Match(templates, overload, matcher_indices, earliest_eval_stage).PrintType(ss);
+    }
+    ss << ")";
+    if (overload.return_matcher_indices.IsValid()) {
+        ss << " -> ";
+        auto* matcher_indices = context.data[overload.return_matcher_indices];
+        context.Match(templates, overload, matcher_indices, earliest_eval_stage).PrintType(ss);
+    }
+
+    bool first = true;
+    auto separator = [&] {
+        ss << style::Plain(first ? " where:\n     " : "\n     ");
+        first = false;
+    };
+
+    for (size_t i = 0; i < overload.num_templates; i++) {
+        auto& tmpl = context.data[overload.templates + i];
+        if (auto* matcher_indices = context.data[tmpl.matcher_indices]) {
+            separator();
+
+            ss << style::Type(tmpl.name) << style::Plain(" is ");
+            if (tmpl.kind == core::intrinsic::TemplateInfo::Kind::kType) {
+                context.Match(templates, overload, matcher_indices, earliest_eval_stage)
+                    .PrintType(ss);
+            } else {
+                context.Match(templates, overload, matcher_indices, earliest_eval_stage)
+                    .PrintNum(ss);
+            }
+        }
+    }
+}
+
+}  // namespace
+
+typename lsp::TextDocumentSignatureHelpRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentSignatureHelpRequest& r) {
+    auto file = files_.Get(r.text_document.uri);
+    if (!file) {
+        return lsp::Null{};
+    }
+
+    auto& program = (*file)->program;
+    auto pos = Conv(r.position);
+
+    auto call = (*file)->NodeAt<sem::Call>(pos);
+    if (!call) {
+        return lsp::Null{};
+    }
+
+    lsp::SignatureHelp help;
+    help.active_parameter = CalcParamIndex(call->Declaration()->source, pos);
+    Switch(call->Target(),  //
+           [&](const sem::BuiltinFn* target) {
+               auto& data = wgsl::intrinsic::Dialect::kData;
+               auto& builtins = data.builtins;
+               auto& intrinsic_info = builtins[static_cast<size_t>(target->Fn())];
+               std::string name{wgsl::str(target->Fn())};
+
+               for (size_t i = 0; i < intrinsic_info.num_overloads; i++) {
+                   auto& overload = data[intrinsic_info.overloads + i];
+
+                   auto params = Params(data, overload);
+
+                   auto type_mgr = core::type::Manager::Wrap(program.Types());
+                   auto symbols = SymbolTable::Wrap(program.Symbols());
+
+                   StyledText ss;
+                   core::intrinsic::Context ctx{data, type_mgr, symbols};
+                   PrintOverload(ss, ctx, overload, name);
+
+                   lsp::SignatureInformation sig;
+                   sig.parameters = params;
+                   sig.label = ss.Plain();
+                   help.signatures.push_back(sig);
+
+                   if (&overload == &target->Overload()) {
+                       help.active_signature = i;
+                   }
+               }
+           });
+
+    return help;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/signature_help_test.cc b/src/tint/lang/wgsl/ls/signature_help_test.cc
new file mode 100644
index 0000000..ceaf52a
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/signature_help_test.cc
@@ -0,0 +1,281 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <gtest/gtest.h>
+#include <sstream>
+#include <string_view>
+
+#include "gmock/gmock.h"
+
+#include "langsvr/lsp/lsp.h"
+#include "langsvr/lsp/primitives.h"
+#include "langsvr/lsp/printer.h"
+#include "src/tint/lang/wgsl/ls/helpers_test.h"
+
+namespace tint::wgsl::ls {
+namespace {
+
+namespace lsp = langsvr::lsp;
+
+std::vector<lsp::SignatureInformation> MaxSignatures() {
+    std::vector<lsp::SignatureInformation> out;
+
+    {
+        std::vector<lsp::ParameterInformation> parameters;
+        parameters.push_back(lsp::ParameterInformation{
+            /* label */ lsp::String("param-0"),
+            /* documentation */ {},
+        });
+        parameters.push_back(lsp::ParameterInformation{
+            /* label */ lsp::String("param-1"),
+            /* documentation */ {},
+        });
+
+        lsp::SignatureInformation sig{};
+        sig.label = R"('max(T, T) -> T' where:
+     'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16')";
+        sig.parameters = std::move(parameters);
+
+        out.push_back(std::move(sig));
+    }
+
+    {
+        std::vector<lsp::ParameterInformation> parameters;
+        parameters.push_back(lsp::ParameterInformation{
+            /* label */ lsp::String("param-0"),
+            /* documentation */ {},
+        });
+        parameters.push_back(lsp::ParameterInformation{
+            /* label */ lsp::String("param-1"),
+            /* documentation */ {},
+        });
+
+        lsp::SignatureInformation sig{};
+        sig.label = R"('max(vecN<T>, vecN<T>) -> vecN<T>' where:
+     'T' is 'abstract-float', 'abstract-int', 'f32', 'i32', 'u32' or 'f16')";
+        sig.parameters = std::move(parameters);
+
+        out.push_back(std::move(sig));
+    }
+
+    return out;
+}
+
+struct Case {
+    std::string_view markup;
+    std::vector<lsp::SignatureInformation> signatures;
+    lsp::Uinteger active_signature = 0;
+    lsp::Uinteger active_parameter = 0;
+};
+
+std::ostream& operator<<(std::ostream& stream, const Case& c) {
+    return stream << "wgsl: '" << c.markup << "'";
+}
+
+using LsSignatureHelpTest = LsTestWithParam<Case>;
+TEST_P(LsSignatureHelpTest, SignatureHelp) {
+    auto parsed = ParseMarkers(GetParam().markup);
+    ASSERT_EQ(parsed.ranges.size(), 0u);
+    ASSERT_EQ(parsed.positions.size(), 1u);
+
+    lsp::TextDocumentSignatureHelpRequest req{};
+    req.text_document.uri = OpenDocument(parsed.clean);
+    req.position = parsed.positions[0];
+
+    for (auto& n : diagnostics_) {
+        for (auto& d : n.diagnostics) {
+            if (d.severity == lsp::DiagnosticSeverity::kError) {
+                FAIL() << "Error: " << d.message << "\nWGSL:\n" << parsed.clean;
+            }
+        }
+    }
+
+    auto future = client_session_.Send(req);
+    ASSERT_EQ(future, langsvr::Success);
+    auto res = future->get();
+    if (GetParam().signatures.empty()) {
+        ASSERT_TRUE(res.Is<lsp::Null>());
+    } else {
+        ASSERT_TRUE(res.Is<lsp::SignatureHelp>());
+        auto& got = *res.Get<lsp::SignatureHelp>();
+        EXPECT_EQ(got.signatures, GetParam().signatures);
+        EXPECT_EQ(got.active_signature, GetParam().active_signature);
+        EXPECT_EQ(got.active_parameter, GetParam().active_parameter);
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+                         LsSignatureHelpTest,
+                         ::testing::ValuesIn(std::vector<Case>{
+                             {
+                                 R"(
+const C = max(⧘1, 2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C : i32 = max(⧘1, 2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 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 */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1,⧘ 2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1, 2⧘);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(⧘ vec2(1), vec2(2));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 1,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(⧘ vec3(1), vec3(2));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 1,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(vec4(1) ⧘, vec4(2));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 1,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(vec2(1),⧘ vec2(2));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 1,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(vec3(1), vec3(2) ⧘);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 1,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = min(max(1, ⧘2), max(3, 4));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+fn f() {
+    let x = max(1, ⧘2);
+}
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max( (1 + (2⧘) * 3), 4);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1, (2 + (3⧘) * 4));
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max( array(1,2,3)[1⧘], 2);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 0,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1, array(1,2,3)[⧘2]);
+)",
+                                 MaxSignatures(),
+                                 /* active_signature */ 0,
+                                 /* active_parameter */ 1,
+                             },  // =========================================
+                             {
+                                 R"(
+const C = max(1, 2) ⧘;
+)",
+                                 {},
+                             },
+                         }));
+
+}  // namespace
+}  // namespace tint::wgsl::ls