wwagner19 updated this revision to Diff 227643.
wwagner19 added a comment.
Herald added a subscriber: usaxena95.

Unfortunately, I had to take a bit of a hiatus there, but i'm back a few months 
later with an updated patch incorporating all of @sammccall 's feedback! 
Notably,

- Windows paths are now accounted for, basically we first try to parse a unix 
path, and fall back to windows if possible. After, windows paths are converted 
to forward-slash notation, so the prefix stuff can work.
- Mapping LSP jsonrpc keys is now done, albeit a bit awkward due to no delete 
key/value API
- The lit test is improved, as it no longer relies on background index and 
tests windows client path

As for the validity of this feature, I am well aware of vscode's remote 
feature, but it is still essential for vim/emacs/etc. editors, IMO.

Please take a look when you have a chance, thanks.


CHANGES SINCE LAST ACTION
  https://reviews.llvm.org/D64305/new/

https://reviews.llvm.org/D64305

Files:
  clang-tools-extra/clangd/PathMapping.cpp
  clang-tools-extra/clangd/PathMapping.h
  clang-tools-extra/clangd/test/Inputs/path-mappings/compile_commands.json
  clang-tools-extra/clangd/test/Inputs/path-mappings/definition.jsonrpc
  clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.cpp
  clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.h
  clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h
  clang-tools-extra/clangd/test/path-mappings.test
  clang-tools-extra/clangd/tool/ClangdMain.cpp
  clang-tools-extra/clangd/unittests/PathMappingTests.cpp

Index: clang-tools-extra/clangd/unittests/PathMappingTests.cpp
===================================================================
--- clang-tools-extra/clangd/unittests/PathMappingTests.cpp
+++ clang-tools-extra/clangd/unittests/PathMappingTests.cpp
@@ -10,121 +10,200 @@
 #include "llvm/Support/JSON.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
-
+#include <string>
 namespace clang {
 namespace clangd {
 namespace {
 using ::testing::ElementsAre;
-using ::testing::Pair;
-
-TEST(ParsePathMappingTests, ParseFailed) {
-  auto FailedParse = [](const std::vector<std::string> &RawMappings) {
-    auto Mappings = parsePathMappings(RawMappings);
-    if (!Mappings) {
-      consumeError(Mappings.takeError());
-      return true;
-    }
-    return false;
-  };
+MATCHER_P2(Mapping, ClientPath, ServerPath, "") {
+  return arg.ClientPath == ClientPath && arg.ServerPath == ServerPath;
+}
+
+bool failedParse(llvm::StringRef RawMappings) {
+  llvm::Expected<PathMappings> 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<PathMappings> 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<PathMappings> 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|"}));
+  EXPECT_TRUE(failedParse("/home/myuser1="));
   // mappings need to be absolute
-  EXPECT_TRUE(FailedParse({"home/project|/workarea/project"}));
-  // improper delimiter
-  EXPECT_TRUE(FailedParse({"/home||/workarea"}));
+  EXPECT_TRUE(failedParse("home/project=/workarea/project"));
+  // duplicate delimiter
+  EXPECT_TRUE(failedParse("/home==/workarea"));
   // no delimiter
-  EXPECT_TRUE(FailedParse({"/home"}));
+  EXPECT_TRUE(failedParse("/home"));
+  // improper delimiter
+  EXPECT_TRUE(failedParse("/home,/workarea"));
 }
 
-TEST(ParsePathMappingTests, AllowsWindowsAndUnixPaths) {
-  std::vector<std::string> RawPathMappings = {
-      "/C:/home/project|/workarea/project",
-      "/home/project/.includes|/C:/opt/include"};
+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(Pair("/C:/home/project", "/workarea/project"),
-                          Pair("/home/project/.includes", "/C:/opt/include")));
+              ElementsAre(Mapping("/home/project", "/workarea/project"),
+                          Mapping("/home/project/.includes", "/opt/include")));
 }
 
-TEST(ParsePathMappingTests, ParsesCorrectly) {
-  std::vector<std::string> RawPathMappings = {
-      "/home/project|/workarea/project",
-      "/home/project/.includes|/opt/include"};
-  auto Parsed = parsePathMappings(RawPathMappings);
-  ASSERT_TRUE(bool(Parsed));
-  EXPECT_THAT(*Parsed,
-              ElementsAre(Pair("/home/project", "/workarea/project"),
-                          Pair("/home/project/.includes", "/opt/include")));
+bool mapsProperly(llvm::StringRef Orig, llvm::StringRef Expected,
+                  llvm::StringRef RawMappings, bool IsIncoming) {
+  llvm::Expected<PathMappings> Mappings = parsePathMappings(RawMappings);
+  if (!Mappings)
+    return false;
+  llvm::Optional<std::string> MappedPath =
+      doPathMapping(Orig, IsIncoming, *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", "", true));
+}
+
+TEST(DoPathMappingTests, UsesFirstMatch) {
+  EXPECT_TRUE(mapsProperly("file:///home/foo.cpp", "file:///workarea1/foo.cpp",
+                           "/home=/workarea1,/home=/workarea2", true));
+}
+
+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", true));
 }
 
-TEST(DoPathMappingTests, PreservesOriginalParams) {
+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", false));
+}
+
+TEST(DoPathMappingTests, OnlyMapFileUris) {
+  EXPECT_TRUE(mapsProperly("test:///home/foo.cpp", "test:///home/foo.cpp",
+                           "/home=/workarea", true));
+}
+
+TEST(DoPathMappingTests, RespectsCaseSensitivity) {
+  EXPECT_TRUE(mapsProperly("file:///HOME/foo.cpp", "file:///HOME/foo.cpp",
+                           "/home=/workarea", true));
+}
+
+TEST(DoPathMappingTests, MapsWindowsPaths) {
+  // Maps windows properly
+  EXPECT_TRUE(mapsProperly("file:///C:/home/foo.cpp",
+                           "file:///C:/workarea/foo.cpp", "C:/home=C:/workarea",
+                           true));
+}
+
+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:///C:/workarea/foo.cpp",
+                           "C:/home=/C:/workarea", true));
+}
+
+TEST(ApplyPathMappingTests, PreservesOriginalParams) {
   auto Params = llvm::json::parse(R"({
     "textDocument": {"uri": "file:///home/foo.cpp"},
     "position": {"line": 0, "character": 0}
   })");
   ASSERT_TRUE(bool(Params));
-  auto MappedParams =
-      doPathMapping(*Params, /*IsIncoming=*/true, /*Mappings=*/{});
-  EXPECT_EQ(MappedParams, *Params);
-}
-
-TEST(DoPathMappingTests, MapsUsingFirstMatch) {
-  auto Params = llvm::json::parse(R"({
-        "textDocument": {"uri": "file:///home/project/foo.cpp"},
-        "position": {"line": 0, "character": 0}
-    })");
-  auto ExpectedParams = llvm::json::parse(R"({
-        "textDocument": {"uri": "file:///workarea1/project/foo.cpp"},
-        "position": {"line": 0, "character": 0}
-    })");
-  ASSERT_TRUE(bool(Params) && bool(ExpectedParams));
-  PathMappings Mappings{{"/home", "/workarea1"}, {"/home", "/workarea2"}};
-  auto MappedParams = doPathMapping(*Params, /*IsIncoming=*/true, Mappings);
-  EXPECT_EQ(MappedParams, *ExpectedParams);
+  llvm::json::Value ExpectedParams = *Params;
+  PathMappings Mappings;
+  applyPathMappings(*Params, /*IsIncoming=*/true, Mappings);
+  EXPECT_EQ(*Params, ExpectedParams);
 }
 
-TEST(DoPathMappingTests, MapsOutgoing) {
+TEST(ApplyPathMappingTests, MapsAllMatchingPaths) {
+  // Handles nested objects and array values
   auto Params = llvm::json::parse(R"({
-        "result": "file:///opt/include/foo.h"
-    })");
+    "rootUri": {"uri": "file:///home/foo.cpp"},
+    "workspaceFolders": ["file:///home/src", "file:///tmp"]
+  })");
   auto ExpectedParams = llvm::json::parse(R"({
-        "result": "file:///home/project/.includes/foo.h"
-    })");
-  ASSERT_TRUE(bool(Params) && bool(ExpectedParams));
-  PathMappings Mappings{{"/home/project/.includes", "/opt/include"}};
-  auto MappedParams = doPathMapping(*Params, /*IsIncoming=*/false, Mappings);
-  EXPECT_EQ(MappedParams, *ExpectedParams);
+    "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, /*IsIncoming=*/true, *Mappings);
+  EXPECT_EQ(*Params, *ExpectedParams);
 }
 
-TEST(DoPathMappingTests, MapsAllMatchingPaths) {
+TEST(ApplyPathMappingTests, MapsOutbound) {
   auto Params = llvm::json::parse(R"({
-        "rootUri": "file:///home/project",
-        "workspaceFolders": ["file:///home/misc/project2"]
-    })");
+    "id": 1,
+    "result": [
+      {"uri": "file:///opt/include/foo.h"},
+      {"uri": "file:///workarea/src/foo.cpp"}]
+  })");
   auto ExpectedParams = llvm::json::parse(R"({
-        "rootUri": "file:///workarea/project",
-        "workspaceFolders": ["file:///workarea/misc/project2"]
-    })");
-  ASSERT_TRUE(bool(Params) && bool(ExpectedParams));
-  PathMappings Mappings{{"/home", "/workarea"}};
-  auto MappedParams = doPathMapping(*Params, /*IsIncoming=*/true, Mappings);
-  EXPECT_EQ(MappedParams, *ExpectedParams);
+    "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, /*IsIncoming=*/false, *Mappings);
+  EXPECT_EQ(*Params, *ExpectedParams);
 }
 
-TEST(DoPathMappingTests, OnlyMapsFileUris) {
+TEST(ApplyPathMappingTests, MapsKeys) {
   auto Params = llvm::json::parse(R"({
-        "rootUri": "file:///home/project",
-        "workspaceFolders": ["test:///home/misc/project2"]
-    })");
+    "changes": {
+      "file:///home/foo.cpp": {"newText": "..."},
+      "file:///home/src/bar.cpp": {"newText": "..."}
+    }
+  })");
   auto ExpectedParams = llvm::json::parse(R"({
-        "rootUri": "file:///workarea/project",
-        "workspaceFolders": ["test:///home/misc/project2"]
-    })");
-  ASSERT_TRUE(bool(Params) && bool(ExpectedParams));
-  PathMappings Mappings{{"/home", "/workarea"}};
-  auto MappedParams = doPathMapping(*Params, /*IsIncoming=*/true, Mappings);
-  EXPECT_EQ(MappedParams, *ExpectedParams);
+    "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, /*IsIncoming=*/true, *Mappings);
+  EXPECT_EQ(*Params, *ExpectedParams);
 }
 
 } // namespace
Index: clang-tools-extra/clangd/tool/ClangdMain.cpp
===================================================================
--- clang-tools-extra/clangd/tool/ClangdMain.cpp
+++ clang-tools-extra/clangd/tool/ClangdMain.cpp
@@ -336,16 +336,18 @@
     Hidden,
 };
 
-static llvm::cl::list<std::string> PathMappingsArg(
+static llvm::cl::opt<std::string> PathMappingsArg(
     "path-mappings",
-    llvm::cl::desc("Comma separated list of '<host_path>|<remote_path>' pairs "
-                   "that can be used to map between file locations on the host "
-                   "and and a remote "
-                   "location where clangd is running,"
-                   "e.g. "
-                   "/home/project/|/workarea/project,/home/project/.includes|/"
-                   "opt/include"),
-    llvm::cl::CommaSeparated);
+    cat(Protocol),
+    llvm::cl::desc(
+        "Comma separated list of '<client_path>=<server_path>' 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"
+        "opt/include"),
+    llvm::cl::init(""));
 
 opt<Path> InputMirrorFile{
     "input-mirror-file",
@@ -396,8 +398,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<llvm::StringError>(
           "Expect URI body to be an absolute path starting with '/': " + Body,
@@ -645,7 +647,7 @@
         InputMirrorStream ? InputMirrorStream.getPointer() : nullptr,
         PrettyPrint, InputStyle);
   }
-  if (PathMappingsArg.size()) {
+  if (!PathMappingsArg.empty()) {
     auto Mappings = parsePathMappings(PathMappingsArg);
     if (!Mappings) {
       auto Err = Mappings.takeError();
Index: clang-tools-extra/clangd/test/path-mappings.test
===================================================================
--- clang-tools-extra/clangd/test/path-mappings.test
+++ clang-tools-extra/clangd/test/path-mappings.test
@@ -1,13 +1,65 @@
-# We need to splice paths into file:// URIs for this test.
-# UNSUPPORTED: win32
-
-# Use a copy of inputs, as we'll mutate it
+# 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
-# Need to embed the correct temp path in the actual JSON-RPC requests.
-# RUN: sed -i "s|DIRECTORY|%t|" `find %t -type f`
-
-# We're editing bar.cpp, which includes foo.h, where foo.h/cpp only "exist" in the remote.
-# With path mappings, when we go to definition on foo(), we get back a host file uri
-# RUN: clangd -background-index -background-index-rebuild-period=0 --path-mappings '%t/host|%t/remote' -lit-test < %t/definition.jsonrpc | FileCheck %t/definition.jsonrpc
-
+{"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/test/Inputs/path-mappings/server/foo.h
===================================================================
--- clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h
+++ clang-tools-extra/clangd/test/Inputs/path-mappings/server/foo.h
@@ -1,4 +1,4 @@
 #ifndef FOO_H
 #define FOO_H
-int foo();
+int foo() { return 42; }
 #endif
Index: clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.cpp
===================================================================
--- clang-tools-extra/clangd/test/Inputs/path-mappings/remote/foo.cpp
+++ /dev/null
@@ -1,2 +0,0 @@
-#include "foo.h"
-int foo() { return 42; }
Index: clang-tools-extra/clangd/test/Inputs/path-mappings/definition.jsonrpc
===================================================================
--- clang-tools-extra/clangd/test/Inputs/path-mappings/definition.jsonrpc
+++ /dev/null
@@ -1,51 +0,0 @@
-{
-  "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://DIRECTORY/host/bar.cpp",
-      "languageId": "cpp",
-      "version": 1,
-      "text": "#include \"foo.h\"\nint main(){\nreturn foo();\n}"
-    }
-  }
-}
----
-{
-  "jsonrpc": "2.0",
-  "id": 1,
-  "method": "sync",
-  "params": null
-}
----
-{
-  "jsonrpc": "2.0",
-  "id": 2,
-  "method": "textDocument/definition",
-  "params": {
-    "textDocument": {
-      "uri": "file://DIRECTORY/host/bar.cpp"
-    },
-    "position": {
-      "line": 2,
-      "character": 8
-    }
-  }
-}
-# CHECK: "uri": "file://{{.*}}/host/foo.cpp"
----
-{"jsonrpc":"2.0","id":3,"method":"shutdown"}
----
-{"jsonrpc":"2.0","method":"exit"}
Index: clang-tools-extra/clangd/test/Inputs/path-mappings/compile_commands.json
===================================================================
--- clang-tools-extra/clangd/test/Inputs/path-mappings/compile_commands.json
+++ /dev/null
@@ -1,5 +0,0 @@
-[{
-  "directory": "DIRECTORY",
-  "command": "clang DIRECTORY/remote/foo.cpp",
-  "file": "DIRECTORY/remote/foo.cpp"
-}]
Index: clang-tools-extra/clangd/PathMapping.h
===================================================================
--- clang-tools-extra/clangd/PathMapping.h
+++ clang-tools-extra/clangd/PathMapping.h
@@ -5,12 +5,13 @@
 // 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 <memory>
 #include <tuple>
-#include <utility>
 #include <vector>
 
 namespace clang {
@@ -18,29 +19,42 @@
 
 class Transport;
 
-/// PathMappings are a collection of paired host and remote paths.
+/// 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 host environment may have source files or dependencies
-/// at different locations than the remote.
+/// LSP messages, as the client's environment may have source files or
+/// dependencies at different locations than the server.
 ///
 /// For example, if the mappings were {{"/home/user", "/workarea"}}, then
 /// an inbound 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).
-using PathMappings = std::vector<std::pair<std::string, std::string>>;
+struct PathMapping {
+  PathMapping() {}
+  PathMapping(llvm::StringRef ClientPath, llvm::StringRef ServerPath)
+      : ClientPath(ClientPath), ServerPath(ServerPath) {}
+  std::string ClientPath;
+  std::string ServerPath;
+};
+using PathMappings = std::vector<PathMapping>;
 
-/// Parse the command line \pRawPathMappings (e.g. "/host|/remote") into
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const PathMapping &M);
+
+/// Parse the command line \pRawPathMappings (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<PathMappings>
-parsePathMappings(const std::vector<std::string> &RawPathMappings);
+llvm::Expected<PathMappings> parsePathMappings(llvm::StringRef RawPathMappings);
+
+/// Returns a modified \pS with the first matching path in \pPathMappings
+/// substituted, if applicable
+llvm::Optional<std::string> doPathMapping(llvm::StringRef S, bool IsIncoming,
+                                          const PathMappings &Mappings);
 
-/// Returns an altered \pParams, where all the file:// URIs have the \pMappings
-/// applied. \pIsIncoming affects which direction the mappings are applied.
+/// Applies the \pMappings to all the file:// URIs in \pParams.
+/// \pIsIncoming affects which direction the mappings are applied.
 /// NOTE: The first matching mapping will be applied, otherwise \pParams will be
 /// untouched.
-llvm::json::Value doPathMapping(const llvm::json::Value &Params,
-                                bool IsIncoming, const PathMappings &Mappings);
+void applyPathMappings(llvm::json::Value &Params, bool IsIncoming,
+                       const PathMappings &Mappings);
 
 /// Creates a wrapping transport over \pTransp that applies the \pMappings to
 /// all inbound and outbound LSP messages. All calls are then delegated to the
Index: clang-tools-extra/clangd/PathMapping.cpp
===================================================================
--- clang-tools-extra/clangd/PathMapping.cpp
+++ clang-tools-extra/clangd/PathMapping.cpp
@@ -9,11 +9,16 @@
 #include "Logger.h"
 #include "Transport.h"
 #include "URI.h"
+#include "llvm/ADT/None.h"
+#include "llvm/ADT/Optional.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallString.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Errno.h"
+#include "llvm/Support/JSON.h"
 #include "llvm/Support/Path.h"
 #include "llvm/Support/raw_ostream.h"
+#include <algorithm>
 #include <memory>
 #include <tuple>
 #include <vector>
@@ -21,22 +26,40 @@
 namespace clang {
 namespace clangd {
 namespace {
+using MapperFunc = llvm::function_ref<llvm::Optional<std::string>(
+    llvm::StringRef, bool, const PathMappings &)>;
 
-// Recurively apply the \pMF function on every string value in \pV
-template <typename MapperFunc>
-void recursivelyMap(llvm::json::Value &V, const MapperFunc &MF) {
+// Recursively apply the \pMF to all string keys/values in \pV
+void recursivelyMap(llvm::json::Value &V, bool IsIncoming,
+                    const PathMappings &Mappings, const MapperFunc &MF) {
   using Kind = llvm::json::Value::Kind;
-  const auto &K = V.kind();
+  const Kind &K = V.kind();
   if (K == Kind::Object) {
-    for (auto &KV : *V.getAsObject()) {
-      recursivelyMap(KV.second, MF);
+    llvm::json::Object *Obj = V.getAsObject();
+    llvm::json::Object MappedObj;
+    // 1. Map all the Keys
+    for (auto &KV : *Obj) {
+      if (llvm::Optional<std::string> MappedKey =
+              MF(KV.first.str(), IsIncoming, 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, IsIncoming, Mappings, MF);
     }
   } else if (K == Kind::Array) {
-    for (auto &Val : *V.getAsArray()) {
-      recursivelyMap(Val, MF);
+    for (llvm::json::Value &Val : *V.getAsArray()) {
+      recursivelyMap(Val, IsIncoming, Mappings, MF);
     }
   } else if (K == Kind::String) {
-    V = MF(*V.getAsString());
+    if (llvm::Optional<std::string> Mapped =
+            MF(*V.getAsString(), IsIncoming, Mappings)) {
+      V = std::move(*Mapped);
+    }
   }
 }
 
@@ -47,23 +70,20 @@
       : WrappedHandler(Handler), Mappings(Mappings) {}
 
   bool onNotify(llvm::StringRef Method, llvm::json::Value Params) override {
-    llvm::json::Value MappedParams =
-        doPathMapping(Params, /*IsIncoming=*/true, Mappings);
-    return WrappedHandler.onNotify(Method, std::move(MappedParams));
+    applyPathMappings(Params, /*IsIncoming=*/true, Mappings);
+    return WrappedHandler.onNotify(Method, std::move(Params));
   }
 
   bool onCall(llvm::StringRef Method, llvm::json::Value Params,
               llvm::json::Value ID) override {
-    llvm::json::Value MappedParams =
-        doPathMapping(Params, /*IsIncoming=*/true, Mappings);
-    return WrappedHandler.onCall(Method, std::move(MappedParams),
-                                 std::move(ID));
+    applyPathMappings(Params, /*IsIncoming=*/true, Mappings);
+    return WrappedHandler.onCall(Method, std::move(Params), std::move(ID));
   }
 
   bool onReply(llvm::json::Value ID,
                llvm::Expected<llvm::json::Value> Result) override {
     if (Result) {
-      Result = doPathMapping(*Result, /*IsIncoming=*/true, Mappings);
+      applyPathMappings(*Result, /*IsIncoming=*/true, Mappings);
     }
     return WrappedHandler.onReply(std::move(ID), std::move(Result));
   }
@@ -81,22 +101,20 @@
       : WrappedTransport(std::move(Transp)), Mappings(std::move(Mappings)) {}
 
   void notify(llvm::StringRef Method, llvm::json::Value Params) override {
-    llvm::json::Value MappedParams =
-        doPathMapping(Params, /*IsIncoming=*/false, Mappings);
-    WrappedTransport->notify(Method, std::move(MappedParams));
+    applyPathMappings(Params, /*IsIncoming=*/false, Mappings);
+    WrappedTransport->notify(Method, std::move(Params));
   }
 
   void call(llvm::StringRef Method, llvm::json::Value Params,
             llvm::json::Value ID) override {
-    llvm::json::Value MappedParams =
-        doPathMapping(Params, /*IsIncoming=*/false, Mappings);
-    WrappedTransport->call(Method, std::move(MappedParams), std::move(ID));
+    applyPathMappings(Params, /*IsIncoming=*/false, Mappings);
+    WrappedTransport->call(Method, std::move(Params), std::move(ID));
   }
 
   void reply(llvm::json::Value ID,
              llvm::Expected<llvm::json::Value> Result) override {
     if (Result) {
-      Result = doPathMapping(*Result, /*IsIncoming=*/false, Mappings);
+      applyPathMappings(*Result, /*IsIncoming=*/false, Mappings);
     }
     WrappedTransport->reply(std::move(ID), std::move(Result));
   }
@@ -116,71 +134,100 @@
                                              llvm::inconvertibleErrorCode());
 }
 
+// Returns whether a path mapping should take place for \pOrigPath
+// i.e. \pMappingPath is a proper sub-path of \p OrigPath
+bool mappingMatches(llvm::StringRef OrigPath, llvm::StringRef MappingPath) {
+  namespace path = llvm::sys::path;
+  auto OrigPathItr = path::begin(OrigPath, path::Style::posix);
+  auto OrigPathEnd = path::end(OrigPath);
+  auto MappingPathItr = path::begin(MappingPath, path::Style::posix);
+  auto MappingPathEnd = path::end(MappingPath);
+  if (std::distance(OrigPathItr, OrigPathEnd) <
+      std::distance(MappingPathItr, MappingPathEnd)) {
+    return false;
+  }
+  return std::equal(MappingPathItr, MappingPathEnd, OrigPathItr);
+}
+
+// Converts \pPath to a posix-style absolute, i.e. if it's a windows path
+// then the backward-slash notation will be converted to forward slash
+llvm::Expected<std::string> parsePath(llvm::StringRef Path) {
+  namespace path = llvm::sys::path;
+  std::string ParsedPath;
+  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<PathMappings>
-parsePathMappings(const std::vector<std::string> &RawPathMappings) {
-  if (!RawPathMappings.size()) {
-    return make_string_error("Must provide at least one path mapping");
-  }
-  llvm::StringRef HostPath, RemotePath;
+parsePathMappings(llvm::StringRef RawPathMappings) {
+  llvm::StringRef ClientPath, ServerPath, PathPair, Rest = RawPathMappings;
   PathMappings ParsedMappings;
-  for (llvm::StringRef PathPair : RawPathMappings) {
-    std::tie(HostPath, RemotePath) = PathPair.split("|");
-    if (HostPath.empty() || RemotePath.empty()) {
-      return make_string_error("Not a valid path mapping: " + PathPair);
+  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);
     }
-    if (!llvm::sys::path::is_absolute(HostPath)) {
-      return make_string_error("Path mapping not absolute: " + HostPath);
-    } else if (!llvm::sys::path::is_absolute(RemotePath)) {
-      return make_string_error("Path mapping not absolute: " + RemotePath);
+    llvm::Expected<std::string> ParsedClientPath = parsePath(ClientPath);
+    if (!ParsedClientPath) {
+      return ParsedClientPath.takeError();
     }
-    ParsedMappings.emplace_back(HostPath, RemotePath);
+    llvm::Expected<std::string> ParsedServerPath = parsePath(ServerPath);
+    if (!ParsedServerPath) {
+      return ParsedServerPath.takeError();
+    }
+    ParsedMappings.emplace_back(std::move(*ParsedClientPath),
+                                std::move(*ParsedServerPath));
   }
-  std::string S;
-  llvm::raw_string_ostream OS(S);
-  OS << "Parsed path mappings: ";
-  for (const auto &P : ParsedMappings)
-    OS << llvm::formatv("{0}:{1} ", P.first, P.second);
-  OS.flush();
-  vlog("{0}", OS.str());
   return ParsedMappings;
 }
 
-llvm::json::Value doPathMapping(const llvm::json::Value &Params,
-                                bool IsIncoming, const PathMappings &Mappings) {
-  llvm::json::Value MappedParams = Params;
-  recursivelyMap(
-      MappedParams, [&Mappings, IsIncoming](llvm::StringRef S) -> std::string {
-        if (!S.startswith("file://"))
-          return S;
-        auto Uri = URI::parse(S);
-        if (!Uri) {
-          vlog("Faled to parse URI: {0}\n", S);
-          return S;
-        }
-        for (const auto &Mapping : Mappings) {
-          const auto &From = IsIncoming ? Mapping.first : Mapping.second;
-          const auto &To = IsIncoming ? Mapping.second : Mapping.first;
-          if (Uri->body().startswith(From)) {
-            std::string MappedBody = Uri->body();
-            MappedBody.replace(MappedBody.find(From), From.length(), To);
-            auto MappedUri = URI(Uri->scheme(), Uri->authority(), MappedBody);
-            vlog("Mapped {0} file path from {1} to {2}",
-                 IsIncoming ? "incoming" : "outgoing", Uri->toString(),
-                 MappedUri.toString());
-            return MappedUri.toString();
-          }
-        }
-        return S;
-      });
-  return MappedParams;
+llvm::Optional<std::string> doPathMapping(llvm::StringRef S, bool IsIncoming,
+                                          const PathMappings &Mappings) {
+  if (!S.startswith("file://"))
+    return llvm::None;
+  auto Uri = URI::parse(S);
+  if (!Uri) {
+    return llvm::None;
+  }
+  for (const auto &Mapping : Mappings) {
+    const std::string &From =
+        IsIncoming ? Mapping.ClientPath : Mapping.ServerPath;
+    const std::string &To =
+        IsIncoming ? Mapping.ServerPath : Mapping.ClientPath;
+    if (mappingMatches(Uri->body(), From)) {
+      llvm::SmallString<256> MappedPath(Uri->body());
+      llvm::sys::path::replace_path_prefix(MappedPath, From, To);
+      auto MappedUri = URI(Uri->scheme(), Uri->authority(), MappedPath.c_str());
+      return MappedUri.toString();
+    }
+  }
+  return llvm::None;
+}
+
+void applyPathMappings(llvm::json::Value &Params, bool IsIncoming,
+                       const PathMappings &Mappings) {
+  recursivelyMap(Params, IsIncoming, Mappings, doPathMapping);
 }
 
 std::unique_ptr<Transport>
 createPathMappingTransport(std::unique_ptr<Transport> Transp,
                            PathMappings Mappings) {
-  return llvm::make_unique<PathMappingTransport>(std::move(Transp), Mappings);
+  return std::make_unique<PathMappingTransport>(std::move(Transp), Mappings);
 }
 
 } // namespace clangd
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to