[tintd] Implement TextDocumentDefinitionRequest

Bug: tint:2127
Change-Id: I65f3de4b818a503421610a6539ab61b7cfa8cec3
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/179105
Reviewed-by: David Neto <dneto@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/lang/wgsl/ls/BUILD.bazel b/src/tint/lang/wgsl/ls/BUILD.bazel
index 67c8c2a..db8b7b4 100644
--- a/src/tint/lang/wgsl/ls/BUILD.bazel
+++ b/src/tint/lang/wgsl/ls/BUILD.bazel
@@ -39,6 +39,7 @@
 cc_library(
   name = "ls",
   srcs = [
+    "definition.cc",
     "diagnostics.cc",
     "document.cc",
     "file.cc",
@@ -96,7 +97,9 @@
   name = "test",
   alwayslink = True,
   srcs = [
+    "definition_test.cc",
     "diagnostics_test.cc",
+    "helpers_test.cc",
     "helpers_test.h",
     "symbols_test.cc",
   ],
diff --git a/src/tint/lang/wgsl/ls/BUILD.cmake b/src/tint/lang/wgsl/ls/BUILD.cmake
index 47dc1f4..f39c345 100644
--- a/src/tint/lang/wgsl/ls/BUILD.cmake
+++ b/src/tint/lang/wgsl/ls/BUILD.cmake
@@ -41,6 +41,7 @@
 # Condition: TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER
 ################################################################################
 tint_add_target(tint_lang_wgsl_ls lib
+  lang/wgsl/ls/definition.cc
   lang/wgsl/ls/diagnostics.cc
   lang/wgsl/ls/document.cc
   lang/wgsl/ls/file.cc
@@ -104,7 +105,9 @@
 # Condition: TINT_BUILD_TINTD AND TINT_BUILD_WGSL_READER
 ################################################################################
 tint_add_target(tint_lang_wgsl_ls_test test
+  lang/wgsl/ls/definition_test.cc
   lang/wgsl/ls/diagnostics_test.cc
+  lang/wgsl/ls/helpers_test.cc
   lang/wgsl/ls/helpers_test.h
   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 52190eb..ce8653f 100644
--- a/src/tint/lang/wgsl/ls/BUILD.gn
+++ b/src/tint/lang/wgsl/ls/BUILD.gn
@@ -44,6 +44,7 @@
 if (tint_build_tintd && tint_build_wgsl_reader) {
   libtint_source_set("ls") {
     sources = [
+      "definition.cc",
       "diagnostics.cc",
       "document.cc",
       "file.cc",
@@ -96,7 +97,9 @@
   if (tint_build_tintd && tint_build_wgsl_reader) {
     tint_unittests_source_set("unittests") {
       sources = [
+        "definition_test.cc",
         "diagnostics_test.cc",
+        "helpers_test.cc",
         "helpers_test.h",
         "symbols_test.cc",
       ]
diff --git a/src/tint/lang/wgsl/ls/definition.cc b/src/tint/lang/wgsl/ls/definition.cc
new file mode 100644
index 0000000..5d68a2f
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/definition.cc
@@ -0,0 +1,52 @@
+// 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/wgsl/ls/utils.h"
+
+namespace lsp = langsvr::lsp;
+
+namespace tint::wgsl::ls {
+
+typename lsp::TextDocumentDefinitionRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentDefinitionRequest& r) {
+    typename lsp::TextDocumentDefinitionRequest::SuccessType result = lsp::Null{};
+
+    if (auto file = files_.Get(r.text_document.uri)) {
+        if (auto def = (*file)->Definition(Conv(r.position))) {
+            lsp::Location loc;
+            loc.range = Conv(def->range);
+            loc.uri = r.text_document.uri;
+            result = lsp::Definition{loc};
+        }
+    }
+
+    return result;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/definition_test.cc b/src/tint/lang/wgsl/ls/definition_test.cc
new file mode 100644
index 0000000..5a630a1
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/definition_test.cc
@@ -0,0 +1,175 @@
+// 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"
+#include "src/tint/utils/text/unicode.h"
+
+namespace tint::wgsl::ls {
+namespace {
+
+namespace lsp = langsvr::lsp;
+
+using LsDefinitionTest = LsTestWithParam<std::string_view>;
+TEST_P(LsDefinitionTest, Symbols) {
+    auto parsed = ParseMarkers(GetParam());
+    ASSERT_EQ(parsed.positions.size(), 1u);
+    ASSERT_LE(parsed.ranges.size(), 1u);
+
+    lsp::TextDocumentDefinitionRequest 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 (parsed.ranges.empty()) {
+        ASSERT_TRUE(res.Is<lsp::Null>());
+    } else {
+        ASSERT_TRUE(res.Is<lsp::Definition>());
+        auto definition = *res.Get<lsp::Definition>();
+        ASSERT_TRUE(definition.Is<lsp::Location>());
+        EXPECT_THAT(definition.Get<lsp::Location>()->uri, req.text_document.uri);
+        EXPECT_THAT(definition.Get<lsp::Location>()->range, parsed.ranges[0]);
+    }
+}
+
+// TODO(bclayton): Type aliases.
+INSTANTIATE_TEST_SUITE_P(,
+                         LsDefinitionTest,
+                         ::testing::ValuesIn(std::vector<std::string_view>{
+                             R"(
+const「CONST」= 42;
+fn f() { _ = ⧘CONST; }
+)",  // =========================================
+                             R"(
+var<private>「VAR」= 42;
+fn f() { _ = V⧘AR; }
+)",  // =========================================
+                             R"(
+override「OVERRIDE」= 42;
+fn f() { _ = OVERRID⧘E; }
+)",  // =========================================
+                             R"(
+struct「STRUCT」{ i : i32 }
+fn f(s : ⧘STRUCT) {}
+)",  // =========================================
+                             R"(
+struct S {「i」: i32 }
+fn f(s : S) { _ = s.⧘i; }
+)",  // =========================================
+                             R"(
+fn f(「p」: i32) { _ = ⧘p; }
+)",  // =========================================
+                             R"(
+fn f() {
+    const「i」= 42;
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+fn f() {
+    let「i」= 42;
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+fn f() {
+    var「i」= 42;
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+fn f() {
+    var i = 42;
+    {
+        var「i」= 42;
+        _ = ⧘i;
+    }
+}
+)",  // =========================================
+                             R"(
+fn f() {
+    var「i」= 42;
+    {
+        var i = 42;
+    }
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+const i = 42;
+fn f() {
+    var「i」= 42;
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+const i = 42;
+fn f(「i」: i32) {
+    _ = ⧘i;
+}
+)",  // =========================================
+                             R"(
+fn「a」() {}
+fn b() { ⧘a(); }
+)",  // =========================================
+                             R"(
+fn b() { ⧘a(); }
+fn「a」() {}
+)",  // =========================================
+                             R"(
+fn f() {
+    let「i」= 42;
+    _ = (max(i⧘, 8) * 5);
+}
+)",  // =========================================
+                             R"(
+const C = m⧘ax(1, 2);
+)",  // =========================================
+                             R"(
+const C : i⧘32 = 42;
+)"}));
+
+}  // namespace
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/file.cc b/src/tint/lang/wgsl/ls/file.cc
index f89b099..39393fc 100644
--- a/src/tint/lang/wgsl/ls/file.cc
+++ b/src/tint/lang/wgsl/ls/file.cc
@@ -36,6 +36,7 @@
 #include "src/tint/lang/wgsl/sem/function_expression.h"
 #include "src/tint/lang/wgsl/sem/member_accessor_expression.h"
 #include "src/tint/lang/wgsl/sem/struct.h"
+#include "src/tint/lang/wgsl/sem/type_expression.h"
 #include "src/tint/lang/wgsl/sem/variable.h"
 #include "src/tint/utils/rtti/switch.h"
 
@@ -94,6 +95,18 @@
             }
         }
     };
+    auto struct_ = [&](const sem::Struct* s) {
+        if (include_declaration) {
+            references.push_back(s->Declaration()->name->source.range);
+        }
+        for (auto* node : nodes) {
+            if (auto* ident = node->As<ast::IdentifierExpression>()) {
+                if (program.Sem().Get<sem::Struct>(node) == s) {
+                    references.push_back(ident->source.range);
+                }
+            }
+        }
+    };
 
     Switch(
         Unwrap(NodeAt<CastableBase>(l)),  //
@@ -106,7 +119,11 @@
                 struct_member(member);
             }
         },
-        [&](const sem::StructMember* m) { struct_member(m); });
+        [&](const sem::StructMember* m) { struct_member(m); },
+        [&](const sem::TypeExpression* te) {
+            return Switch(te->Type(),  //
+                          [&](const sem::Struct* s) { return struct_(s); });
+        });
     return references;
 }
 
@@ -123,7 +140,11 @@
             }
             return nullptr;
         },
-        [&](const sem::StructMember* m) { return m->Declaration()->name; });
+        [&](const sem::StructMember* m) { return m->Declaration()->name; },
+        [&](const sem::TypeExpression* te) {
+            return Switch(te->Type(),  //
+                          [&](const sem::Struct* s) { return s->Declaration()->name; });
+        });
     if (ident) {
         return TextAndRange{ident->symbol.Name(), ident->source.range};
     }
diff --git a/src/tint/lang/wgsl/ls/helpers_test.cc b/src/tint/lang/wgsl/ls/helpers_test.cc
new file mode 100644
index 0000000..d996285
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/helpers_test.cc
@@ -0,0 +1,86 @@
+// 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/helpers_test.h"
+
+#include <utility>
+
+namespace tint::wgsl::ls {
+
+namespace lsp = langsvr::lsp;
+
+ParsedMarkers ParseMarkers(std::string_view str) {
+    std::stringstream clean;
+    lsp::Position current_position;
+    std::vector<langsvr::lsp::Position> positions;
+    std::vector<langsvr::lsp::Range> ranges;
+    std::optional<langsvr::lsp::Range> current_range;
+    while (!str.empty()) {
+        auto [codepoint, len] =
+            utf8::Decode(reinterpret_cast<const uint8_t*>(str.data()), str.length());
+        if (codepoint == 0 || len == 0) {
+            break;
+        }
+
+        switch (codepoint) {
+            case '\n':
+                current_position.line++;
+                current_position.character = 0;
+                clean << "\n";
+                break;
+            case U"「"[0]:
+                // Range start. Replace with ' '
+                current_position.character++;
+                current_range = lsp::Range{};
+                current_range->start = current_position;
+                clean << ' ';
+                break;
+            case U"」"[0]:
+                // Range end. Replace with ' '
+                if (current_range) {
+                    current_range->end = current_position;
+                    ranges.push_back(*current_range);
+                    current_range.reset();
+                }
+                clean << ' ';
+                current_position.character++;
+                break;
+            case U"⧘"[0]:
+                // Position. Consume
+                positions.push_back(current_position);
+                break;
+            default:
+                clean << str.substr(0, len);
+                current_position.character++;
+                break;
+        }
+        str = str.substr(len);
+    }
+    return ParsedMarkers{std::move(ranges), std::move(positions), clean.str()};
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/helpers_test.h b/src/tint/lang/wgsl/ls/helpers_test.h
index 08377d2..d98b7a2 100644
--- a/src/tint/lang/wgsl/ls/helpers_test.h
+++ b/src/tint/lang/wgsl/ls/helpers_test.h
@@ -90,6 +90,21 @@
 template <typename T>
 using LsTestWithParam = LsTestImpl<testing::TestWithParam<T>>;
 
+/// Result structure of ParseMarkers
+struct ParsedMarkers {
+    /// All parsed ranges, marked up with '「' and '」'. For example: `「my_range」`
+    std::vector<langsvr::lsp::Range> ranges;
+    /// All parsed positions, marked up with '⧘'. For example: `posi⧘tion`
+    std::vector<langsvr::lsp::Position> positions;
+    /// The string with all markup removed.
+    /// '「' and '」' are replaced with whitespace.
+    /// '⧘' are omitted with no replacement characters.
+    std::string clean;
+};
+
+/// ParseMarkers parses location and range markers from the string @p str.
+ParsedMarkers ParseMarkers(std::string_view str);
+
 }  // namespace tint::wgsl::ls
 
 #endif  // SRC_TINT_LANG_WGSL_LS_HELPERS_TEST_H_
diff --git a/src/tint/lang/wgsl/ls/server.cc b/src/tint/lang/wgsl/ls/server.cc
index e8e3791..0e2d9c0 100644
--- a/src/tint/lang/wgsl/ls/server.cc
+++ b/src/tint/lang/wgsl/ls/server.cc
@@ -38,6 +38,7 @@
 Server::Server(langsvr::Session& session) : session_(session) {
     session.Register([&](const lsp::InitializeRequest&) {
         lsp::InitializeResult result;
+        result.capabilities.definition_provider = true;
         result.capabilities.document_symbol_provider = [] {
             lsp::DocumentSymbolOptions opts;
             return opts;
@@ -58,6 +59,7 @@
         [&](const lsp::WorkspaceDidChangeConfigurationNotification&) { return langsvr::Success; });
 
     // Request handlers
+    session.Register([&](const lsp::TextDocumentDefinitionRequest& r) { return Handle(r); });
     session.Register([&](const lsp::TextDocumentDocumentSymbolRequest& r) { return Handle(r); });
 }
 
diff --git a/src/tint/lang/wgsl/ls/server.h b/src/tint/lang/wgsl/ls/server.h
index ab84a36..03dce58 100644
--- a/src/tint/lang/wgsl/ls/server.h
+++ b/src/tint/lang/wgsl/ls/server.h
@@ -55,6 +55,10 @@
     bool ShuttingDown() const { return shutting_down_; }
 
   private:
+    /// Handler for langsvr::lsp::TextDocumentDefinitionRequest
+    typename langsvr::lsp::TextDocumentDefinitionRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentDefinitionRequest&);
+
     /// Handler for langsvr::lsp::TextDocumentDocumentSymbolRequest
     typename langsvr::lsp::TextDocumentDocumentSymbolRequest::ResultType  //
     Handle(const langsvr::lsp::TextDocumentDocumentSymbolRequest& r);