Index: clang-tools-extra/clangd/CMakeLists.txt =================================================================== --- clang-tools-extra/clangd/CMakeLists.txt +++ clang-tools-extra/clangd/CMakeLists.txt @@ -62,6 +62,7 @@ IncludeFixer.cpp JSONTransport.cpp Logger.cpp + PathMapping.cpp Protocol.cpp Quality.cpp ParsedAST.cpp Index: clang-tools-extra/clangd/PathMapping.h =================================================================== --- /dev/null +++ clang-tools-extra/clangd/PathMapping.h @@ -0,0 +1,67 @@ +//===--- PathMapping.h - apply path mappings to LSP messages -===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include + +namespace clang { +namespace clangd { + +class Transport; + +/// PathMappings are a collection of paired client and server paths. +/// These pairs are used to alter file:// URIs appearing in inbound and outbound +/// LSP messages, as the client's environment may have source files or +/// dependencies at different locations than the server. Therefore, both +/// paths are stored as they appear in file URI bodies, e.g. /usr/include or +/// /C:/config +/// +/// For example, if the mappings were {{"/home/user", "/workarea"}}, then +/// a client-to-server LSP message would have file:///home/user/foo.cpp +/// remapped to file:///workarea/foo.cpp, and the same would happen for replies +/// (in the opposite order). +struct PathMapping { + std::string ClientPath; + std::string ServerPath; + enum class Direction { ClientToServer, ServerToClient }; +}; +using PathMappings = std::vector; + +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const PathMapping &M); + +/// Parse the command line \p RawPathMappings (e.g. "/client=/server") into +/// pairs. Returns an error if the mappings are malformed, i.e. not absolute or +/// not a proper pair. +llvm::Expected parsePathMappings(llvm::StringRef RawPathMappings); + +/// Returns a modified \p S with the first matching path in \p Mappings +/// substituted, if applicable +llvm::Optional doPathMapping(llvm::StringRef S, + PathMapping::Direction Dir, + const PathMappings &Mappings); + +/// Applies the \p Mappings to all the file:// URIs in \p Params. +/// NOTE: The first matching mapping will be applied, otherwise \p Params will +/// be untouched. +void applyPathMappings(llvm::json::Value &Params, PathMapping::Direction Dir, + const PathMappings &Mappings); + +/// Creates a wrapping transport over \p Transp that applies the \p Mappings to +/// all inbound and outbound LSP messages. All calls are then delegated to the +/// regular transport (e.g. XPC, JSON). +std::unique_ptr +createPathMappingTransport(std::unique_ptr Transp, + PathMappings Mappings); + +} // namespace clangd +} // namespace clang Index: clang-tools-extra/clangd/PathMapping.cpp =================================================================== --- /dev/null +++ clang-tools-extra/clangd/PathMapping.cpp @@ -0,0 +1,217 @@ +//===--- PathMapping.cpp - apply path mappings to LSP messages -===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +#include "PathMapping.h" +#include "Transport.h" +#include "URI.h" +#include "llvm/ADT/None.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/Support/Errno.h" +#include "llvm/Support/Path.h" +#include +#include + +namespace clang { +namespace clangd { +namespace { +using MapperFunc = llvm::function_ref( + llvm::StringRef, PathMapping::Direction, const PathMappings &)>; + +// Recursively apply the MF to all string keys/values in V +void recursivelyMap(llvm::json::Value &V, PathMapping::Direction Dir, + const PathMappings &Mappings, const MapperFunc &MF) { + using Kind = llvm::json::Value::Kind; + Kind K = V.kind(); + if (K == Kind::Object) { + llvm::json::Object *Obj = V.getAsObject(); + llvm::json::Object MappedObj; + // 1. Map all the Keys + for (auto &KV : *Obj) { + if (llvm::Optional MappedKey = + MF(KV.first.str(), Dir, Mappings)) { + MappedObj.try_emplace(std::move(*MappedKey), std::move(KV.second)); + } else { + MappedObj.try_emplace(std::move(KV.first), std::move(KV.second)); + } + } + *Obj = std::move(MappedObj); + // 2. Map all the values + for (auto &KV : *Obj) { + recursivelyMap(KV.second, Dir, Mappings, MF); + } + } else if (K == Kind::Array) { + for (llvm::json::Value &Val : *V.getAsArray()) { + recursivelyMap(Val, Dir, Mappings, MF); + } + } else if (K == Kind::String) { + if (llvm::Optional Mapped = + MF(*V.getAsString(), Dir, Mappings)) { + V = std::move(*Mapped); + } + } +} + +class PathMappingMessageHandler : public Transport::MessageHandler { +public: + PathMappingMessageHandler(MessageHandler &Handler, + const PathMappings &Mappings) + : WrappedHandler(Handler), Mappings(Mappings) {} + + bool onNotify(llvm::StringRef Method, llvm::json::Value Params) override { + applyPathMappings(Params, PathMapping::Direction::ClientToServer, Mappings); + return WrappedHandler.onNotify(Method, std::move(Params)); + } + + bool onCall(llvm::StringRef Method, llvm::json::Value Params, + llvm::json::Value ID) override { + applyPathMappings(Params, PathMapping::Direction::ClientToServer, Mappings); + return WrappedHandler.onCall(Method, std::move(Params), std::move(ID)); + } + + bool onReply(llvm::json::Value ID, + llvm::Expected Result) override { + if (Result) { + applyPathMappings(*Result, PathMapping::Direction::ClientToServer, + Mappings); + } + return WrappedHandler.onReply(std::move(ID), std::move(Result)); + } + +private: + Transport::MessageHandler &WrappedHandler; + const PathMappings &Mappings; +}; + +// Apply path mappings to all LSP messages by intercepting all params/results +// and then delegating to the normal transport +class PathMappingTransport : public Transport { +public: + PathMappingTransport(std::unique_ptr Transp, PathMappings Mappings) + : WrappedTransport(std::move(Transp)), Mappings(std::move(Mappings)) {} + + void notify(llvm::StringRef Method, llvm::json::Value Params) override { + applyPathMappings(Params, PathMapping::Direction::ServerToClient, Mappings); + WrappedTransport->notify(Method, std::move(Params)); + } + + void call(llvm::StringRef Method, llvm::json::Value Params, + llvm::json::Value ID) override { + applyPathMappings(Params, PathMapping::Direction::ServerToClient, Mappings); + WrappedTransport->call(Method, std::move(Params), std::move(ID)); + } + + void reply(llvm::json::Value ID, + llvm::Expected Result) override { + if (Result) { + applyPathMappings(*Result, PathMapping::Direction::ServerToClient, + Mappings); + } + WrappedTransport->reply(std::move(ID), std::move(Result)); + } + + llvm::Error loop(MessageHandler &Handler) override { + PathMappingMessageHandler WrappedHandler(Handler, Mappings); + return WrappedTransport->loop(WrappedHandler); + } + +private: + std::unique_ptr WrappedTransport; + PathMappings Mappings; +}; + +inline llvm::Error make_string_error(const llvm::Twine &Message) { + return llvm::make_error(Message, + llvm::inconvertibleErrorCode()); +} + +// Converts a unix/windows path to the path portion of a file URI +// e.g. "C:\foo" -> "/C:/foo" +llvm::Expected parsePath(llvm::StringRef Path) { + namespace path = llvm::sys::path; + if (path::is_absolute(Path, path::Style::posix)) { + return Path; + } else if (path::is_absolute(Path, path::Style::windows)) { + std::string Converted = path::convert_to_slash(Path, path::Style::windows); + if (Converted.front() != '/') { + Converted = "/" + Converted; + } + return Converted; + } + return make_string_error("Path not absolute: " + Path); +} + +} // namespace + +llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const PathMapping &M) { + return OS << M.ClientPath << ", " << M.ServerPath; +} + +llvm::Expected +parsePathMappings(llvm::StringRef RawPathMappings) { + llvm::StringRef ClientPath, ServerPath, PathPair, Rest = RawPathMappings; + PathMappings ParsedMappings; + while (!Rest.empty()) { + std::tie(PathPair, Rest) = Rest.split(","); + std::tie(ClientPath, ServerPath) = PathPair.split("="); + if (ClientPath.empty() || ServerPath.empty()) { + return make_string_error("Not a valid path mapping pair: " + PathPair); + } + llvm::Expected ParsedClientPath = parsePath(ClientPath); + if (!ParsedClientPath) { + return ParsedClientPath.takeError(); + } + llvm::Expected ParsedServerPath = parsePath(ServerPath); + if (!ParsedServerPath) { + return ParsedServerPath.takeError(); + } + ParsedMappings.push_back( + {std::move(*ParsedClientPath), std::move(*ParsedServerPath)}); + } + return ParsedMappings; +} + +llvm::Optional doPathMapping(llvm::StringRef S, + PathMapping::Direction Dir, + const PathMappings &Mappings) { + // Retrun early to optimize for the common case, wherein S is not a file URI + if (!S.startswith("file://")) + return llvm::None; + auto Uri = URI::parse(S); + if (!Uri) { + llvm::consumeError(Uri.takeError()); + return llvm::None; + } + for (const auto &Mapping : Mappings) { + const std::string &From = Dir == PathMapping::Direction::ClientToServer + ? Mapping.ClientPath + : Mapping.ServerPath; + const std::string &To = Dir == PathMapping::Direction::ClientToServer + ? Mapping.ServerPath + : Mapping.ClientPath; + llvm::StringRef Body = Uri->body(); + if (Body.consume_front(From) && (Body.empty() || Body.front() == '/')) { + std::string MappedBody = (To + Body).str(); + return URI(Uri->scheme(), Uri->authority(), MappedBody.c_str()) + .toString(); + } + } + return llvm::None; +} + +void applyPathMappings(llvm::json::Value &Params, PathMapping::Direction Dir, + const PathMappings &Mappings) { + recursivelyMap(Params, Dir, Mappings, doPathMapping); +} + +std::unique_ptr +createPathMappingTransport(std::unique_ptr Transp, + PathMappings Mappings) { + return std::make_unique(std::move(Transp), Mappings); +} + +} // namespace clangd +} // namespace clang Index: clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h =================================================================== --- /dev/null +++ clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h @@ -0,0 +1,4 @@ +#ifndef FOO_H +#define FOO_H +int foo() { return 42; } +#endif Index: clang-tools-extra/clangd/test/path-mappings.test =================================================================== --- /dev/null +++ clang-tools-extra/clangd/test/path-mappings.test @@ -0,0 +1,65 @@ +# RUN: clangd --path-mappings 'C:\client=%t/server' -lit-test < %s | FileCheck -strict-whitespace %s +# Copy over the server file into test workspace +# RUN: rm -rf %t +# RUN: cp -r %S/Inputs/path-mappings %t +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///C:/client/bar.cpp", + "languageId": "cpp", + "version": 1, + "text": "#include \"foo.h\"\nint main(){\nreturn foo();\n}" + } + } +} +--- +# Ensure that the client gets back the same client path (clangd thinks it edited %t/server/bar.cpp) +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [], +# CHECK-NEXT: "uri": "file:///C:/client/bar.cpp" +# CHECK-NEXT: } +--- +# We're editing bar.cpp, which includes foo.h, where foo.h "exists" at a server location +# With path mappings, when we go to definition on foo(), we get back a client file uri +{ + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/definition", + "params": { + "textDocument": { + "uri": "file:///C:/client/bar.cpp" + }, + "position": { + "line": 2, + "character": 8 + } + } +} +--- +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": {{[0-9]+}}, +# CHECK-NEXT: "line": {{[0-9]+}} +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": {{[0-9]+}}, +# CHECK-NEXT: "line": {{[0-9]+}} +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file:///C:/client/foo.h" +# CHECK-NEXT: } +# CHECK-NEXT: ] +# +--- +{"jsonrpc":"2.0","id":2,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: clang-tools-extra/clangd/tool/ClangdMain.cpp =================================================================== --- clang-tools-extra/clangd/tool/ClangdMain.cpp +++ clang-tools-extra/clangd/tool/ClangdMain.cpp @@ -10,6 +10,7 @@ #include "CodeComplete.h" #include "Features.inc" #include "Path.h" +#include "PathMapping.h" #include "Protocol.h" #include "Shutdown.h" #include "Trace.h" @@ -350,6 +351,19 @@ Hidden, }; +static llvm::cl::opt PathMappingsArg( + "path-mappings", + cat(Protocol), + llvm::cl::desc( + "Comma separated list of '=' pairs " + "that can be used to map between file locations on the client " + "and and a remote " + "location where clangd is running," + "e.g. " + "/home/project=/workarea/project,/home/project/.includes=/opt/include" + ), + llvm::cl::init("")); + opt InputMirrorFile{ "input-mirror-file", cat(Protocol), @@ -399,8 +413,8 @@ getAbsolutePath(llvm::StringRef /*Authority*/, llvm::StringRef Body, llvm::StringRef /*HintPath*/) const override { using namespace llvm::sys; - // Still require "/" in body to mimic file scheme, as we want lengths of an - // equivalent URI in both schemes to be the same. + // Still require "/" in body to mimic file scheme, as we want lengths of + // an equivalent URI in both schemes to be the same. if (!Body.startswith("/")) return llvm::make_error( "Expect URI body to be an absolute path starting with '/': " + Body, @@ -654,7 +668,16 @@ InputMirrorStream ? InputMirrorStream.getPointer() : nullptr, PrettyPrint, InputStyle); } - + if (!PathMappingsArg.empty()) { + auto Mappings = parsePathMappings(PathMappingsArg); + if (!Mappings) { + auto Err = Mappings.takeError(); + llvm::errs() << llvm::toString(std::move(Err)) << "\n"; + return 1; + } + TransportLayer = + createPathMappingTransport(std::move(TransportLayer), *Mappings); + } // Create an empty clang-tidy option. std::mutex ClangTidyOptMu; std::unique_ptr Index: clang-tools-extra/clangd/unittests/CMakeLists.txt =================================================================== --- clang-tools-extra/clangd/unittests/CMakeLists.txt +++ clang-tools-extra/clangd/unittests/CMakeLists.txt @@ -55,6 +55,7 @@ IndexTests.cpp JSONTransportTests.cpp ParsedASTTests.cpp + PathMappingTests.cpp PrintASTTests.cpp QualityTests.cpp RenameTests.cpp Index: clang-tools-extra/clangd/unittests/PathMappingTests.cpp =================================================================== --- /dev/null +++ clang-tools-extra/clangd/unittests/PathMappingTests.cpp @@ -0,0 +1,216 @@ +//===-- PathMappingTests.cpp ------------------------*- C++ -*-----------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "PathMapping.h" +#include "llvm/Support/JSON.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +namespace clang { +namespace clangd { +namespace { +using ::testing::ElementsAre; +MATCHER_P2(Mapping, ClientPath, ServerPath, "") { + return arg.ClientPath == ClientPath && arg.ServerPath == ServerPath; +} + +bool failedParse(llvm::StringRef RawMappings) { + llvm::Expected Mappings = parsePathMappings(RawMappings); + if (!Mappings) { + consumeError(Mappings.takeError()); + return true; + } + return false; +} + +TEST(ParsePathMappingTests, WindowsPath) { + // Relative path to C drive + EXPECT_TRUE(failedParse(R"(C:a=/root)")); + EXPECT_TRUE(failedParse(R"(\C:a=/root)")); + // Relative path to current drive. + EXPECT_TRUE(failedParse(R"(\a=/root)")); + // Absolute paths + llvm::Expected ParsedMappings = + parsePathMappings(R"(C:\a=/root)"); + ASSERT_TRUE(bool(ParsedMappings)); + EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping("/C:/a", "/root"))); + // Absolute UNC path + ParsedMappings = parsePathMappings(R"(\\Server\C$=/root)"); + ASSERT_TRUE(bool(ParsedMappings)); + EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping("//Server/C$", "/root"))); +} + +TEST(ParsePathMappingTests, UnixPath) { + // Relative unix path + EXPECT_TRUE(failedParse("a/b=/root")); + // Absolute unix path + llvm::Expected ParsedMappings = parsePathMappings("/A/b=/root"); + ASSERT_TRUE(bool(ParsedMappings)); + EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping("/A/b", "/root"))); + // Aboslute unix path w/ backslash + ParsedMappings = parsePathMappings(R"(/a/b\\ar=/root)"); + ASSERT_TRUE(bool(ParsedMappings)); + EXPECT_THAT(*ParsedMappings, ElementsAre(Mapping(R"(/a/b\\ar)", "/root"))); +} + +TEST(ParsePathMappingTests, ImproperFormat) { + // uneven mappings + EXPECT_TRUE(failedParse("/home/myuser1=")); + // mappings need to be absolute + EXPECT_TRUE(failedParse("home/project=/workarea/project")); + // duplicate delimiter + EXPECT_TRUE(failedParse("/home==/workarea")); + // no delimiter + EXPECT_TRUE(failedParse("/home")); + // improper delimiter + EXPECT_TRUE(failedParse("/home,/workarea")); +} + +TEST(ParsePathMappingTests, ParsesMultiple) { + std::string RawPathMappings = + "/home/project=/workarea/project,/home/project/.includes=/opt/include"; + auto Parsed = parsePathMappings(RawPathMappings); + ASSERT_TRUE(bool(Parsed)); + EXPECT_THAT(*Parsed, + ElementsAre(Mapping("/home/project", "/workarea/project"), + Mapping("/home/project/.includes", "/opt/include"))); +} + +bool mapsProperly(llvm::StringRef Orig, llvm::StringRef Expected, + llvm::StringRef RawMappings, PathMapping::Direction Dir) { + llvm::Expected Mappings = parsePathMappings(RawMappings); + if (!Mappings) + return false; + llvm::Optional MappedPath = doPathMapping(Orig, Dir, *Mappings); + std::string Actual = MappedPath ? *MappedPath : Orig.str(); + EXPECT_STREQ(Expected.str().c_str(), Actual.c_str()); + return Expected == Actual; +} + +TEST(DoPathMappingTests, PreservesOriginal) { + // Preserves original path when no mapping + EXPECT_TRUE(mapsProperly("file:///home", "file:///home", "", + PathMapping::Direction::ClientToServer)); +} + +TEST(DoPathMappingTests, UsesFirstMatch) { + EXPECT_TRUE(mapsProperly("file:///home/foo.cpp", "file:///workarea1/foo.cpp", + "/home=/workarea1,/home=/workarea2", + PathMapping::Direction::ClientToServer)); +} + +TEST(DoPathMappingTests, IgnoresSubstrings) { + // Doesn't map substrings that aren't a proper path prefix + EXPECT_TRUE(mapsProperly("file://home/foo-bar.cpp", "file://home/foo-bar.cpp", + "/home/foo=/home/bar", + PathMapping::Direction::ClientToServer)); +} + +TEST(DoPathMappingTests, MapsOutgoingPaths) { + // When IsIncoming is false (i.e.a response), map the other way + EXPECT_TRUE(mapsProperly("file:///workarea/foo.cpp", "file:///home/foo.cpp", + "/home=/workarea", + PathMapping::Direction::ServerToClient)); +} + +TEST(DoPathMappingTests, OnlyMapFileUris) { + EXPECT_TRUE(mapsProperly("test:///home/foo.cpp", "test:///home/foo.cpp", + "/home=/workarea", + PathMapping::Direction::ClientToServer)); +} + +TEST(DoPathMappingTests, RespectsCaseSensitivity) { + EXPECT_TRUE(mapsProperly("file:///HOME/foo.cpp", "file:///HOME/foo.cpp", + "/home=/workarea", + PathMapping::Direction::ClientToServer)); +} + +TEST(DoPathMappingTests, MapsWindowsPaths) { + // Maps windows properly + EXPECT_TRUE(mapsProperly("file:///C:/home/foo.cpp", + "file:///C:/workarea/foo.cpp", R"(C:\home=C:\workarea)", + PathMapping::Direction::ClientToServer)); +} + +TEST(DoPathMappingTests, MapsWindowsUnixInterop) { + // Path mappings with a windows-style client path and unix-style server path + EXPECT_TRUE(mapsProperly( + "file:///C:/home/foo.cpp", "file:///workarea/foo.cpp", + R"(C:\home=/workarea)", PathMapping::Direction::ClientToServer)); +} + +TEST(ApplyPathMappingTests, PreservesOriginalParams) { + auto Params = llvm::json::parse(R"({ + "textDocument": {"uri": "file:///home/foo.cpp"}, + "position": {"line": 0, "character": 0} + })"); + ASSERT_TRUE(bool(Params)); + llvm::json::Value ExpectedParams = *Params; + PathMappings Mappings; + applyPathMappings(*Params, PathMapping::Direction::ClientToServer, Mappings); + EXPECT_EQ(*Params, ExpectedParams); +} + +TEST(ApplyPathMappingTests, MapsAllMatchingPaths) { + // Handles nested objects and array values + auto Params = llvm::json::parse(R"({ + "rootUri": {"uri": "file:///home/foo.cpp"}, + "workspaceFolders": ["file:///home/src", "file:///tmp"] + })"); + auto ExpectedParams = llvm::json::parse(R"({ + "rootUri": {"uri": "file:///workarea/foo.cpp"}, + "workspaceFolders": ["file:///workarea/src", "file:///tmp"] + })"); + auto Mappings = parsePathMappings("/home=/workarea"); + ASSERT_TRUE(bool(Params) && bool(ExpectedParams) && bool(Mappings)); + applyPathMappings(*Params, PathMapping::Direction::ClientToServer, *Mappings); + EXPECT_EQ(*Params, *ExpectedParams); +} + +TEST(ApplyPathMappingTests, MapsOutbound) { + auto Params = llvm::json::parse(R"({ + "id": 1, + "result": [ + {"uri": "file:///opt/include/foo.h"}, + {"uri": "file:///workarea/src/foo.cpp"}] + })"); + auto ExpectedParams = llvm::json::parse(R"({ + "id": 1, + "result": [ + {"uri": "file:///home/.includes/foo.h"}, + {"uri": "file:///home/src/foo.cpp"}] + })"); + auto Mappings = + parsePathMappings("/home=/workarea,/home/.includes=/opt/include"); + ASSERT_TRUE(bool(Params) && bool(ExpectedParams) && bool(Mappings)); + applyPathMappings(*Params, PathMapping::Direction::ServerToClient, *Mappings); + EXPECT_EQ(*Params, *ExpectedParams); +} + +TEST(ApplyPathMappingTests, MapsKeys) { + auto Params = llvm::json::parse(R"({ + "changes": { + "file:///home/foo.cpp": {"newText": "..."}, + "file:///home/src/bar.cpp": {"newText": "..."} + } + })"); + auto ExpectedParams = llvm::json::parse(R"({ + "changes": { + "file:///workarea/foo.cpp": {"newText": "..."}, + "file:///workarea/src/bar.cpp": {"newText": "..."} + } + })"); + auto Mappings = parsePathMappings("/home=/workarea"); + ASSERT_TRUE(bool(Params) && bool(ExpectedParams) && bool(Mappings)); + applyPathMappings(*Params, PathMapping::Direction::ClientToServer, *Mappings); + EXPECT_EQ(*Params, *ExpectedParams); +} + +} // namespace +} // namespace clangd +} // namespace clang