[tintd] Implement TextDocumentDocumentSymbolRequest

Bug: tint:2127
Change-Id: Ifb115a28a45d45951adde3e939b14a78ad2b8f42
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/179121
Kokoro: Kokoro <noreply+kokoro@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 57f5691..67c8c2a 100644
--- a/src/tint/lang/wgsl/ls/BUILD.bazel
+++ b/src/tint/lang/wgsl/ls/BUILD.bazel
@@ -44,6 +44,7 @@
     "file.cc",
     "serve.cc",
     "server.cc",
+    "symbols.cc",
   ],
   hdrs = [
     "file.h",
@@ -97,6 +98,7 @@
   srcs = [
     "diagnostics_test.cc",
     "helpers_test.h",
+    "symbols_test.cc",
   ],
   deps = [
     "//src/tint/lang/core",
diff --git a/src/tint/lang/wgsl/ls/BUILD.cmake b/src/tint/lang/wgsl/ls/BUILD.cmake
index 5c0e4ec..47dc1f4 100644
--- a/src/tint/lang/wgsl/ls/BUILD.cmake
+++ b/src/tint/lang/wgsl/ls/BUILD.cmake
@@ -49,6 +49,7 @@
   lang/wgsl/ls/serve.h
   lang/wgsl/ls/server.cc
   lang/wgsl/ls/server.h
+  lang/wgsl/ls/symbols.cc
   lang/wgsl/ls/utils.h
 )
 
@@ -105,6 +106,7 @@
 tint_add_target(tint_lang_wgsl_ls_test test
   lang/wgsl/ls/diagnostics_test.cc
   lang/wgsl/ls/helpers_test.h
+  lang/wgsl/ls/symbols_test.cc
 )
 
 tint_target_add_dependencies(tint_lang_wgsl_ls_test test
diff --git a/src/tint/lang/wgsl/ls/BUILD.gn b/src/tint/lang/wgsl/ls/BUILD.gn
index 4983b76..52190eb 100644
--- a/src/tint/lang/wgsl/ls/BUILD.gn
+++ b/src/tint/lang/wgsl/ls/BUILD.gn
@@ -52,6 +52,7 @@
       "serve.h",
       "server.cc",
       "server.h",
+      "symbols.cc",
       "utils.h",
     ]
     deps = [
@@ -97,6 +98,7 @@
       sources = [
         "diagnostics_test.cc",
         "helpers_test.h",
+        "symbols_test.cc",
       ]
       deps = [
         "${tint_src_dir}:gmock_and_gtest",
diff --git a/src/tint/lang/wgsl/ls/server.cc b/src/tint/lang/wgsl/ls/server.cc
index 710a5d0..e8e3791 100644
--- a/src/tint/lang/wgsl/ls/server.cc
+++ b/src/tint/lang/wgsl/ls/server.cc
@@ -38,6 +38,10 @@
 Server::Server(langsvr::Session& session) : session_(session) {
     session.Register([&](const lsp::InitializeRequest&) {
         lsp::InitializeResult result;
+        result.capabilities.document_symbol_provider = [] {
+            lsp::DocumentSymbolOptions opts;
+            return opts;
+        }();
         return result;
     });
 
@@ -46,11 +50,15 @@
         return lsp::Null{};
     });
 
+    // Notification handlers
     session.Register([&](const lsp::TextDocumentDidOpenNotification& n) { return Handle(n); });
     session.Register([&](const lsp::TextDocumentDidCloseNotification& n) { return Handle(n); });
     session.Register([&](const lsp::TextDocumentDidChangeNotification& n) { return Handle(n); });
     session.Register(
         [&](const lsp::WorkspaceDidChangeConfigurationNotification&) { return langsvr::Success; });
+
+    // Request handlers
+    session.Register([&](const lsp::TextDocumentDocumentSymbolRequest& 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 10870b4..ab84a36 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::TextDocumentDocumentSymbolRequest
+    typename langsvr::lsp::TextDocumentDocumentSymbolRequest::ResultType  //
+    Handle(const langsvr::lsp::TextDocumentDocumentSymbolRequest& r);
+
     /// Handler for langsvr::lsp::TextDocumentDidOpenNotification
     langsvr::Result<langsvr::SuccessType>  //
     Handle(const langsvr::lsp::TextDocumentDidOpenNotification&);
diff --git a/src/tint/lang/wgsl/ls/symbols.cc b/src/tint/lang/wgsl/ls/symbols.cc
new file mode 100644
index 0000000..78771ba
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/symbols.cc
@@ -0,0 +1,95 @@
+// 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/ast/const.h"
+#include "src/tint/lang/wgsl/ls/server.h"
+
+#include "src/tint/lang/wgsl/ast/alias.h"
+#include "src/tint/lang/wgsl/ast/identifier.h"
+#include "src/tint/lang/wgsl/ast/module.h"
+#include "src/tint/lang/wgsl/ast/struct.h"
+#include "src/tint/lang/wgsl/ls/utils.h"
+#include "src/tint/utils/rtti/switch.h"
+
+namespace tint::wgsl::ls {
+
+namespace lsp = langsvr::lsp;
+
+typename lsp::TextDocumentDocumentSymbolRequest::ResultType  //
+Server::Handle(const lsp::TextDocumentDocumentSymbolRequest& r) {
+    typename lsp::TextDocumentDocumentSymbolRequest::SuccessType result = lsp::Null{};
+
+    std::vector<lsp::DocumentSymbol> symbols;
+    if (auto file = files_.Get(r.text_document.uri)) {
+        for (auto* decl : (*file)->program.AST().Functions()) {
+            lsp::DocumentSymbol sym;
+            sym.range = Conv(decl->source.range);
+            sym.selection_range = Conv(decl->name->source.range);
+            sym.kind = lsp::SymbolKind::kFunction;
+            sym.name = decl->name->symbol.NameView();
+            symbols.push_back(sym);
+        }
+        for (auto* decl : (*file)->program.AST().GlobalVariables()) {
+            lsp::DocumentSymbol sym;
+            sym.range = Conv(decl->source.range);
+            sym.selection_range = Conv(decl->name->source.range);
+            sym.kind =
+                decl->Is<ast::Const>() ? lsp::SymbolKind::kConstant : lsp::SymbolKind::kVariable;
+            sym.name = decl->name->symbol.NameView();
+            symbols.push_back(sym);
+        }
+        for (auto* decl : (*file)->program.AST().TypeDecls()) {
+            Switch(
+                decl,  //
+                [&](const ast::Struct* str) {
+                    lsp::DocumentSymbol sym;
+                    sym.range = Conv(str->source.range);
+                    sym.selection_range = Conv(decl->name->source.range);
+                    sym.kind = lsp::SymbolKind::kStruct;
+                    sym.name = decl->name->symbol.NameView();
+                    symbols.push_back(sym);
+                },
+                [&](const ast::Alias* str) {
+                    lsp::DocumentSymbol sym;
+                    sym.range = Conv(str->source.range);
+                    sym.selection_range = Conv(decl->name->source.range);
+                    // TODO(bclayton): Is there a better symbol kind?
+                    sym.kind = lsp::SymbolKind::kObject;
+                    sym.name = decl->name->symbol.NameView();
+                    symbols.push_back(sym);
+                });
+        }
+    }
+
+    if (!symbols.empty()) {
+        result = std::move(symbols);
+    }
+
+    return result;
+}
+
+}  // namespace tint::wgsl::ls
diff --git a/src/tint/lang/wgsl/ls/symbols_test.cc b/src/tint/lang/wgsl/ls/symbols_test.cc
new file mode 100644
index 0000000..8899adf
--- /dev/null
+++ b/src/tint/lang/wgsl/ls/symbols_test.cc
@@ -0,0 +1,161 @@
+// 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 <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;
+
+struct Case {
+    const std::string_view wgsl;
+    const std::vector<lsp::DocumentSymbol> symbols;
+};
+
+struct Symbol : lsp::DocumentSymbol {
+    explicit Symbol(std::string_view n) { name = n; }
+
+    Symbol& Kind(lsp::SymbolKind k) {
+        kind = k;
+        return *this;
+    }
+
+    Symbol& Range(lsp::Uinteger start_line,
+                  lsp::Uinteger start_column,
+                  lsp::Uinteger end_line,
+                  lsp::Uinteger end_column) {
+        range = lsp::Range{{start_line, start_column}, {end_line, end_column}};
+        return *this;
+    }
+
+    Symbol& SelectionRange(lsp::Uinteger start_line,
+                           lsp::Uinteger start_column,
+                           lsp::Uinteger end_line,
+                           lsp::Uinteger end_column) {
+        selection_range = lsp::Range{{start_line, start_column}, {end_line, end_column}};
+        return *this;
+    }
+};
+
+std::ostream& operator<<(std::ostream& stream, const Case& c) {
+    return stream << "wgsl: '" << c.wgsl << "'";
+}
+
+using LsSymbolsTest = LsTestWithParam<Case>;
+TEST_P(LsSymbolsTest, Symbols) {
+    lsp::TextDocumentDocumentSymbolRequest req{};
+    req.text_document.uri = OpenDocument(GetParam().wgsl);
+    auto future = client_session_.Send(req);
+    ASSERT_EQ(future, langsvr::Success);
+    auto res = future->get();
+    if (GetParam().symbols.empty()) {
+        ASSERT_TRUE(res.Is<lsp::Null>());
+    } else {
+        ASSERT_TRUE(res.Is<std::vector<lsp::DocumentSymbol>>());
+        EXPECT_THAT(*res.Get<std::vector<lsp::DocumentSymbol>>(),
+                    testing::ContainerEq(GetParam().symbols));
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+                         LsSymbolsTest,
+                         ::testing::ValuesIn(std::vector<Case>{
+                             {
+                                 "",
+                                 {},
+                             },
+                             {
+                                 ""
+                                 /* 0 */ "const C = 1;\n"
+                                 /* 1 */ "/* blah */ var V : i32 = 2i;\n"
+                                 /* 2 */ "override O = 3f;\n",
+                                 {
+                                     Symbol{"C"}
+                                         .Kind(lsp::SymbolKind::kConstant)
+                                         .Range(0, 0, 0, 11)
+                                         .SelectionRange(0, 6, 0, 7),
+                                     Symbol{"V"}
+                                         .Kind(lsp::SymbolKind::kVariable)
+                                         .Range(1, 11, 1, 27)
+                                         .SelectionRange(1, 15, 1, 16),
+                                     Symbol{"O"}
+                                         .Kind(lsp::SymbolKind::kVariable)
+                                         .Range(2, 0, 2, 15)
+                                         .SelectionRange(2, 9, 2, 10),
+                                 },
+                             },
+                             {
+                                 ""
+                                 /* 0 */ "fn fa() {}\n"
+                                 /* 1 */ "/* blah */ fn fb() -> i32 {\n"
+                                 /* 2 */ "  return 1;\n"
+                                 /* 3 */ "} // blah",
+                                 {
+                                     Symbol{"fa"}
+                                         .Kind(lsp::SymbolKind::kFunction)
+                                         .Range(0, 0, 0, 10)
+                                         .SelectionRange(0, 3, 0, 5),
+                                     Symbol{"fb"}
+                                         .Kind(lsp::SymbolKind::kFunction)
+                                         .Range(1, 11, 3, 1)
+                                         .SelectionRange(1, 14, 1, 16),
+                                 },
+                             },
+                             {
+                                 ""
+                                 /* 0 */ "struct s1 { i : i32 }\n"
+                                 /* 1 */ "alias A = i32;\n"
+                                 /* 2 */ "/* blah */ struct s2 {\n"
+                                 /* 3 */ "  a : i32,\n"
+                                 /* 4 */ "} // blah",
+                                 {
+                                     Symbol{"s1"}
+                                         .Kind(lsp::SymbolKind::kStruct)
+                                         .Range(0, 0, 0, 21)
+                                         .SelectionRange(0, 7, 0, 9),
+                                     Symbol{"A"}
+                                         .Kind(lsp::SymbolKind::kObject)
+                                         .Range(1, 0, 1, 13)
+                                         .SelectionRange(1, 6, 1, 7),
+                                     Symbol{"s2"}
+                                         .Kind(lsp::SymbolKind::kStruct)
+                                         .Range(2, 11, 4, 1)
+                                         .SelectionRange(2, 18, 2, 20),
+                                 },
+                             },
+                         }));
+
+}  // namespace
+}  // namespace tint::wgsl::ls